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:
+48
-19
@@ -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
@@ -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 中可读
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user