security: require auth token for all API endpoints except health — 小橘 🍊

- query, inspect, invoke now require Authorization: Bearer token
- Only /_health remains public (monitoring/uptime checks)
- Data sovereignty: CF resources belong to the user, no anonymous access
This commit is contained in:
2026-04-03 11:54:02 +00:00
parent e86bae8d4a
commit d80cc1b9e0
3 changed files with 54 additions and 25 deletions
+48 -19
View File
@@ -31,7 +31,7 @@ export async function handleRequest(request: Request, env: RouterEnv): Promise<R
return handleRemove(request, env)
}
// GET /_api/query — public, no auth
// GET /_api/query
if (method === 'GET' && path === '/_api/query') {
return handleQuery(request, env)
}
@@ -40,10 +40,10 @@ export async function handleRequest(request: Request, env: RouterEnv): Promise<R
const inspectMatch = path.match(/^\/_api\/inspect\/(.+)$/)
if (method === 'GET' && inspectMatch) {
const capability = inspectMatch[1]!
return handleInspect(capability, env)
return handleInspect(capability, request, env)
}
// GET /run/{capability} — invoke (no auth required)
// /run/{capability} — invoke
const runMatch = path.match(/^\/run\/([^/]+)$/)
if (runMatch) {
const capability = runMatch[1]!
@@ -146,24 +146,44 @@ async function handleRemove(request: Request, env: RouterEnv): Promise<Response>
}
async function handleQuery(request: Request, env: RouterEnv): Promise<Response> {
const url = new URL(request.url)
const q = url.searchParams.get('q') ?? undefined
const modeRaw = url.searchParams.get('mode')
const mode = (modeRaw === 'find' || modeRaw === 'explore') ? modeRaw : undefined
const limitRaw = url.searchParams.get('limit')
const limit = limitRaw ? parseInt(limitRaw, 10) : undefined
const cursor = url.searchParams.get('cursor') ?? undefined
try {
const authHeader = request.headers.get('Authorization')
await env.auth.validateToken(authHeader)
const result = await env.backend.query({ q, mode, limit, cursor })
return jsonOk(result)
const url = new URL(request.url)
const q = url.searchParams.get('q') ?? undefined
const modeRaw = url.searchParams.get('mode')
const mode = (modeRaw === 'find' || modeRaw === 'explore') ? modeRaw : undefined
const limitRaw = url.searchParams.get('limit')
const limit = limitRaw ? parseInt(limitRaw, 10) : undefined
const cursor = url.searchParams.get('cursor') ?? undefined
const result = await env.backend.query({ q, mode, limit, cursor })
return jsonOk(result)
} catch (e) {
if (e instanceof AuthError) {
return jsonError(e.status, e.message)
}
throw e
}
}
async function handleInspect(capability: string, env: RouterEnv): Promise<Response> {
const result = await env.backend.inspect(capability)
if (!result) {
return jsonError(404, 'Capability not found')
async function handleInspect(capability: string, request: Request, env: RouterEnv): Promise<Response> {
try {
const authHeader = request.headers.get('Authorization')
await env.auth.validateToken(authHeader)
const result = await env.backend.inspect(capability)
if (!result) {
return jsonError(404, 'Capability not found')
}
return jsonOk(result)
} catch (e) {
if (e instanceof AuthError) {
return jsonError(e.status, e.message)
}
throw e
}
return jsonOk(result)
}
async function handleInvoke(
@@ -171,8 +191,17 @@ async function handleInvoke(
request: Request,
env: RouterEnv,
): Promise<Response> {
// Direct invocation via Dynamic Workers — no redirect, no sub-worker fetch
return await env.backend.invoke(capability, request)
try {
const authHeader = request.headers.get('Authorization')
await env.auth.validateToken(authHeader)
return await env.backend.invoke(capability, request)
} catch (e) {
if (e instanceof AuthError) {
return jsonError(e.status, e.message)
}
throw e
}
}
function jsonOk(body: unknown, status = 200): Response {
+5 -5
View File
@@ -54,7 +54,7 @@ describe('Query API', () => {
// Test 1: 无参数 query → explore 模式,全量摘要(不用 embedding)
it('无参数 query → 返回全部能力(explore 摘要格式)', async () => {
const req = makeRequest('GET', '/_api/query')
const req = makeRequest('GET', '/_api/query', { token: 'deploy-token' })
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(resp.status).toBe(200)
@@ -273,18 +273,18 @@ describe('Query API', () => {
})
it('limit via URL query string', async () => {
const req = makeRequest('GET', '/_api/query?limit=2')
const req = makeRequest('GET', '/_api/query?limit=2', { token: 'deploy-token' })
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
const body = await resp.json() as { total: number; items: unknown[] }
expect(body.items).toHaveLength(2)
expect(body.total).toBe(3)
})
// Test 11: query 需要 auth token
it('query 接口公开,不需要 token', async () => {
// Test 11: query 需要 auth token
it('query 接口需要 token', async () => {
const req = makeRequest('GET', '/_api/query')
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(resp.status).toBe(200)
expect(resp.status).toBe(401)
})
// Test 12: deploy metadata 存储并在 query 中可读
+1 -1
View File
@@ -43,7 +43,7 @@ describe('S7: 列出能力(已迁移至 query 接口)', () => {
})
it('/_api/query should return all capabilities (explore mode)', async () => {
const req = makeRequest('GET', '/_api/query')
const req = makeRequest('GET', '/_api/query', { token: 'deploy-token' })
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
expect(resp.status).toBe(200)