open source is social v-it.org
0
fork

Configure Feed

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

Rename oauth command to login and use vit.json creds

- rename src/cmd/oauth.js to src/cmd/login.js and update CLI registration

- migrate credential storage from src/lib/env.js to src/lib/config.js

- switch credential keys to did/access_token/refresh_token/expires_at

- update pds-record, plc-verify, and firehose to read did from config

- add created_at and updated_at epoch timestamp stamping in saveConfig()

- remove src/lib/env.js and update README, CLAUDE, skills, and VOCAB docs

+46 -95
+1 -1
CLAUDE.md
··· 5 5 ## Project Overview 6 6 7 7 vit is a Bun CLI with subcommands for DID:PLC operations and Bluesky OAuth: 8 - - `vit oauth` 8 + - `vit login` 9 9 - `vit plc-register` 10 10 - `vit plc-verify` 11 11 - `vit firehose`
+8 -10
README.md
··· 28 28 - **remix** — mix a post with local codebase and create a plan to implement; auto-likes 29 29 - **ship** — take any locally implemented feature and write up a post (or quote post if was remixed) 30 30 31 - ## oauth 31 + ## login 32 32 33 - Obtain an ATProto OAuth access token via browser-based authorization. 33 + Log in to Bluesky via browser-based OAuth. 34 34 35 35 ### Usage 36 36 37 37 ```bash 38 - vit oauth --handle alice.bsky.social 38 + vit login --handle 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 44 3. After you approve, print the DPoP-bound access token and DID 45 - 4. Save credentials (`BSKY_DID`, `BSKY_ACCESS_TOKEN`, `BSKY_REFRESH_TOKEN`, `BSKY_EXPIRES_AT`) to `.env` 46 - 47 - If `.env` already exists, only the `BSKY_*` variables are updated and other variables are preserved. 45 + 4. Save credentials (`did`, `access_token`, `refresh_token`, `expires_at`) to `vit.json` 48 46 49 47 ### Options 50 48 ··· 85 83 vit plc-verify 86 84 ``` 87 85 88 - This reads `BSKY_DID` from `.env` and: 86 + This reads `did` from `vit.json` and: 89 87 - Resolves the DID document via `https://plc.directory/{did}` 90 88 - Fetches the audit log 91 89 - Prints a summary of handles, services, verification methods, and operation history 92 90 93 91 ### Options 94 92 95 - - `--did <did>` - Check a specific DID (overrides `.env`) 93 + - `--did <did>` - Check a specific DID (overrides config) 96 94 - `-v, --verbose` - Show full API responses 97 95 98 96 ## firehose ··· 107 105 108 106 ### Options 109 107 110 - - `--did <did>` - Filter by DID (reads `BSKY_DID` from `.env` if not provided) 108 + - `--did <did>` - Filter by DID (reads saved DID from config if not provided) 111 109 - `--collection <nsid>` - Collection NSID to filter (default: `org.v-it.hello`) 112 110 - `-v, --verbose` - Show full JSON for each event 113 111 ··· 123 121 124 122 ### Options 125 123 126 - - `--did <did>` - DID to use (overrides `.env`) 124 + - `--did <did>` - DID to use (overrides config) 127 125 - `--message <msg>` - Message to write (default: `hello world`) 128 126 - `-v, --verbose` - Show full API responses
+1 -1
VOCAB.md
··· 89 89 `doctor` is an alias for `init`. 90 90 91 91 Capabilities: 92 - - Log in to Bluesky (invokes OAuth flow) 92 + - Log in to Bluesky (invokes login flow) 93 93 - Install skills (agent capabilities) 94 94 - Initialize `.vit/` in the current git repo 95 95 - Verify environment is correctly configured
+3 -3
skills/vit/SKILL.md
··· 12 12 ```bash 13 13 bun install # install dependencies 14 14 vit init # check environment and configure vit 15 - vit oauth --handle alice.bsky.social # authenticate with Bluesky 15 + vit login --handle alice.bsky.social # authenticate with Bluesky 16 16 ``` 17 17 18 18 ## Subcommands ··· 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 oauth --handle <h>` | Browser-based ATProto OAuth, saves tokens to .env | 25 + | `vit login --handle <h>` | Browser-based ATProto OAuth, saves tokens to vit.json | 26 26 | `vit config [action]` | Read/write vit.json config (list, set, delete) | 27 27 | `vit plc-register` | Generate and register a DID:PLC genesis operation | 28 28 | `vit plc-verify` | Verify DID document and audit log from PLC directory | ··· 69 69 ## Configuration 70 70 71 71 - **`.vit/`** — local project directory, stores beacon and follows 72 - - **`.env`** — credentials (`BSKY_DID`, `BSKY_ACCESS_TOKEN`, etc.), written by `vit oauth` 72 + - **`vit.json`** — credentials (`did`, `access_token`, etc.) and config, written by `vit login` and `vit config` 73 73 - **`vit config`** — read/write `vit.json` user-level config
+2 -2
src/cli.js
··· 5 5 import registerConfig from './cmd/config.js'; 6 6 import registerDoctor from './cmd/doctor.js'; 7 7 import registerInit from './cmd/init.js'; 8 - import registerOauth from './cmd/oauth.js'; 8 + import registerLogin from './cmd/login.js'; 9 9 import registerPlcRegister from './cmd/plc-register.js'; 10 10 import registerPlcVerify from './cmd/plc-verify.js'; 11 11 import registerFirehose from './cmd/firehose.js'; ··· 21 21 registerConfig(program); 22 22 registerDoctor(program); 23 23 registerInit(program); 24 - registerOauth(program); 24 + registerLogin(program); 25 25 registerPlcRegister(program); 26 26 registerPlcVerify(program); 27 27 registerFirehose(program);
+5 -5
src/cmd/firehose.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { loadEnv } from '../lib/env.js'; 4 + import { loadConfig } from '../lib/config.js'; 5 5 6 6 const JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; 7 7 const DEFAULT_COLLECTION = 'org.v-it.hello'; ··· 108 108 .command('firehose') 109 109 .description('Listen to Bluesky Jetstream firehose for custom record events') 110 110 .option('-v, --verbose', 'Show full JSON for each event') 111 - .option('--did <did>', 'Filter by DID (reads saved BSKY_DID if not provided)') 111 + .option('--did <did>', 'Filter by DID (reads saved DID from config if not provided)') 112 112 .option('--collection <nsid>', 'Collection NSID to filter', DEFAULT_COLLECTION) 113 113 .action(async (opts) => { 114 114 try { 115 115 if (!opts.did) { 116 - const env = loadEnv(); 117 - if (env.BSKY_DID) { 118 - opts.did = env.BSKY_DID; 116 + const config = loadConfig(); 117 + if (config.did) { 118 + opts.did = config.did; 119 119 } 120 120 } 121 121
+14 -14
src/cmd/oauth.js src/cmd/login.js
··· 2 2 // Copyright (c) 2026 sol pbc 3 3 4 4 import { writeFileSync } from 'node:fs'; 5 - import { saveToEnv } from '../lib/env.js'; 5 + import { loadConfig, saveConfig } from '../lib/config.js'; 6 6 import { createOAuthClient, createSessionStore, createStore } from '../lib/oauth.js'; 7 - import { configPath } from '../lib/paths.js'; 8 7 9 8 export default function register(program) { 10 9 program 11 - .command('oauth') 12 - .description('Obtain an ATProto OAuth access token via browser authorization') 10 + .command('login') 11 + .description('Log in to Bluesky via browser-based OAuth') 13 12 .option('--handle <handle>', 'Bluesky handle (e.g. alice.bsky.social)') 14 13 .option('-v, --verbose', 'Show discovery details') 15 14 .option('--output <file>', 'Save token JSON to file') ··· 125 124 console.log(`DID: ${session.did}`); 126 125 127 126 const sessionData = await sessionStore.get(session.did); 127 + const tokens = sessionData?.tokenSet ?? {}; 128 128 const outputData = { 129 129 did: session.did, 130 - accessToken: sessionData?.tokenSet?.access_token ?? null, 131 - refreshToken: sessionData?.tokenSet?.refresh_token ?? null, 132 - expiresAt: sessionData?.tokenSet?.expires_at ?? null, 130 + accessToken: tokens.access_token ?? null, 131 + refreshToken: tokens.refresh_token ?? null, 132 + expiresAt: tokens.expires_at ?? null, 133 133 }; 134 134 135 135 console.log(JSON.stringify(outputData, null, 2)); ··· 138 138 writeFileSync(output, `${JSON.stringify(outputData, null, 2)}\n`); 139 139 } 140 140 141 - saveToEnv({ 142 - BSKY_DID: outputData.did, 143 - BSKY_ACCESS_TOKEN: outputData.accessToken ?? '', 144 - BSKY_REFRESH_TOKEN: outputData.refreshToken ?? '', 145 - BSKY_EXPIRES_AT: outputData.expiresAt ?? '', 146 - }); 147 - console.log(`Saved credentials to ${configPath('.env')}`); 141 + const config = loadConfig(); 142 + config.did = session.did; 143 + config.access_token = tokens.access_token; 144 + config.refresh_token = tokens.refresh_token; 145 + config.expires_at = tokens.expires_at; 146 + saveConfig(config); 147 + console.log('\nCredentials saved to vit.json'); 148 148 } catch (err) { 149 149 console.error(err instanceof Error ? err.message : String(err)); 150 150 process.exitCode = 1;
+5 -5
src/cmd/pds-record.js
··· 2 2 // Copyright (c) 2026 sol pbc 3 3 4 4 import { Agent } from '@atproto/api'; 5 - import { loadEnv } from '../lib/env.js'; 5 + import { loadConfig } from '../lib/config.js'; 6 6 import { createOAuthClient, createSessionStore } from '../lib/oauth.js'; 7 7 import { configPath } from '../lib/paths.js'; 8 8 ··· 14 14 .option('--message <msg>', 'Message to write', 'hello world') 15 15 .action(async (opts) => { 16 16 try { 17 - const env = loadEnv(); 18 - const did = opts.did || env.BSKY_DID; 17 + const config = loadConfig(); 18 + const did = opts.did || config.did; 19 19 20 20 if (!did) { 21 - throw new Error('No DID found. Run `vit oauth` first or pass --did <did>.'); 21 + throw new Error('No DID found. Run `vit login` first or pass --did <did>.'); 22 22 } 23 23 24 24 const sessionStore = createSessionStore(); 25 25 const sessionData = await sessionStore.get(did); 26 26 if (!sessionData) { 27 27 throw new Error( 28 - `No session found for ${did} in ${configPath('bsky_session.json')}. Run \`vit oauth\` first to authenticate.`, 28 + `No session found for ${did} in ${configPath('bsky_session.json')}. Run \`vit login\` first to authenticate.`, 29 29 ); 30 30 } 31 31
+4 -4
src/cmd/plc-verify.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { loadEnv } from '../lib/env.js'; 4 + import { loadConfig } from '../lib/config.js'; 5 5 6 6 export default function register(program) { 7 7 program ··· 11 11 .option('--did <did>', 'DID to check (overrides saved credentials)') 12 12 .action(async (opts) => { 13 13 try { 14 - const env = loadEnv(); 15 - const did = opts.did || env.BSKY_DID; 14 + const config = loadConfig(); 15 + const did = opts.did || config.did; 16 16 17 17 if (!did) { 18 - throw new Error('No DID found. Run `vit oauth` first or pass --did <did>.'); 18 + throw new Error('No DID found. Run `vit login` first or pass --did <did>.'); 19 19 } 20 20 21 21 if (!did.startsWith('did:plc:')) {
+3
src/lib/config.js
··· 16 16 } 17 17 18 18 export function saveConfig(obj) { 19 + const now = Math.floor(Date.now() / 1000); 20 + if (!obj.created_at) obj.created_at = now; 21 + obj.updated_at = now; 19 22 mkdirSync(configDir, { recursive: true }); 20 23 writeFileSync(vitJsonPath, JSON.stringify(obj, null, 2) + '\n'); 21 24 }
-50
src/lib/env.js
··· 1 - // SPDX-License-Identifier: AGPL-3.0-only 2 - // Copyright (c) 2026 sol pbc 3 - 4 - import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; 5 - import { configDir, configPath } from './paths.js'; 6 - 7 - export function loadEnv() { 8 - const envPath = configPath('.env'); 9 - const vars = {}; 10 - let content; 11 - if (!existsSync(envPath)) { 12 - return vars; 13 - } 14 - try { 15 - content = readFileSync(envPath, 'utf-8'); 16 - } catch { 17 - return vars; 18 - } 19 - for (const line of content.split('\n')) { 20 - const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)/); 21 - if (m) vars[m[1]] = m[2]; 22 - } 23 - return vars; 24 - } 25 - 26 - export function saveToEnv(vars) { 27 - const envPath = configPath('.env'); 28 - let lines = []; 29 - if (existsSync(envPath)) { 30 - try { 31 - lines = readFileSync(envPath, 'utf-8').split('\n'); 32 - } catch {} 33 - } 34 - while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop(); 35 - const updated = new Set(); 36 - for (let i = 0; i < lines.length; i++) { 37 - const m = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)=/); 38 - if (m && m[1] in vars) { 39 - lines[i] = `${m[1]}=${vars[m[1]]}`; 40 - updated.add(m[1]); 41 - } 42 - } 43 - for (const [key, value] of Object.entries(vars)) { 44 - if (!updated.has(key)) { 45 - lines.push(`${key}=${value}`); 46 - } 47 - } 48 - mkdirSync(configDir, { recursive: true }); 49 - writeFileSync(envPath, lines.join('\n') + '\n'); 50 - }