open source is social v-it.org
at main 257 lines 7.3 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4const CORS_HEADERS = { 5 'Access-Control-Allow-Origin': '*', 6 'Access-Control-Allow-Methods': 'GET, OPTIONS', 7 'Access-Control-Allow-Headers': 'Content-Type', 8}; 9 10function json(data, status = 200) { 11 return new Response(JSON.stringify(data), { 12 status, 13 headers: { 'Content-Type': 'application/json', ...CORS_HEADERS }, 14 }); 15} 16 17function parseLimit(value) { 18 const parsed = Number.parseInt(value ?? '50', 10); 19 if (!Number.isFinite(parsed) || parsed <= 0) { 20 return 50; 21 } 22 return Math.min(parsed, 100); 23} 24 25function parseCursor(value) { 26 if (!value) { 27 return null; 28 } 29 30 const parsed = Number.parseInt(value, 10); 31 return Number.isFinite(parsed) && parsed > 0 ? parsed : null; 32} 33 34export async function handleRequest(request, env) { 35 if (request.method === 'OPTIONS') { 36 return new Response(null, { status: 204, headers: CORS_HEADERS }); 37 } 38 39 const url = new URL(request.url); 40 const { pathname, searchParams } = url; 41 42 if (request.method !== 'GET') { 43 return json({ error: 'method not allowed' }, 405); 44 } 45 46 if (pathname === '/api/caps') { 47 const cursor = parseCursor(searchParams.get('cursor')); 48 const limit = parseLimit(searchParams.get('limit')); 49 const beacon = searchParams.get('beacon'); 50 const kind = searchParams.get('kind'); 51 const sort = searchParams.get('sort'); 52 53 const conditions = []; 54 const bindings = []; 55 56 if (beacon) { 57 const beacons = beacon.split(',').filter(Boolean); 58 const placeholders = beacons.map(() => '?').join(', '); 59 conditions.push(`c.beacon IN (${placeholders})`); 60 bindings.push(...beacons); 61 } 62 63 if (kind) { 64 conditions.push('c.kind = ?'); 65 bindings.push(kind); 66 } 67 68 if (cursor) { 69 conditions.push('c.id < ?'); 70 bindings.push(cursor); 71 } 72 73 let sql = `SELECT c.*, h.handle, 74 (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = c.uri AND v.kind = 'want') as want_vouch_count 75 FROM caps c LEFT JOIN handles h ON c.did = h.did`; 76 if (conditions.length > 0) { 77 sql += ` WHERE ${conditions.join(' AND ')}`; 78 } 79 if (sort === 'want-vouches') { 80 sql += ' ORDER BY want_vouch_count DESC, c.id DESC'; 81 } else { 82 sql += ' ORDER BY c.id DESC'; 83 } 84 sql += ' LIMIT ?'; 85 bindings.push(limit); 86 87 const { results } = await env.DB.prepare(sql).bind(...bindings).all(); 88 return json({ 89 caps: results, 90 cursor: results.length > 0 ? results[results.length - 1].id : null, 91 }); 92 } 93 94 if (pathname === '/api/cap') { 95 const ref = searchParams.get('ref'); 96 const uri = searchParams.get('uri'); 97 const beacon = searchParams.get('beacon'); 98 99 if (!ref && !uri) { 100 return json({ error: 'ref or uri is required' }, 400); 101 } 102 103 if (ref && uri) { 104 return json({ error: 'provide ref or uri, not both' }, 400); 105 } 106 107 const conditions = []; 108 const bindings = []; 109 110 if (uri) { 111 conditions.push('c.uri = ?'); 112 bindings.push(uri); 113 } 114 115 if (ref) { 116 conditions.push('c.ref = ?'); 117 bindings.push(ref); 118 119 if (beacon) { 120 const beacons = beacon.split(',').filter(Boolean); 121 const placeholders = beacons.map(() => '?').join(', '); 122 conditions.push(`c.beacon IN (${placeholders})`); 123 bindings.push(...beacons); 124 } 125 } 126 127 let sql = `SELECT c.*, h.handle, 128 (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = c.uri) as vouch_count 129 FROM caps c 130 LEFT JOIN handles h ON c.did = h.did 131 WHERE ${conditions.join(' AND ')}`; 132 sql += ' ORDER BY c.created_at DESC LIMIT 1'; 133 134 const result = await env.DB.prepare(sql).bind(...bindings).first(); 135 return json({ cap: result }); 136 } 137 138 if (pathname === '/api/vouches') { 139 const capUri = searchParams.get('cap_uri'); 140 if (!capUri) { 141 return json({ error: 'cap_uri is required' }, 400); 142 } 143 144 const { results } = await env.DB.prepare( 145 `SELECT v.*, h.handle 146 FROM vouches v 147 LEFT JOIN handles h ON v.did = h.did 148 WHERE v.cap_uri = ? 149 ORDER BY v.id DESC`, 150 ) 151 .bind(capUri) 152 .all(); 153 154 return json({ vouches: results }); 155 } 156 157 if (pathname === '/api/beacons') { 158 const { results } = await env.DB.prepare('SELECT * FROM beacons ORDER BY last_activity DESC').all(); 159 return json({ beacons: results }); 160 } 161 162 if (pathname === '/api/skills') { 163 const cursor = parseCursor(searchParams.get('cursor')); 164 const limit = parseLimit(searchParams.get('limit')); 165 const tag = searchParams.get('tag'); 166 167 const conditions = []; 168 const bindings = []; 169 170 if (tag) { 171 conditions.push('INSTR(s.tags, ?) > 0'); 172 bindings.push(tag); 173 } 174 175 const dids = searchParams.getAll('did'); 176 if (dids.length > 0) { 177 conditions.push('s.did IN (' + dids.map(function() { return '?'; }).join(', ') + ')'); 178 bindings.push(...dids); 179 } 180 181 if (cursor) { 182 conditions.push('s.id < ?'); 183 bindings.push(cursor); 184 } 185 186 let sql = 'SELECT s.*, h.handle FROM skills s LEFT JOIN handles h ON s.did = h.did'; 187 if (conditions.length > 0) { 188 sql += ` WHERE ${conditions.join(' AND ')}`; 189 } 190 sql += ' ORDER BY s.id DESC LIMIT ?'; 191 bindings.push(limit); 192 193 const { results } = await env.DB.prepare(sql).bind(...bindings).all(); 194 return json({ 195 skills: results, 196 cursor: results.length > 0 ? results[results.length - 1].id : null, 197 }); 198 } 199 200 if (pathname === '/api/skill') { 201 const name = searchParams.get('name'); 202 const uri = searchParams.get('uri'); 203 204 if (!name && !uri) { 205 return json({ error: 'name or uri is required' }, 400); 206 } 207 208 if (name && uri) { 209 return json({ error: 'provide name or uri, not both' }, 400); 210 } 211 212 const conditions = []; 213 const bindings = []; 214 215 if (uri) { 216 conditions.push('s.uri = ?'); 217 bindings.push(uri); 218 } 219 220 if (name) { 221 conditions.push('s.name = ?'); 222 bindings.push(name); 223 } 224 225 let sql = `SELECT s.*, h.handle, 226 (SELECT COUNT(*) FROM vouches v WHERE v.cap_uri = s.uri) as vouch_count 227 FROM skills s 228 LEFT JOIN handles h ON s.did = h.did 229 WHERE ${conditions.join(' AND ')}`; 230 sql += ' ORDER BY s.created_at DESC LIMIT 1'; 231 232 const result = await env.DB.prepare(sql).bind(...bindings).first(); 233 return json({ skill: result }); 234 } 235 236 if (pathname === '/api/stats') { 237 const [caps, vouches, beacons, dids, skills, skillPubs] = await env.DB.batch([ 238 env.DB.prepare('SELECT COUNT(*) as count FROM caps'), 239 env.DB.prepare('SELECT COUNT(*) as count FROM vouches'), 240 env.DB.prepare('SELECT COUNT(*) as count FROM beacons'), 241 env.DB.prepare('SELECT COUNT(DISTINCT did) as count FROM caps'), 242 env.DB.prepare('SELECT COUNT(*) as count FROM skills'), 243 env.DB.prepare('SELECT COUNT(DISTINCT did) as count FROM skills'), 244 ]); 245 246 return json({ 247 total_caps: caps.results[0]?.count ?? 0, 248 total_vouches: vouches.results[0]?.count ?? 0, 249 total_beacons: beacons.results[0]?.count ?? 0, 250 active_dids: dids.results[0]?.count ?? 0, 251 total_skills: skills.results[0]?.count ?? 0, 252 skill_publishers: skillPubs.results[0]?.count ?? 0, 253 }); 254 } 255 256 return json({ error: 'not found' }, 404); 257}