open source is social v-it.org
0
fork

Configure Feed

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

fix: protocol correctness — lexicon gaps, validation, pagination, did:web, parallelization

Eight protocol correctness fixes for ATProto integration:

1. Cap lexicon: add recapRef type (uri + ref) and optional recap property;
add optional kind field with knownValues (feat, fix, test, docs, etc.)
2. Skill lexicon: add required cid to recapRef for content-addressable integrity
3. Flip validate: false → true on all 4 putRecord call sites (ship×2, vouch×2)
4. Cursor-based pagination in listRecordsFromPds — fetches all records, not just first page
5. Migrate vouch.js direct SDK listRecords call to shared paginated helper
6. did:web resolution in resolvePds and resolveHandleFromDid
7. Extract Jetstream URL to constants.js with VIT_JETSTREAM_URL env var and --jetstream CLI option
8. Parallelize all 8 sequential DID loops with Promise.allSettled concurrency batching

+352 -162
+30
lexicons/org/v-it/cap.json
··· 47 47 "ref": "lex:org.v-it.cap#replyRef", 48 48 "description": "Reference to the parent and root caps when this cap is a reply in a thread." 49 49 }, 50 + "recap": { 51 + "type": "ref", 52 + "ref": "lex:org.v-it.cap#recapRef", 53 + "description": "Reference to the parent cap this was derived from." 54 + }, 50 55 "embed": { 51 56 "type": "union", 52 57 "description": "Embedded content: images, video, external links, or record references.", ··· 87 92 "maxLength": 512, 88 93 "description": "Beacon URI scoping this cap to a project." 89 94 }, 95 + "kind": { 96 + "type": "string", 97 + "maxLength": 64, 98 + "maxGraphemes": 32, 99 + "knownValues": ["feat", "fix", "test", "docs", "refactor", "chore", "perf", "style"], 100 + "description": "Category of the capability." 101 + }, 90 102 "createdAt": { 91 103 "type": "string", 92 104 "format": "datetime", ··· 108 120 "type": "ref", 109 121 "ref": "lex:com.atproto.repo.strongRef", 110 122 "description": "The direct parent cap being replied to." 123 + } 124 + } 125 + }, 126 + "recapRef": { 127 + "type": "object", 128 + "description": "Reference to a parent cap this was derived from.", 129 + "required": ["uri", "ref"], 130 + "properties": { 131 + "uri": { 132 + "type": "string", 133 + "format": "at-uri", 134 + "description": "AT URI of the parent cap record." 135 + }, 136 + "ref": { 137 + "type": "string", 138 + "maxLength": 128, 139 + "maxGraphemes": 64, 140 + "description": "Three lowercase words separated by dashes identifying the parent cap." 111 141 } 112 142 } 113 143 }
+5 -1
lexicons/org/v-it/skill.json
··· 133 133 "recapRef": { 134 134 "type": "object", 135 135 "description": "Reference to a parent skill this was derived from.", 136 - "required": ["uri"], 136 + "required": ["uri", "cid"], 137 137 "properties": { 138 138 "uri": { 139 139 "type": "string", 140 140 "format": "at-uri", 141 141 "description": "AT URI of the parent skill record." 142 + }, 143 + "cid": { 144 + "type": "string", 145 + "description": "CID of the parent skill record for content-addressable integrity." 142 146 } 143 147 } 144 148 }
+8 -7
src/cmd/firehose.js
··· 2 2 // Copyright (c) 2026 sol pbc 3 3 4 4 import { loadConfig } from '../lib/config.js'; 5 - import { CAP_COLLECTION } from '../lib/constants.js'; 5 + import { CAP_COLLECTION, JETSTREAM_URL } from '../lib/constants.js'; 6 6 import { resolveRef } from '../lib/cap-ref.js'; 7 7 import { brand } from '../lib/brand.js'; 8 - 9 - const JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; 10 8 11 9 let ws = null; 12 10 let shuttingDown = false; 13 11 let backoff = 1000; 14 12 15 - function buildUrl(collection, did, cursor) { 16 - const url = new URL(JETSTREAM_URL); 13 + function buildUrl(baseUrl, collection, did, cursor) { 14 + const url = new URL(baseUrl); 17 15 url.searchParams.set('wantedCollections', collection); 18 16 if (did) url.searchParams.set('wantedDids', did); 19 17 if (cursor) url.searchParams.set('cursor', cursor); ··· 59 57 } 60 58 61 59 function connect(opts, cursor) { 62 - const url = buildUrl(opts.collection, opts.did, cursor); 60 + const jetstreamUrl = opts.jetstream || JETSTREAM_URL; 61 + const url = buildUrl(jetstreamUrl, opts.collection, opts.did, cursor); 63 62 let lastCursor = cursor; 64 63 65 64 ws = new WebSocket(url); ··· 115 114 .option('--did <did>', 'Filter by DID (reads saved DID from config if not provided)') 116 115 .option('--global', 'Show cap events from all DIDs across the network') 117 116 .option('--collection <nsid>', 'Collection NSID to filter', CAP_COLLECTION) 117 + .option('--jetstream <url>', 'Jetstream WebSocket URL (default: VIT_JETSTREAM_URL env or built-in)') 118 118 .action(async (opts) => { 119 119 try { 120 120 if (opts.global && opts.did) { ··· 141 141 }); 142 142 } 143 143 144 - const url = buildUrl(opts.collection, opts.did, null); 144 + const jetstreamUrl = opts.jetstream || JETSTREAM_URL; 145 + const url = buildUrl(jetstreamUrl, opts.collection, opts.did, null); 145 146 console.log(`${brand} firehose`); 146 147 console.log(` Collection: ${opts.collection}`); 147 148 if (opts.did) console.log(` DID filter: ${opts.did}`);
+11 -15
src/cmd/learn.js
··· 12 12 import { shouldBypassVet } from '../lib/trust-gate.js'; 13 13 import { isSkillRef, nameFromSkillRef, isValidSkillRef } from '../lib/skill-ref.js'; 14 14 import { mark, name } from '../lib/brand.js'; 15 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 15 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 16 16 17 17 export default function register(program) { 18 18 program ··· 108 108 109 109 // Fetch skills from each DID, find matching ref 110 110 let match = null; 111 - for (const repoDid of dids) { 112 - try { 113 - const pds = await resolvePds(repoDid); 114 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 115 - const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50); 116 - for (const rec of res.records) { 117 - const recName = rec.value.name; 118 - if (recName === skillName) { 119 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 120 - match = rec; 121 - } 111 + await queryDidsInParallel(dids, async (repoDid) => { 112 + const pds = await resolvePds(repoDid); 113 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 114 + const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION); 115 + for (const rec of res.records) { 116 + const recName = rec.value.name; 117 + if (recName === skillName) { 118 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 119 + match = rec; 122 120 } 123 121 } 124 - } catch (err) { 125 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`); 126 122 } 127 - } 123 + }); 128 124 129 125 if (!match) { 130 126 console.error(`no skill found with ref '${ref}' from followed accounts.`);
+12 -16
src/cmd/remix.js
··· 9 9 import { shouldBypassVet } from '../lib/trust-gate.js'; 10 10 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 11 11 import { brand, name } from '../lib/brand.js'; 12 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 12 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 13 13 14 14 export default function register(program) { 15 15 program ··· 83 83 if (verbose) console.log(`[verbose] querying ${dids.length} accounts`); 84 84 85 85 let match = null; 86 - for (const repoDid of dids) { 87 - try { 88 - const pds = await resolvePds(repoDid); 89 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 90 - const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 91 - for (const rec of res.records) { 92 - if (rec.value.beacon !== beacon) continue; 93 - const recRef = resolveRef(rec.value, rec.cid); 94 - if (recRef === ref) { 95 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 96 - match = rec; 97 - } 86 + await queryDidsInParallel(dids, async (repoDid) => { 87 + const pds = await resolvePds(repoDid); 88 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 89 + const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION); 90 + for (const rec of res.records) { 91 + if (rec.value.beacon !== beacon) continue; 92 + const recRef = resolveRef(rec.value, rec.cid); 93 + if (recRef === ref) { 94 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 95 + match = rec; 98 96 } 99 97 } 100 - } catch (err) { 101 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`); 102 98 } 103 - } 99 + }); 104 100 105 101 if (!match) { 106 102 console.error(`no cap found with ref '${ref}' for this beacon.`);
+4 -4
src/cmd/scan.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { CAP_COLLECTION, SKILL_COLLECTION } from '../lib/constants.js'; 4 + import { CAP_COLLECTION, SKILL_COLLECTION, JETSTREAM_URL } from '../lib/constants.js'; 5 5 import { resolveRef } from '../lib/cap-ref.js'; 6 6 import { resolveHandleFromDid } from '../lib/pds.js'; 7 7 import { brand } from '../lib/brand.js'; 8 - 9 - const JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; 10 8 11 9 export default function register(program) { 12 10 program ··· 18 16 .option('--caps', 'Show only cap publishers') 19 17 .option('--tag <tag>', 'Filter skills by tag') 20 18 .option('-v, --verbose', 'Show each event as it arrives') 19 + .option('--jetstream <url>', 'Jetstream WebSocket URL (default: VIT_JETSTREAM_URL env or built-in)') 21 20 .action(async (opts) => { 22 21 try { 23 22 const days = parseInt(opts.days, 10); ··· 38 37 if (wantCaps) collections.push(CAP_COLLECTION); 39 38 if (wantSkills) collections.push(SKILL_COLLECTION); 40 39 41 - const url = new URL(JETSTREAM_URL); 40 + const jetstreamUrl = opts.jetstream || JETSTREAM_URL; 41 + const url = new URL(jetstreamUrl); 42 42 for (const col of collections) { 43 43 url.searchParams.append('wantedCollections', col); 44 44 }
+25 -17
src/cmd/ship.js
··· 12 12 import { REF_PATTERN, resolveRef } from '../lib/cap-ref.js'; 13 13 import { isValidSkillName, skillRefFromName } from '../lib/skill-ref.js'; 14 14 import { name } from '../lib/brand.js'; 15 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 15 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 16 16 17 17 function parseFrontmatter(text) { 18 18 const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); ··· 227 227 collection: SKILL_COLLECTION, 228 228 rkey, 229 229 record, 230 - validate: false, 230 + validate: true, 231 231 }; 232 232 233 233 if (verbose) console.log(`[verbose] putRecord ${putArgs.collection} rkey=${rkey}`); ··· 325 325 } 326 326 } 327 327 328 + if (opts.kind) { 329 + const validKinds = ['feat', 'fix', 'test', 'docs', 'refactor', 'chore', 'perf', 'style']; 330 + if (!validKinds.includes(opts.kind)) { 331 + console.error(`error: --kind must be one of: ${validKinds.join(', ')}`); 332 + process.exitCode = 1; 333 + return; 334 + } 335 + } 336 + 328 337 const now = new Date().toISOString(); 329 338 330 339 // preflight: session ··· 345 354 if (verbose) console.log(`[verbose] recap: querying ${dids.length} accounts`); 346 355 347 356 let match = null; 348 - for (const repoDid of dids) { 349 - try { 350 - const pds = await resolvePds(repoDid); 351 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 352 - const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 353 - for (const rec of res.records) { 354 - const recRef = resolveRef(rec.value, rec.cid); 355 - if (recRef === opts.recap) { 356 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 357 - match = rec; 358 - } 357 + await queryDidsInParallel(dids, async (repoDid) => { 358 + const pds = await resolvePds(repoDid); 359 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 360 + const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION); 361 + for (const rec of res.records) { 362 + const recRef = resolveRef(rec.value, rec.cid); 363 + if (recRef === opts.recap) { 364 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 365 + match = rec; 359 366 } 360 367 } 361 - } catch (err) { 362 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`); 363 368 } 364 - } 369 + }); 365 370 366 371 if (match) { 367 372 recapUri = match.uri; ··· 383 388 }; 384 389 if (projectConfig.beacon) record.beacon = projectConfig.beacon; 385 390 if (opts.recap) record.recap = { uri: recapUri, ref: opts.recap }; 391 + if (opts.kind) record.kind = opts.kind; 386 392 const rkey = TID.nextStr(); 387 393 if (verbose) console.log(`[verbose] Record built, rkey: ${rkey}`); 388 394 const putArgs = { ··· 390 396 collection: CAP_COLLECTION, 391 397 rkey, 392 398 record, 393 - validate: false, 399 + validate: true, 394 400 }; 395 401 if (verbose) console.log(`[verbose] putRecord ${putArgs.collection} rkey=${rkey}`); 396 402 const putRes = await agent.com.atproto.repo.putRecord(putArgs); ··· 434 440 .option('--description <description>', 'Description of the cap') 435 441 .option('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation)') 436 442 .option('--recap <ref>', 'Ref of the cap this derives from (quote-post semantics)') 443 + .option('--kind <kind>', 'Category: feat, fix, test, docs, refactor, chore, perf, style') 437 444 .option('--skill <path>', 'Publish a skill directory (reads SKILL.md + resources)') 438 445 .option('--tags <tags>', 'Comma-separated discovery tags (for skills)') 439 446 .option('--version <version>', 'Version string (for skills, overrides frontmatter)') ··· 476 483 --description One sentence explaining what this cap does 477 484 --ref Three lowercase words with dashes (your-ref-name) 478 485 --recap <ref> Optional. Ref of the cap this derives from (links back to original) 486 + --kind Category: feat, fix, test, docs, refactor, chore, perf, style 479 487 body (stdin) Full cap content, piped or via heredoc 480 488 481 489 Skill fields:
+24 -30
src/cmd/skim.js
··· 9 9 import { resolveRef } from '../lib/cap-ref.js'; 10 10 import { skillRefFromName } from '../lib/skill-ref.js'; 11 11 import { name } from '../lib/brand.js'; 12 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 12 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 13 13 14 14 export default function register(program) { 15 15 program ··· 86 86 // fetch from each DID 87 87 const allItems = []; 88 88 89 - for (const repoDid of dids) { 90 - try { 91 - const pds = await resolvePds(repoDid); 92 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 89 + await queryDidsInParallel(dids, async (repoDid) => { 90 + const pds = await resolvePds(repoDid); 91 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 93 92 94 - // Fetch caps (filtered by beacon) 95 - if (wantCaps && beacon) { 96 - const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 97 - const caps = res.records.filter(r => r.value.beacon === beacon); 98 - if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`); 99 - for (const cap of caps) { 100 - cap._handle = handleMap.get(repoDid) || repoDid; 101 - cap._type = 'cap'; 102 - } 103 - allItems.push(...caps); 93 + if (wantCaps && beacon) { 94 + const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION); 95 + const caps = res.records.filter(r => r.value.beacon === beacon); 96 + if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`); 97 + for (const cap of caps) { 98 + cap._handle = handleMap.get(repoDid) || repoDid; 99 + cap._type = 'cap'; 104 100 } 101 + allItems.push(...caps); 102 + } 105 103 106 - // Fetch skills (unfiltered — skills are universal) 107 - if (wantSkills) { 108 - try { 109 - const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50); 110 - if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} skills`); 111 - for (const skill of res.records) { 112 - skill._handle = handleMap.get(repoDid) || repoDid; 113 - skill._type = 'skill'; 114 - } 115 - allItems.push(...res.records); 116 - } catch (err) { 117 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`); 104 + if (wantSkills) { 105 + try { 106 + const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION); 107 + if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} skills`); 108 + for (const skill of res.records) { 109 + skill._handle = handleMap.get(repoDid) || repoDid; 110 + skill._type = 'skill'; 118 111 } 112 + allItems.push(...res.records); 113 + } catch (err) { 114 + if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`); 119 115 } 120 - } catch (err) { 121 - if (verbose) console.log(`[verbose] ${repoDid}: error: ${err.message}`); 122 116 } 123 - } 117 + }); 124 118 125 119 // sort by createdAt descending 126 120 allItems.sort((a, b) => {
+21 -29
src/cmd/vet.js
··· 11 11 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 12 12 import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; 13 13 import { mark, brand, name } from '../lib/brand.js'; 14 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 14 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 15 15 16 16 function ensureGitignore() { 17 17 const gitignorePath = join(vitDir(), '.gitignore'); ··· 139 139 140 140 // fetch caps from each DID, find matching ref 141 141 let match = null; 142 - for (const repoDid of dids) { 143 - try { 144 - const pds = await resolvePds(repoDid); 145 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 146 - const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 147 - for (const rec of res.records) { 148 - if (rec.value.beacon !== beacon) continue; 149 - const recRef = resolveRef(rec.value, rec.cid); 150 - if (recRef === ref) { 151 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 152 - match = rec; 153 - } 142 + await queryDidsInParallel(dids, async (repoDid) => { 143 + const pds = await resolvePds(repoDid); 144 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 145 + const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION); 146 + for (const rec of res.records) { 147 + if (rec.value.beacon !== beacon) continue; 148 + const recRef = resolveRef(rec.value, rec.cid); 149 + if (recRef === ref) { 150 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 151 + match = rec; 154 152 } 155 153 } 156 - } catch (err) { 157 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`); 158 154 } 159 - } 155 + }); 160 156 161 157 if (!match) { 162 158 console.error(`no cap found with ref '${ref}' for this beacon.`); ··· 214 210 if (verbose) console.log(`[verbose] querying ${dids.length} accounts`); 215 211 216 212 let match = null; 217 - for (const repoDid of dids) { 218 - try { 219 - const pds = await resolvePds(repoDid); 220 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 221 - const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50); 222 - for (const rec of res.records) { 223 - if (rec.value.name === skillName) { 224 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 225 - match = rec; 226 - } 213 + await queryDidsInParallel(dids, async (repoDid) => { 214 + const pds = await resolvePds(repoDid); 215 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 216 + const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION); 217 + for (const rec of res.records) { 218 + if (rec.value.name === skillName) { 219 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 220 + match = rec; 227 221 } 228 222 } 229 - } catch (err) { 230 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`); 231 223 } 232 - } 224 + }); 233 225 234 226 if (!match) { 235 227 console.error(`no skill found with ref '${ref}' from followed accounts.`);
+23 -33
src/cmd/vouch.js
··· 9 9 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 10 10 import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; 11 11 import { mark, name } from '../lib/brand.js'; 12 - import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 12 + import { resolvePds, listRecordsFromPds, queryDidsInParallel } from '../lib/pds.js'; 13 13 14 14 export default function register(program) { 15 15 program ··· 70 70 if (verbose) console.log(`[verbose] querying ${dids.length} accounts`); 71 71 72 72 let match = null; 73 - for (const repoDid of dids) { 74 - try { 75 - const pds = await resolvePds(repoDid); 76 - if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 77 - const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50); 78 - for (const rec of res.records) { 79 - if (rec.value.name === skillName) { 80 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 81 - match = rec; 82 - } 73 + await queryDidsInParallel(dids, async (repoDid) => { 74 + const pds = await resolvePds(repoDid); 75 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 76 + const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION); 77 + for (const rec of res.records) { 78 + if (rec.value.name === skillName) { 79 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 80 + match = rec; 83 81 } 84 82 } 85 - } catch (err) { 86 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`); 87 83 } 88 - } 84 + }); 89 85 90 86 if (!match) { 91 87 console.error(`no skill found with ref '${ref}' from followed accounts.`); ··· 111 107 collection: VOUCH_COLLECTION, 112 108 rkey, 113 109 record: vouchRecord, 114 - validate: false, 110 + validate: true, 115 111 }); 116 112 117 113 try { ··· 163 159 if (verbose) console.log(`[verbose] querying ${dids.length} accounts`); 164 160 165 161 let match = null; 166 - for (const repoDid of dids) { 167 - try { 168 - const res = await agent.com.atproto.repo.listRecords({ 169 - repo: repoDid, 170 - collection: CAP_COLLECTION, 171 - limit: 50, 172 - }); 173 - for (const rec of res.data.records) { 174 - if (rec.value.beacon !== beacon) continue; 175 - const recRef = resolveRef(rec.value, rec.cid); 176 - if (recRef === ref) { 177 - if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 178 - match = rec; 179 - } 162 + await queryDidsInParallel(dids, async (repoDid) => { 163 + const pds = await resolvePds(repoDid); 164 + if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`); 165 + const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION); 166 + for (const rec of res.records) { 167 + if (rec.value.beacon !== beacon) continue; 168 + const recRef = resolveRef(rec.value, rec.cid); 169 + if (recRef === ref) { 170 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 171 + match = rec; 180 172 } 181 173 } 182 - } catch (err) { 183 - if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`); 184 174 } 185 - } 175 + }); 186 176 187 177 if (!match) { 188 178 console.error(`no cap found with ref '${ref}' for this beacon.`); ··· 208 198 collection: VOUCH_COLLECTION, 209 199 rkey, 210 200 record: vouchRecord, 211 - validate: false, 201 + validate: true, 212 202 }); 213 203 214 204 try {
+1
src/lib/constants.js
··· 4 4 export const CAP_COLLECTION = 'org.v-it.cap'; 5 5 export const VOUCH_COLLECTION = 'org.v-it.vouch'; 6 6 export const SKILL_COLLECTION = 'org.v-it.skill'; 7 + export const JETSTREAM_URL = process.env.VIT_JETSTREAM_URL || 'wss://jetstream2.us-east.bsky.network/subscribe';
+46 -10
src/lib/pds.js
··· 4 4 const PLC_DIRECTORY = 'https://plc.directory'; 5 5 const pdsCache = new Map(); 6 6 7 + function didDocUrl(did) { 8 + if (did.startsWith('did:web:')) { 9 + const parts = did.slice('did:web:'.length).split(':'); 10 + const domain = decodeURIComponent(parts[0]); 11 + const path = parts.slice(1).map(decodeURIComponent).join('/'); 12 + return path 13 + ? `https://${domain}/${path}/did.json` 14 + : `https://${domain}/.well-known/did.json`; 15 + } 16 + return `${PLC_DIRECTORY}/${did}`; 17 + } 18 + 7 19 export async function resolvePds(did) { 8 20 if (pdsCache.has(did)) return pdsCache.get(did); 9 - const res = await fetch(`${PLC_DIRECTORY}/${did}`); 21 + const res = await fetch(didDocUrl(did)); 10 22 if (!res.ok) throw new Error(`failed to resolve PDS for ${did}: ${res.status} ${res.statusText}`); 11 23 const doc = await res.json(); 12 24 const pds = doc.service?.find(s => s.type === 'AtprotoPersonalDataServer'); ··· 15 27 return pds.serviceEndpoint; 16 28 } 17 29 18 - export async function listRecordsFromPds(pdsUrl, repo, collection, limit) { 19 - const url = new URL('/xrpc/com.atproto.repo.listRecords', pdsUrl); 20 - url.searchParams.set('repo', repo); 21 - url.searchParams.set('collection', collection); 22 - if (limit) url.searchParams.set('limit', String(limit)); 23 - const res = await fetch(url); 24 - if (!res.ok) throw new Error(`listRecords failed for ${repo}: ${res.status} ${res.statusText}`); 25 - return res.json(); 30 + export async function listRecordsFromPds(pdsUrl, repo, collection) { 31 + const all = []; 32 + let cursor; 33 + do { 34 + const url = new URL('/xrpc/com.atproto.repo.listRecords', pdsUrl); 35 + url.searchParams.set('repo', repo); 36 + url.searchParams.set('collection', collection); 37 + url.searchParams.set('limit', '100'); 38 + if (cursor) url.searchParams.set('cursor', cursor); 39 + const res = await fetch(url); 40 + if (!res.ok) throw new Error(`listRecords failed for ${repo}: ${res.status} ${res.statusText}`); 41 + const data = await res.json(); 42 + all.push(...(data.records || [])); 43 + cursor = data.cursor; 44 + } while (cursor); 45 + return { records: all }; 26 46 } 27 47 28 48 export async function resolveHandleFromDid(did) { 29 - const res = await fetch(`${PLC_DIRECTORY}/${did}`); 49 + const res = await fetch(didDocUrl(did)); 30 50 if (!res.ok) return did; 31 51 const doc = await res.json(); 32 52 const aka = doc.alsoKnownAs?.find(a => a.startsWith('at://')); 33 53 return aka ? aka.replace('at://', '') : did; 34 54 } 55 + 56 + export async function queryDidsInParallel(dids, queryFn, { concurrency = 10 } = {}) { 57 + const results = []; 58 + for (let i = 0; i < dids.length; i += concurrency) { 59 + const batch = dids.slice(i, i + concurrency); 60 + const settled = await Promise.allSettled(batch.map(queryFn)); 61 + for (const result of settled) { 62 + if (result.status === 'fulfilled') { 63 + results.push(result.value); 64 + } else { 65 + console.error(`warning: query failed: ${result.reason?.message || result.reason}`); 66 + } 67 + } 68 + } 69 + return results; 70 + }
+19
test/jetstream-help.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, expect, test } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + 7 + describe('jetstream help', () => { 8 + test('firehose help shows --jetstream', () => { 9 + const result = run('firehose --help'); 10 + expect(result.exitCode).toBe(0); 11 + expect(result.stdout).toContain('--jetstream <url>'); 12 + }); 13 + 14 + test('scan help shows --jetstream', () => { 15 + const result = run('scan --help'); 16 + expect(result.exitCode).toBe(0); 17 + expect(result.stdout).toContain('--jetstream <url>'); 18 + }); 19 + });
+110
test/pds.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { afterEach, describe, expect, test } from 'bun:test'; 5 + import { listRecordsFromPds, resolvePds, resolveHandleFromDid } from '../src/lib/pds.js'; 6 + 7 + const originalFetch = globalThis.fetch; 8 + 9 + afterEach(() => { 10 + globalThis.fetch = originalFetch; 11 + }); 12 + 13 + describe('pds helpers', () => { 14 + test('listRecordsFromPds follows pagination cursors and returns all records', async () => { 15 + const calls = []; 16 + globalThis.fetch = async (url) => { 17 + calls.push(String(url)); 18 + const cursor = new URL(String(url)).searchParams.get('cursor'); 19 + const body = cursor === 'next-page' 20 + ? { records: [{ uri: 'at://did:plc:123/org.v-it.cap/2', value: { text: 'two' } }] } 21 + : { 22 + records: [{ uri: 'at://did:plc:123/org.v-it.cap/1', value: { text: 'one' } }], 23 + cursor: 'next-page', 24 + }; 25 + return new Response(JSON.stringify(body), { 26 + status: 200, 27 + headers: { 'content-type': 'application/json' }, 28 + }); 29 + }; 30 + 31 + const res = await listRecordsFromPds('https://pds.example', 'did:plc:123', 'org.v-it.cap'); 32 + 33 + expect(res.records).toHaveLength(2); 34 + expect(calls).toHaveLength(2); 35 + expect(calls[0]).toContain('limit=100'); 36 + expect(calls[1]).toContain('cursor=next-page'); 37 + }); 38 + 39 + test('resolvePds uses .well-known did document for did:web domains', async () => { 40 + const calls = []; 41 + globalThis.fetch = async (url) => { 42 + calls.push(String(url)); 43 + return new Response(JSON.stringify({ 44 + service: [{ type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example' }], 45 + }), { 46 + status: 200, 47 + headers: { 'content-type': 'application/json' }, 48 + }); 49 + }; 50 + 51 + const pds = await resolvePds('did:web:example.com'); 52 + 53 + expect(pds).toBe('https://pds.example'); 54 + expect(calls[0]).toBe('https://example.com/.well-known/did.json'); 55 + }); 56 + 57 + test('resolvePds uses path did document for did:web with path segments', async () => { 58 + const calls = []; 59 + globalThis.fetch = async (url) => { 60 + calls.push(String(url)); 61 + return new Response(JSON.stringify({ 62 + service: [{ type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example/path' }], 63 + }), { 64 + status: 200, 65 + headers: { 'content-type': 'application/json' }, 66 + }); 67 + }; 68 + 69 + const pds = await resolvePds('did:web:example.com:path:segment'); 70 + 71 + expect(pds).toBe('https://pds.example/path'); 72 + expect(calls[0]).toBe('https://example.com/path/segment/did.json'); 73 + }); 74 + 75 + test('resolvePds still uses plc.directory for did:plc identifiers', async () => { 76 + const calls = []; 77 + globalThis.fetch = async (url) => { 78 + calls.push(String(url)); 79 + return new Response(JSON.stringify({ 80 + service: [{ type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example/plc' }], 81 + }), { 82 + status: 200, 83 + headers: { 'content-type': 'application/json' }, 84 + }); 85 + }; 86 + 87 + const pds = await resolvePds('did:plc:abc123'); 88 + 89 + expect(pds).toBe('https://pds.example/plc'); 90 + expect(calls[0]).toBe('https://plc.directory/did:plc:abc123'); 91 + }); 92 + 93 + test('resolveHandleFromDid uses did:web URL for did:web identifiers', async () => { 94 + const calls = []; 95 + globalThis.fetch = async (url) => { 96 + calls.push(String(url)); 97 + return new Response(JSON.stringify({ 98 + alsoKnownAs: ['at://alice.example.com'], 99 + }), { 100 + status: 200, 101 + headers: { 'content-type': 'application/json' }, 102 + }); 103 + }; 104 + 105 + const handle = await resolveHandleFromDid('did:web:example.com'); 106 + 107 + expect(handle).toBe('alice.example.com'); 108 + expect(calls[0]).toBe('https://example.com/.well-known/did.json'); 109 + }); 110 + });
+13
test/ship.test.js
··· 64 64 expect(r.stderr).toMatch(/--recap must be exactly three lowercase words/i); 65 65 }); 66 66 67 + test('rejects --kind with invalid value', () => { 68 + const r = run('ship --title "Hi" --description "desc" --ref "one-two-three" --kind "invalid" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 69 + expect(r.exitCode).not.toBe(0); 70 + expect(r.stderr).toMatch(/--kind must be one of/i); 71 + }); 72 + 73 + test('--help shows --kind and --recap', () => { 74 + const r = run('ship --help'); 75 + expect(r.exitCode).toBe(0); 76 + expect(r.stdout).toContain('--kind <kind>'); 77 + expect(r.stdout).toContain('--recap <ref>'); 78 + }); 79 + 67 80 test('accepts valid ref format (fails at auth, not validation)', () => { 68 81 const r = run('ship --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 69 82 expect(r.exitCode).not.toBe(0);