feat(auth): shared-secret auth web-app → ops-agent
- ops-agent/src/auth.ts: constant-time compare via timingSafeEqual to prevent timing attacks; store secret as Buffer - ops-agent/src/index.ts + ops-agent.service: bind on 127.0.0.1:3099 (was 4242, per plan) - app/api/agent/[...path]/route.ts: Next.js proxy route that verifies ops_session cookie then forwards requests to agent with Authorization: Bearer <secret> - .env.example + deploy/ops-dashboard.env.example: add OPS_AGENT_SECRET and OPS_AGENT_URL - README.md: rotation procedure for the shared secret Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d605eb17a5
commit
92d450609c
7 changed files with 90 additions and 8 deletions
46
app/api/agent/[...path]/route.ts
Normal file
46
app/api/agent/[...path]/route.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { NextRequest } from 'next/server'
|
||||
import { getCurrentUser } from '@/lib/session'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const AGENT_URL = process.env.OPS_AGENT_URL ?? 'http://127.0.0.1:3099'
|
||||
const AGENT_SECRET = process.env.OPS_AGENT_SECRET ?? ''
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> },
|
||||
) {
|
||||
const user = await getCurrentUser()
|
||||
if (!user) {
|
||||
return Response.json({ error: 'unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { path } = await params
|
||||
const subpath = path.join('/')
|
||||
const body = await request.text()
|
||||
|
||||
let agentResponse: Response
|
||||
try {
|
||||
agentResponse = await fetch(`${AGENT_URL}/agent/v1/${subpath}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AGENT_SECRET}`,
|
||||
},
|
||||
body,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'agent unreachable'
|
||||
return Response.json({ error: message }, { status: 502 })
|
||||
}
|
||||
|
||||
const contentType = agentResponse.headers.get('Content-Type') ?? 'application/json'
|
||||
return new Response(agentResponse.body, {
|
||||
status: agentResponse.status,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue