open source is social v-it.org
0
fork

Configure Feed

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

Clean up OAuth plumbing: fix config paths, deduplicate session restore

- Fix env-paths suffix: ~/.config/vit/ instead of ~/.config/vit-nodejs/
- Rename bsky_session.json to session.json
- Fix client ID mismatch: skim/ship now use same default as login
- Extract restoreAgent() helper to DRY up skim/ship OAuth boilerplate
- Stop writing dead token fields to vit.json (only DID is needed)
- Replace stale skip-login check with session store lookup
- Remove --output flag from login (DPoP-bound tokens aren't portable)
- Remove clientId param from createOAuthClient (no callers use it)
- Un-export requestLock (only used internally)
- Remove stale bsky_session.json from .gitignore
- Update README and SKILL.md to match current CLI surface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+43 -81
-1
.gitignore
··· 8 8 9 9 # Environment 10 10 .env 11 - bsky_session.json 12 11 13 12 # vit project state (user-local) 14 13 .vit/*
+4 -9
README.md
··· 35 35 ### Usage 36 36 37 37 ```bash 38 - vit login --handle alice.bsky.social 38 + vit login alice.bsky.social 39 39 ``` 40 40 41 41 This will: 42 42 1. Start a temporary localhost callback server 43 43 2. Open your browser to the Bluesky authorization page 44 - 3. After you approve, print the DPoP-bound access token and DID 45 - 4. Save credentials (`did`, `access_token`, `refresh_token`, `expires_at`) to `vit.json` 44 + 3. After you approve, print your DID 45 + 4. Save your DID to `vit.json` and OAuth session to `session.json` 46 46 47 47 ### Options 48 48 49 - - `--handle <handle>` - Bluesky handle (required) 50 49 - `-v, --verbose` - Show discovery and protocol details 51 - - `--output <file>` - Save token JSON to a file 52 - 53 - ### Notes 54 - 55 - The access token is DPoP-bound, meaning it requires a DPoP proof JWT for each API request. The token cannot be used as a simple Bearer token. 50 + - `--reset` - Force re-login even if credentials are valid 56 51 57 52 ## firehose 58 53
+3 -2
skills/vit/SKILL.md
··· 22 22 | `vit init` | Check environment readiness, configure vit for first use | 23 23 | `vit setup` | Initialize user-level config | 24 24 | `vit doctor` | Check vit setup status (alias for init) | 25 - | `vit login --handle <h>` | Browser-based ATProto OAuth, saves tokens to vit.json | 25 + | `vit login <handle>` | Browser-based ATProto OAuth, saves DID to vit.json | 26 26 | `vit config [action]` | Read/write vit.json config (list, set, delete) | 27 27 | `vit firehose` | Listen to Bluesky Jetstream for custom record events | 28 28 | `vit ship <text>` | Write a cap to the authenticated PDS | ··· 68 68 ## Configuration 69 69 70 70 - **`.vit/`** — local project directory, stores config.json (beacon) and local state (JSONL logs) 71 - - **`vit.json`** — credentials (`did`, `access_token`, etc.) and config, written by `vit login` and `vit config` 71 + - **`vit.json`** — user config (`did`, `setup_at`, etc.), written by `vit login`, `vit setup`, and `vit config` 72 + - **`session.json`** — OAuth session data managed by the ATProto client, written by `vit login` 72 73 - **`vit config`** — read/write `vit.json` user-level config
+5 -26
src/cmd/login.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { writeFileSync } from 'node:fs'; 5 4 import { loadConfig, saveConfig } from '../lib/config.js'; 6 5 import { createOAuthClient, createSessionStore, createStore } from '../lib/oauth.js'; 7 6 ··· 11 10 .description('Log in to Bluesky via browser-based OAuth') 12 11 .argument('<handle>', 'Bluesky handle (e.g. alice.bsky.social)') 13 12 .option('-v, --verbose', 'Show discovery details') 14 - .option('--output <file>', 'Save token JSON to file') 15 13 .option('--reset', 'Force re-login even if credentials are valid') 16 14 .action(async (handle, opts) => { 17 - const { verbose, output, reset } = opts; 15 + const { verbose, reset } = opts; 18 16 handle = handle.replace(/^@/, ''); 19 17 20 18 if (!reset) { 21 19 const existing = loadConfig(); 22 - if (existing.did && existing.access_token && existing.expires_at) { 23 - const expiresAt = new Date(existing.expires_at).getTime(); 24 - if (expiresAt > Date.now() + 60_000) { 20 + if (existing.did) { 21 + const session = await createSessionStore().get(existing.did); 22 + if (session) { 25 23 console.log(`Already logged in as ${existing.did}`); 26 - console.log(`Token expires: ${existing.expires_at}`); 27 24 return; 28 25 } 29 26 } ··· 134 131 135 132 console.log(`DID: ${session.did}`); 136 133 137 - const sessionData = await sessionStore.get(session.did); 138 - const tokens = sessionData?.tokenSet ?? {}; 139 - const outputData = { 140 - did: session.did, 141 - accessToken: tokens.access_token ?? null, 142 - refreshToken: tokens.refresh_token ?? null, 143 - expiresAt: tokens.expires_at ?? null, 144 - }; 145 - 146 - console.log(JSON.stringify(outputData, null, 2)); 147 - 148 - if (output) { 149 - writeFileSync(output, `${JSON.stringify(outputData, null, 2)}\n`); 150 - } 151 - 152 134 const config = loadConfig(); 153 135 config.did = session.did; 154 - config.access_token = tokens.access_token; 155 - config.refresh_token = tokens.refresh_token; 156 - config.expires_at = tokens.expires_at; 157 136 saveConfig(config); 158 - console.log('\nCredentials saved to vit.json'); 137 + console.log('Logged in'); 159 138 } catch (err) { 160 139 console.error(err instanceof Error ? err.message : String(err)); 161 140 process.exitCode = 1;
+4 -19
src/cmd/ship.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { Agent } from '@atproto/api'; 5 4 import { TID } from '@atproto/common-web'; 6 5 import { loadConfig } from '../lib/config.js'; 7 - import { createOAuthClient, createSessionStore } from '../lib/oauth.js'; 6 + import { restoreAgent } from '../lib/oauth.js'; 8 7 import { appendLog } from '../lib/vit-dir.js'; 9 8 10 9 export default function register(program) { ··· 16 15 .action(async (text, opts) => { 17 16 try { 18 17 const { verbose } = opts; 19 - const envDid = loadConfig().did; 20 - const did = opts.did || envDid; 21 - if (verbose) console.log(`[verbose] Config loaded, DID: ${did}`); 18 + const did = opts.did || loadConfig().did; 19 + if (verbose) console.log(`[verbose] DID: ${did}`); 22 20 23 - const clientId = `http://localhost?redirect_uri=${encodeURIComponent('http://127.0.0.1')}&scope=${encodeURIComponent('atproto transition:generic')}`; 24 - const sessionStore = createSessionStore(); 25 - const client = createOAuthClient({ 26 - clientId, 27 - sessionStore, 28 - stateStore: { 29 - set: async () => {}, 30 - get: async () => undefined, 31 - del: async () => {}, 32 - }, 33 - redirectUri: 'http://127.0.0.1', 34 - }); 35 - const session = await client.restore(did); 21 + const { agent, session } = await restoreAgent(did); 36 22 if (verbose) console.log(`[verbose] Session restored, PDS: ${session.serverMetadata?.issuer}`); 37 - const agent = new Agent(session); 38 23 39 24 const record = { 40 25 $type: 'org.v-it.cap',
+4 -19
src/cmd/skim.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { Agent } from '@atproto/api'; 5 4 import { loadConfig } from '../lib/config.js'; 6 - import { createOAuthClient, createSessionStore } from '../lib/oauth.js'; 5 + import { restoreAgent } from '../lib/oauth.js'; 7 6 8 7 export default function register(program) { 9 8 program ··· 15 14 .action(async (opts) => { 16 15 try { 17 16 const { verbose } = opts; 18 - const envDid = loadConfig().did; 19 - const did = opts.did || envDid; 20 - if (verbose) console.log(`[verbose] Config loaded, DID: ${did}`); 17 + const did = opts.did || loadConfig().did; 18 + if (verbose) console.log(`[verbose] DID: ${did}`); 21 19 22 - const clientId = `http://localhost?redirect_uri=${encodeURIComponent('http://127.0.0.1')}&scope=${encodeURIComponent('atproto transition:generic')}`; 23 - const sessionStore = createSessionStore(); 24 - const client = createOAuthClient({ 25 - clientId, 26 - sessionStore, 27 - stateStore: { 28 - set: async () => {}, 29 - get: async () => undefined, 30 - del: async () => {}, 31 - }, 32 - redirectUri: 'http://127.0.0.1', 33 - }); 34 - const session = await client.restore(did); 20 + const { agent, session } = await restoreAgent(did); 35 21 if (verbose) console.log(`[verbose] Session restored, PDS: ${session.serverMetadata?.issuer}`); 36 - const agent = new Agent(session); 37 22 38 23 const listArgs = { 39 24 repo: did,
+22 -4
src/lib/oauth.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 + import { Agent } from '@atproto/api'; 4 5 import { NodeOAuthClient } from '@atproto/oauth-client-node'; 5 6 import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 6 7 import { configDir, configPath } from './paths.js'; 7 8 8 - export const requestLock = async (_name, fn) => await fn(); 9 + const requestLock = async (_name, fn) => await fn(); 10 + 11 + const noopStore = { 12 + set: async () => {}, 13 + get: async () => undefined, 14 + del: async () => {}, 15 + }; 9 16 10 17 export function createStore() { 11 18 const map = new Map(); ··· 22 29 } 23 30 24 31 export function createSessionStore() { 25 - const sessionFile = configPath('bsky_session.json'); 32 + const sessionFile = configPath('session.json'); 26 33 let data = {}; 27 34 try { 28 35 data = JSON.parse(readFileSync(sessionFile, 'utf-8')); ··· 42 49 }; 43 50 } 44 51 45 - export function createOAuthClient({ stateStore, sessionStore, redirectUri, clientId }) { 52 + export function createOAuthClient({ stateStore, sessionStore, redirectUri }) { 46 53 return new NodeOAuthClient({ 47 54 requestLock, 48 55 clientMetadata: { 49 - client_id: clientId || 'https://v-it.org/client-metadata.json', 56 + client_id: 'https://v-it.org/client-metadata.json', 50 57 client_name: 'vit CLI', 51 58 application_type: 'native', 52 59 grant_types: ['authorization_code', 'refresh_token'], ··· 61 68 sessionStore, 62 69 }); 63 70 } 71 + 72 + export async function restoreAgent(did) { 73 + const sessionStore = createSessionStore(); 74 + const client = createOAuthClient({ 75 + sessionStore, 76 + stateStore: noopStore, 77 + redirectUri: 'http://127.0.0.1', 78 + }); 79 + const session = await client.restore(did); 80 + return { agent: new Agent(session), session }; 81 + }
+1 -1
src/lib/paths.js
··· 4 4 import envPaths from 'env-paths'; 5 5 import { join } from 'node:path'; 6 6 7 - const paths = envPaths('vit'); 7 + const paths = envPaths('vit', { suffix: '' }); 8 8 9 9 export const configDir = paths.config; 10 10 export const configPath = (filename) => join(paths.config, filename);