-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.ts
More file actions
67 lines (60 loc) · 2.67 KB
/
Copy pathauth.ts
File metadata and controls
67 lines (60 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
65
66
67
import type { AppToolContext } from './types'
/**
* Header names carrying the server-set per-turn context + the capability token.
* Defaults are product-neutral (`X-Agent-App-*`); a product that already ships
* a header convention (e.g. `X-Acme-User-Id`) passes its own.
*/
export interface ToolHeaderNames {
userId: string
workspaceId: string
threadId: string
}
export const DEFAULT_HEADER_NAMES: ToolHeaderNames = {
userId: 'X-Agent-App-User-Id',
workspaceId: 'X-Agent-App-Workspace-Id',
threadId: 'X-Agent-App-Thread-Id',
}
export interface AuthenticateOptions {
/** Verify the bearer capability token belongs to `userId`. The product's
* HMAC/JWT impl — the seam that keeps token crypto out of this package. */
verifyToken: (userId: string, bearer: string) => Promise<boolean>
headerNames?: ToolHeaderNames
}
export type ToolAuthResult =
| { ok: true; ctx: AppToolContext }
| { ok: false; response: Response }
/**
* Recover + verify the trusted context for a tool request. The user comes from
* a server-set header and the bearer token MUST verify against THAT user; the
* workspace comes from a header too — never from tool args — so the model can
* neither forge identity nor target another workspace. Fail-closed: any missing
* credential or a token minted for another user yields a 401/400 Response.
*/
export async function authenticateToolRequest(request: Request, opts: AuthenticateOptions): Promise<ToolAuthResult> {
const h = opts.headerNames ?? DEFAULT_HEADER_NAMES
const userId = request.headers.get(h.userId)?.trim()
const workspaceId = request.headers.get(h.workspaceId)?.trim()
const threadId = request.headers.get(h.threadId)?.trim() || null
const bearer = request.headers.get('authorization')?.match(/^Bearer\s+(.+)$/i)?.[1]
if (!userId || !bearer) {
return { ok: false, response: Response.json({ error: 'Missing capability credentials' }, { status: 401 }) }
}
if (!(await opts.verifyToken(userId, bearer))) {
return { ok: false, response: Response.json({ error: 'Invalid capability token' }, { status: 401 }) }
}
if (!workspaceId) {
return { ok: false, response: Response.json({ error: 'Missing workspace context' }, { status: 400 }) }
}
return { ok: true, ctx: { userId, workspaceId, threadId } }
}
/** Read a tool's argument object from the request body, tolerant of MCP host
* aliases (`args` / `arguments`) or a bare body. Returns null on non-JSON. */
export async function readToolArgs<T>(request: Request): Promise<T | null> {
let body: { args?: T; arguments?: T }
try {
body = (await request.json()) as typeof body
} catch {
return null
}
return (body.args ?? body.arguments ?? (body as T)) as T
}