A minimal web editor for managing standard.site records in your atproto PDS
1import { Hono } from 'hono';
2import { serveStatic } from 'hono/bun';
3import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
4import { authRoutes } from './routes/auth';
5import { publicationRoutes } from './routes/publication';
6import { documentRoutes } from './routes/documents';
7import { layout } from './views/layouts/main';
8import { homePage } from './views/home';
9import { getSession } from './lib/session';
10import { getClientMetadata, getJwks } from './lib/oauth';
11import { csrfProtection, getCSRFToken } from './lib/csrf';
12
13export const app = new Hono();
14
15// Static files
16app.use('/public/*', serveStatic({ root: './' }));
17
18// OAuth metadata endpoints at root level
19// These MUST be publicly accessible (no authentication)
20app.get('/client-metadata.json', async (c) => {
21 try {
22 const metadata = await getClientMetadata();
23 // Set appropriate cache headers
24 c.header('Cache-Control', 'public, max-age=600'); // Cache for 10 minutes
25 c.header('Access-Control-Allow-Origin', '*');
26 return c.json(metadata);
27 } catch (error) {
28 console.error('Error getting client metadata:', error);
29 return c.json({ error: 'Failed to get client metadata' }, 500);
30 }
31});
32
33app.get('/jwks.json', async (c) => {
34 try {
35 const jwks = await getJwks();
36 // Set appropriate cache headers
37 c.header('Cache-Control', 'public, max-age=600'); // Cache for 10 minutes
38 c.header('Access-Control-Allow-Origin', '*');
39 return c.json(jwks);
40 } catch (error) {
41 console.error('Error getting JWKS:', error);
42 return c.json({ error: 'Failed to get JWKS' }, 500);
43 }
44});
45
46// Session middleware - adds session and CSRF token to context
47app.use('*', async (c, next) => {
48 const session = await getSession(c);
49 c.set('session', session);
50 // Generate CSRF token for all requests (sets cookie if not present)
51 const csrfToken = getCSRFToken(c);
52 c.set('csrfToken', csrfToken);
53 await next();
54});
55
56// CSRF protection for state-changing requests
57// Applied after session middleware but before routes
58app.use('/auth/*', csrfProtection);
59app.use('/publication/*', csrfProtection);
60app.use('/documents/*', csrfProtection);
61
62// Home page
63app.get('/', async (c) => {
64 const session = c.get('session');
65 return c.html(layout(homePage(session), { session }));
66});
67
68// Mount routes
69app.route('/auth', authRoutes);
70app.route('/publication', publicationRoutes);
71app.route('/documents', documentRoutes);
72
73const port = parseInt(process.env.PORT || '8000');
74console.log(`Starting server on http://localhost:${port}`);
75console.log(`Public URL: ${process.env.PUBLIC_URL || 'http://localhost:' + port}`);
76
77export default {
78 port,
79 fetch: app.fetch,
80};