This repository has no description
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
51 const conditions = [];
52 const bindings = [];
53
54 if (beacon) {
55 conditions.push('c.beacon = ?');
56 bindings.push(beacon);
57 }
58
59 if (cursor) {
60 conditions.push('c.id < ?');
61 bindings.push(cursor);
62 }
63
64 let sql = 'SELECT c.*, h.handle FROM caps c LEFT JOIN handles h ON c.did = h.did';
65 if (conditions.length > 0) {
66 sql += ` WHERE ${conditions.join(' AND ')}`;
67 }
68 sql += ' ORDER BY c.id DESC LIMIT ?';
69 bindings.push(limit);
70
71 const { results } = await env.DB.prepare(sql).bind(...bindings).all();
72 return json({
73 caps: results,
74 cursor: results.length > 0 ? results[results.length - 1].id : null,
75 });
76 }
77
78 if (pathname === '/api/vouches') {
79 const capUri = searchParams.get('cap_uri');
80 if (!capUri) {
81 return json({ error: 'cap_uri is required' }, 400);
82 }
83
84 const { results } = await env.DB.prepare(
85 `SELECT v.*, h.handle
86 FROM vouches v
87 LEFT JOIN handles h ON v.did = h.did
88 WHERE v.cap_uri = ?
89 ORDER BY v.id DESC`,
90 )
91 .bind(capUri)
92 .all();
93
94 return json({ vouches: results });
95 }
96
97 if (pathname === '/api/beacons') {
98 const { results } = await env.DB.prepare('SELECT * FROM beacons ORDER BY last_activity DESC').all();
99 return json({ beacons: results });
100 }
101
102 if (pathname === '/api/skills') {
103 const cursor = parseCursor(searchParams.get('cursor'));
104 const limit = parseLimit(searchParams.get('limit'));
105 const tag = searchParams.get('tag');
106
107 const conditions = [];
108 const bindings = [];
109
110 if (tag) {
111 conditions.push('INSTR(s.tags, ?) > 0');
112 bindings.push(tag);
113 }
114
115 if (cursor) {
116 conditions.push('s.id < ?');
117 bindings.push(cursor);
118 }
119
120 let sql = 'SELECT s.*, h.handle FROM skills s LEFT JOIN handles h ON s.did = h.did';
121 if (conditions.length > 0) {
122 sql += ` WHERE ${conditions.join(' AND ')}`;
123 }
124 sql += ' ORDER BY s.id DESC LIMIT ?';
125 bindings.push(limit);
126
127 const { results } = await env.DB.prepare(sql).bind(...bindings).all();
128 return json({
129 skills: results,
130 cursor: results.length > 0 ? results[results.length - 1].id : null,
131 });
132 }
133
134 if (pathname === '/api/stats') {
135 const [caps, vouches, beacons, dids, skills, skillPubs] = await env.DB.batch([
136 env.DB.prepare('SELECT COUNT(*) as count FROM caps'),
137 env.DB.prepare('SELECT COUNT(*) as count FROM vouches'),
138 env.DB.prepare('SELECT COUNT(*) as count FROM beacons'),
139 env.DB.prepare('SELECT COUNT(DISTINCT did) as count FROM caps'),
140 env.DB.prepare('SELECT COUNT(*) as count FROM skills'),
141 env.DB.prepare('SELECT COUNT(DISTINCT did) as count FROM skills'),
142 ]);
143
144 return json({
145 total_caps: caps.results[0]?.count ?? 0,
146 total_vouches: vouches.results[0]?.count ?? 0,
147 total_beacons: beacons.results[0]?.count ?? 0,
148 active_dids: dids.results[0]?.count ?? 0,
149 total_skills: skills.results[0]?.count ?? 0,
150 skill_publishers: skillPubs.results[0]?.count ?? 0,
151 });
152 }
153
154 return json({ error: 'not found' }, 404);
155}