File
Blob: worker/src/server/index.ts
| 1 | import { Hono } from 'hono'; |
| 2 | import { assertSameOrigin, authenticated } from './auth.ts'; |
| 3 | import { equal } from './crypto.ts'; |
| 4 | import { demand, OperationError } from './rpc.ts'; |
| 5 | import { json } from './http.ts'; |
| 6 | import type { HttpApp } from './http.ts'; |
| 7 | import access from './routes/access.ts'; |
| 8 | import device from './routes/device.ts'; |
| 9 | import viewers from './routes/viewers.ts'; |
| 10 | export { RobotRoom } from './robot-room.ts'; |
| 11 | |
| 12 | const app = new Hono<HttpApp>(); |
| 13 | |
| 14 | app.use('/api/*', async (c, next) => { |
| 15 | const requestId = crypto.randomUUID(), |
| 16 | began = Date.now(); |
| 17 | c.set('requestId', requestId); |
| 18 | c.header('X-Request-Id', requestId); |
| 19 | c.header('Cache-Control', 'no-store'); |
| 20 | demand(['GET', 'POST'].includes(c.req.method), 405, 'Method not allowed.'); |
| 21 | assertSameOrigin(c.req.raw); |
| 22 | if (c.req.path !== '/api/login') { |
| 23 | if (c.req.path.startsWith('/api/device/')) { |
| 24 | demand( |
| 25 | await equal(c.req.header('Authorization') ?? '', `Bearer ${c.env.DEVICE_TOKEN}`), |
| 26 | 401, |
| 27 | 'Device authentication required.', |
| 28 | ); |
| 29 | } else demand(await authenticated(c), 401, 'Enter the viewer password to listen.'); |
| 30 | } |
| 31 | await next(); |
| 32 | if (c.res.status >= 500 || (c.req.method === 'POST' && !c.req.path.endsWith('/heartbeat'))) { |
| 33 | console.log( |
| 34 | JSON.stringify({ |
| 35 | event: 'request.complete', |
| 36 | requestId, |
| 37 | operation: c.req.path.replace(/[a-f0-9-]{36}/g, ':viewer'), |
| 38 | status: c.res.status, |
| 39 | durationMs: Date.now() - began, |
| 40 | }), |
| 41 | ); |
| 42 | } |
| 43 | }); |
| 44 | |
| 45 | app.route('/api', access); |
| 46 | app.route('/api/device', device); |
| 47 | app.route('/api/viewers', viewers); |
| 48 | |
| 49 | app.all('/api/*', (c) => |
| 50 | json( |
| 51 | { error: 'Unknown radio operation.', requestId: c.get('requestId') }, |
| 52 | c.req.method === 'GET' ? 405 : 404, |
| 53 | ), |
| 54 | ); |
| 55 | app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw)); |
| 56 | app.onError((error, c) => { |
| 57 | const status = error instanceof OperationError ? error.status : 500; |
| 58 | if (status >= 500) |
| 59 | console.error(JSON.stringify({ phase: 'request', status, requestId: c.get('requestId') })); |
| 60 | return json( |
| 61 | { |
| 62 | error: |
| 63 | error instanceof OperationError |
| 64 | ? error.message |
| 65 | : 'The radio service could not complete this request.', |
| 66 | requestId: c.get('requestId'), |
| 67 | }, |
| 68 | status, |
| 69 | ); |
| 70 | }); |
| 71 | export default app; |