open source is social v-it.org
0
fork

Configure Feed

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

Add PDS custom record tool and persist OAuth sessions to file

bsky_oauth.js now writes the full OAuth session (including DPoP key
material) to bsky_session.json, enabling other scripts to restore
authenticated sessions without re-authenticating.

New pds_record.js restores the session and writes/reads a custom
org.v-it.hello record to the user's PDS repo.

+184 -11
+1
.gitignore
··· 8 8 9 9 # Environment 10 10 .env 11 + bsky_session.json
+21 -1
bsky_oauth.js
··· 6 6 import { Command } from 'commander'; 7 7 import { readFileSync, writeFileSync } from 'node:fs'; 8 8 9 + const SESSION_FILE = new URL('bsky_session.json', import.meta.url).pathname; 10 + 9 11 function createStore() { 10 12 const map = new Map(); 11 13 ··· 16 18 get: async (key) => map.get(key), 17 19 del: async (key) => { 18 20 map.delete(key); 21 + }, 22 + }; 23 + } 24 + 25 + function createSessionStore() { 26 + let data = {}; 27 + try { 28 + data = JSON.parse(readFileSync(SESSION_FILE, 'utf-8')); 29 + } catch {} 30 + return { 31 + set: async (key, value) => { 32 + data[key] = value; 33 + writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2) + '\n'); 34 + }, 35 + get: async (key) => data[key], 36 + del: async (key) => { 37 + delete data[key]; 38 + writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2) + '\n'); 19 39 }, 20 40 }; 21 41 } ··· 108 128 } 109 129 110 130 const stateStore = createStore(); 111 - const sessionStore = createStore(); 131 + const sessionStore = createSessionStore(); 112 132 113 133 const client = new NodeOAuthClient({ 114 134 clientMetadata: {
+9 -10
bun.lock
··· 4 4 "": { 5 5 "name": "vit", 6 6 "dependencies": { 7 + "@atproto/api": "^0.18.20", 7 8 "@atproto/oauth-client-node": "^0.3.16", 8 9 "@ipld/dag-cbor": "^9.2.0", 9 10 "@noble/curves": "^1.8.0", ··· 31 32 "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 32 33 33 34 "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 35 + 36 + "@atproto/api": ["@atproto/api@0.18.20", "", { "dependencies": { "@atproto/common-web": "^0.4.15", "@atproto/lexicon": "^0.6.1", "@atproto/syntax": "^0.4.3", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw=="], 34 37 35 38 "@atproto/common-web": ["@atproto/common-web@0.4.16", "", { "dependencies": { "@atproto/lex-data": "^0.0.11", "@atproto/lex-json": "^0.0.11", "@atproto/syntax": "^0.4.3", "zod": "^3.23.8" } }, "sha512-Ufvaff5JgxUyUyTAG0/3o7ltpy3lnZ1DvLjyAnvAf+hHfiK7OMQg+8byr+orN+KP9MtIQaRTsCgYPX+PxMKUoA=="], 36 39 ··· 63 66 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], 64 67 65 68 "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], 69 + 70 + "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 66 71 67 72 "base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], 68 73 ··· 82 87 83 88 "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 84 89 85 - "multiformats": ["multiformats@13.4.2", "", {}, "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ=="], 90 + "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 91 + 92 + "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], 86 93 87 94 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 88 95 ··· 94 101 95 102 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 96 103 97 - "@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 98 - 99 - "@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 100 - 101 - "@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 102 - 103 - "@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 104 - 105 - "uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 104 + "@ipld/dag-cbor/multiformats": ["multiformats@13.4.2", "", {}, "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ=="], 106 105 } 107 106 }
+1
package.json
··· 2 2 "name": "vit", 3 3 "type": "module", 4 4 "dependencies": { 5 + "@atproto/api": "^0.18.20", 5 6 "@atproto/oauth-client-node": "^0.3.16", 6 7 "@ipld/dag-cbor": "^9.2.0", 7 8 "@noble/curves": "^1.8.0",
+152
pds_record.js
··· 1 + #!/usr/bin/env bun 2 + // SPDX-License-Identifier: AGPL-3.0-only 3 + // Copyright (c) 2026 sol pbc 4 + 5 + import { NodeOAuthClient } from '@atproto/oauth-client-node'; 6 + import { Agent } from '@atproto/api'; 7 + import { Command } from 'commander'; 8 + import { readFileSync, writeFileSync } from 'node:fs'; 9 + 10 + const SESSION_FILE = new URL('bsky_session.json', import.meta.url).pathname; 11 + 12 + function loadEnv() { 13 + const envPath = new URL('.env', import.meta.url).pathname; 14 + const vars = {}; 15 + let content; 16 + try { 17 + content = readFileSync(envPath, 'utf-8'); 18 + } catch { 19 + return vars; 20 + } 21 + for (const line of content.split('\n')) { 22 + const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)/); 23 + if (m) vars[m[1]] = m[2]; 24 + } 25 + return vars; 26 + } 27 + 28 + function createSessionStore() { 29 + let data = {}; 30 + try { 31 + data = JSON.parse(readFileSync(SESSION_FILE, 'utf-8')); 32 + } catch {} 33 + return { 34 + set: async (key, value) => { 35 + data[key] = value; 36 + writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2) + '\n'); 37 + }, 38 + get: async (key) => data[key], 39 + del: async (key) => { delete data[key]; }, 40 + }; 41 + } 42 + 43 + async function main() { 44 + const program = new Command(); 45 + 46 + program 47 + .name('pds_record') 48 + .description('Write and read a custom org.v-it.hello record on the authenticated PDS') 49 + .option('-v, --verbose', 'Show full API responses') 50 + .option('--did <did>', 'DID to use (overrides .env)') 51 + .option('--message <msg>', 'Message to write', 'hello world') 52 + .parse(); 53 + 54 + const opts = program.opts(); 55 + 56 + try { 57 + const env = loadEnv(); 58 + const did = opts.did || env.BSKY_DID; 59 + 60 + if (!did) { 61 + throw new Error('No DID found. Run bsky_oauth.js first or pass --did <did>.'); 62 + } 63 + 64 + // Verify session file exists 65 + let sessionData; 66 + try { 67 + sessionData = JSON.parse(readFileSync(SESSION_FILE, 'utf-8')); 68 + } catch { 69 + throw new Error('Session file not found. Run bsky_oauth.js first to authenticate.'); 70 + } 71 + 72 + if (!sessionData[did]) { 73 + throw new Error(`No session found for ${did}. Run bsky_oauth.js first to authenticate.`); 74 + } 75 + 76 + if (opts.verbose) { 77 + console.log(`[verbose] Restoring session for ${did}`); 78 + } 79 + 80 + // Create OAuth client with file-backed session store 81 + const sessionStore = createSessionStore(); 82 + 83 + const client = new NodeOAuthClient({ 84 + clientMetadata: { 85 + client_id: 'https://v-it.org/client-metadata.json', 86 + client_name: 'vit CLI', 87 + application_type: 'native', 88 + grant_types: ['authorization_code'], 89 + response_types: ['code'], 90 + redirect_uris: ['http://127.0.0.1'], 91 + scope: 'atproto transition:generic', 92 + token_endpoint_auth_method: 'none', 93 + dpop_bound_access_tokens: true, 94 + client_uri: 'https://v-it.org', 95 + }, 96 + stateStore: { 97 + set: async () => {}, 98 + get: async () => undefined, 99 + del: async () => {}, 100 + }, 101 + sessionStore, 102 + }); 103 + 104 + const session = await client.restore(did); 105 + const agent = new Agent(session); 106 + 107 + if (opts.verbose) { 108 + console.log(`[verbose] Session restored, agent ready`); 109 + } 110 + 111 + // Write record 112 + const record = { 113 + $type: 'org.v-it.hello', 114 + message: opts.message, 115 + createdAt: new Date().toISOString(), 116 + }; 117 + 118 + if (opts.verbose) { 119 + console.log(`[verbose] Writing record:`); 120 + console.log(JSON.stringify(record, null, 2)); 121 + } 122 + 123 + const putResult = await agent.com.atproto.repo.putRecord({ 124 + repo: did, 125 + collection: 'org.v-it.hello', 126 + rkey: 'self', 127 + record, 128 + validate: false, 129 + }); 130 + 131 + console.log(`Record written: ${putResult.data.uri}`); 132 + 133 + // Read it back 134 + const getResult = await agent.com.atproto.repo.getRecord({ 135 + repo: did, 136 + collection: 'org.v-it.hello', 137 + rkey: 'self', 138 + }); 139 + 140 + if (opts.verbose) { 141 + console.log(`[verbose] Read-back result:`); 142 + console.log(JSON.stringify(getResult.data, null, 2)); 143 + } 144 + 145 + console.log(`Record value: ${JSON.stringify(getResult.data.value)}`); 146 + } catch (err) { 147 + console.error(err instanceof Error ? err.message : String(err)); 148 + process.exitCode = 1; 149 + } 150 + } 151 + 152 + await main();