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