forked from koshikraj/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagents.ts
More file actions
64 lines (59 loc) · 2.67 KB
/
Copy pathagents.ts
File metadata and controls
64 lines (59 loc) · 2.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { Hono, type MiddlewareHandler } from 'hono'
import { z } from 'zod'
import type { Db } from '../db/client.js'
import { SCOPE_COPY, listGrants, revokeGrant } from '../oauth/index.js'
/**
* The agents a person has authorised, and the control to end one.
*
* Grants rather than tokens, because a token is the wrong unit for both halves
* of this screen: they rotate hourly, so "connected since" would drift, and
* revoking would have to chase every row instead of setting one flag.
*
* A revoked grant stays in the list. Losing it silently would make revocation
* feel like it might not have worked, on the one screen where that doubt is
* least acceptable.
*/
export function agentRoutes(db: Db, session: MiddlewareHandler): Hono {
const app = new Hono()
app.use('*', session)
app.get('/', async (c) => {
const grants = await listGrants(db, c.get('userId'))
return c.json({
agents: grants.map((grant) => ({
id: grant.id,
name: grant.clientName,
uri: grant.clientUri,
// The callbacks it registered. Not secret — they are the agent's own,
// and the person approving the grant already saw one on the consent
// screen. They are also the only thing we know about an agent that the
// agent did not simply assert.
redirectUris: grant.redirectUris,
grantedAt: grant.grantedAt.toISOString(),
lastUsedAt: grant.lastUsedAt?.toISOString() ?? null,
revokedAt: grant.revokedAt?.toISOString() ?? null,
// The same wording as the consent screen. A person should recognise
// what they approved, and two sets of words for one permission is how
// a grant screen and a settings screen start describing different
// products.
scopes: SCOPE_COPY.filter((entry) => grant.scopes.includes(entry.scope)),
})),
})
})
/**
* DELETE, because from the caller's side this ends the grant. The row
* survives — that is our bookkeeping, not their mental model.
*/
app.delete('/:id', async (c) => {
const id = c.req.param('id')
// Postgres raises on a malformed uuid, which would surface as a 500 for
// what is plainly a bad request. Same answer as an id that does not exist.
if (!z.uuid().safeParse(id).success) return c.json({ error: 'not_found' }, 404)
const revoked = await revokeGrant(db, c.get('userId'), id)
// 404 rather than 204 when nothing was live: a caller who revokes twice
// should be able to tell that the second call did nothing, and an id that
// belongs to someone else must not read as a success.
if (!revoked) return c.json({ error: 'not_found' }, 404)
return c.body(null, 204)
})
return app
}