open source is social v-it.org
0
fork

Configure Feed

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

at main 166 lines 5.2 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4import { Agent, AtpAgent } from '@atproto/api'; 5import { NodeOAuthClient } from '@atproto/oauth-client-node'; 6import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 7import { join } from 'node:path'; 8import { configDir, configPath } from './paths.js'; 9import { errorMessage } from './error-format.js'; 10 11const requestLock = async (_name, fn) => await fn(); 12 13const noopStore = { 14 set: async () => {}, 15 get: async () => undefined, 16 del: async () => {}, 17}; 18 19const clientMetadata = { 20 client_id: 'https://v-it.org/client-metadata.json', 21 client_name: 'vit CLI', 22 application_type: 'native', 23 grant_types: ['authorization_code', 'refresh_token'], 24 response_types: ['code'], 25 scope: 'atproto transition:generic', 26 token_endpoint_auth_method: 'none', 27 dpop_bound_access_tokens: true, 28 client_uri: 'https://v-it.org', 29}; 30 31export function createStore() { 32 const map = new Map(); 33 34 return { 35 set: async (key, value) => { 36 map.set(key, value); 37 }, 38 get: async (key) => map.get(key), 39 del: async (key) => { 40 map.delete(key); 41 }, 42 }; 43} 44 45export function createSessionStore() { 46 const sessionFile = configPath('session.json'); 47 let data = {}; 48 if (existsSync(sessionFile)) { 49 try { 50 data = JSON.parse(readFileSync(sessionFile, 'utf-8')); 51 } catch (err) { 52 console.warn(`warning: failed to read ${sessionFile}: ${errorMessage(err)}`); 53 } 54 } 55 return { 56 set: async (key, value) => { 57 data[key] = value; 58 mkdirSync(configDir, { recursive: true }); 59 writeFileSync(sessionFile, JSON.stringify(data, null, 2) + '\n'); 60 }, 61 get: async (key) => data[key], 62 del: async (key) => { 63 delete data[key]; 64 mkdirSync(configDir, { recursive: true }); 65 writeFileSync(sessionFile, JSON.stringify(data, null, 2) + '\n'); 66 }, 67 }; 68} 69 70export function checkSession(did) { 71 // Check project-local app-password session 72 const localPath = join(process.cwd(), '.vit', 'login.json'); 73 try { 74 if (existsSync(localPath)) { 75 const local = JSON.parse(readFileSync(localPath, 'utf-8')); 76 if (local.did === did && local.type === 'app-password' && local.session?.accessJwt) { 77 return did; 78 } 79 } 80 } catch (err) { 81 console.warn(`warning: failed to read ${localPath}: ${errorMessage(err)}`); 82 } 83 84 const sessionFile = configPath('session.json'); 85 if (!existsSync(sessionFile)) return null; 86 try { 87 const raw = readFileSync(sessionFile, 'utf-8'); 88 const data = JSON.parse(raw); 89 const entry = data[did]; 90 if (!entry) return null; 91 // App-password session in global store 92 if (entry.type === 'app-password') { 93 return entry.session?.accessJwt ? did : null; 94 } 95 // OAuth session 96 const tokenSet = entry?.tokenSet; 97 if (!tokenSet) return null; 98 const accessValid = tokenSet.expires_at && new Date(tokenSet.expires_at) > new Date(); 99 if (accessValid || tokenSet.refresh_token) return did; 100 return null; 101 } catch (err) { 102 console.warn(`warning: failed to read ${sessionFile}: ${errorMessage(err)}`); 103 return null; 104 } 105} 106 107export function createOAuthClient({ stateStore, sessionStore, redirectUri }) { 108 return new NodeOAuthClient({ 109 requestLock, 110 clientMetadata: { 111 ...clientMetadata, 112 redirect_uris: [redirectUri], 113 }, 114 stateStore, 115 sessionStore, 116 }); 117} 118 119export async function restoreAgent(did) { 120 // Check project-local app-password session 121 const localPath = join(process.cwd(), '.vit', 'login.json'); 122 try { 123 if (existsSync(localPath)) { 124 const local = JSON.parse(readFileSync(localPath, 'utf-8')); 125 if (local.did === did && local.type === 'app-password' && local.session) { 126 const agent = new AtpAgent({ service: local.service || 'https://bsky.social' }); 127 await agent.resumeSession(local.session); 128 return { agent, session: { did: local.did, handle: local.handle } }; 129 } 130 } 131 } catch (err) { 132 console.warn(`warning: failed to read ${localPath}: ${errorMessage(err)}`); 133 } 134 135 // Check global app-password session 136 const sessionFile = configPath('session.json'); 137 try { 138 if (existsSync(sessionFile)) { 139 const raw = readFileSync(sessionFile, 'utf-8'); 140 const data = JSON.parse(raw); 141 const entry = data[did]; 142 if (entry?.type === 'app-password' && entry.session) { 143 const agent = new AtpAgent({ service: entry.service || 'https://bsky.social' }); 144 await agent.resumeSession(entry.session); 145 return { agent, session: { did, handle: entry.session.handle } }; 146 } 147 } 148 } catch (err) { 149 console.warn(`warning: failed to read ${sessionFile}: ${errorMessage(err)}`); 150 } 151 152 // Existing OAuth restore path 153 const sessionStore = createSessionStore(); 154 const client = new NodeOAuthClient({ 155 handleResolver: { resolve() { throw new Error('handle resolution not needed for restore'); } }, 156 requestLock, 157 clientMetadata: { 158 ...clientMetadata, 159 redirect_uris: ['http://127.0.0.1'], 160 }, 161 stateStore: noopStore, 162 sessionStore, 163 }); 164 const session = await client.restore(did); 165 return { agent: new Agent(session), session }; 166}