feat(auth): validate agent context upstream
This commit is contained in:
+60
-6
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
|
||||
import { requireAgentAuth } from "./auth/agentAuth.js";
|
||||
import { SessionTranscriptStore } from "./sessions/transcriptStore.js";
|
||||
import { ChatSessionBridge } from "./chat/sessionBridge.js";
|
||||
import { config } from "./config.js";
|
||||
@@ -18,7 +19,11 @@ import {
|
||||
} from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
|
||||
import {
|
||||
getRuntimeSessionContext,
|
||||
markRuntimeSessionAuthExpired,
|
||||
type RuntimeSessionContext,
|
||||
} from "./runtime/sessionContext.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -78,6 +83,14 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isRuntimeAuthExpired(context)) {
|
||||
markAuthExpired(context, "access_token_expired");
|
||||
res.status(401).json({
|
||||
message: "access token expired; refresh chat context",
|
||||
detail: sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const command = typeof req.body?.command === "string" ? req.body.command.trim() : "";
|
||||
if (!command) {
|
||||
@@ -252,9 +265,20 @@ app.post("/internal/tools/session-search", async (req, res) => {
|
||||
|
||||
const callBackendJson = async (
|
||||
path: string,
|
||||
accessToken: string | undefined,
|
||||
context: RuntimeSessionContext,
|
||||
payload: unknown,
|
||||
) => {
|
||||
if (isRuntimeAuthExpired(context)) {
|
||||
markAuthExpired(context, "access_token_expired");
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: JSON.stringify({
|
||||
message: "access token expired; refresh chat context",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
|
||||
try {
|
||||
@@ -262,8 +286,8 @@ const callBackendJson = async (
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
if (context.accessToken) {
|
||||
headers.Authorization = `Bearer ${context.accessToken}`;
|
||||
}
|
||||
const response = await fetch(new URL(path, config.TJWATER_API_BASE_URL), {
|
||||
method: "POST",
|
||||
@@ -272,6 +296,9 @@ const callBackendJson = async (
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (response.status === 401) {
|
||||
markAuthExpired(context, "access_token_rejected");
|
||||
}
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
@@ -287,6 +314,32 @@ const parseStringArray = (value: unknown) =>
|
||||
? value.filter((item): item is string => typeof item === "string")
|
||||
: undefined;
|
||||
|
||||
const AUTH_EXPIRY_SKEW_MS = 30_000;
|
||||
|
||||
function isRuntimeAuthExpired(context: RuntimeSessionContext) {
|
||||
if (!context.tokenExpiresAt) {
|
||||
return false;
|
||||
}
|
||||
const expiresAt = Date.parse(context.tokenExpiresAt);
|
||||
if (!Number.isFinite(expiresAt)) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() >= expiresAt - AUTH_EXPIRY_SKEW_MS;
|
||||
}
|
||||
|
||||
function markAuthExpired(
|
||||
context: RuntimeSessionContext,
|
||||
reason: NonNullable<RuntimeSessionContext["authExpired"]>["reason"],
|
||||
) {
|
||||
markRuntimeSessionAuthExpired(context.sessionId, reason);
|
||||
void opencodeRuntime.abortSession(context.sessionId).catch((error) => {
|
||||
logger.warn(
|
||||
{ err: error, sessionId: context.sessionId },
|
||||
"failed to abort runtime after auth expired",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
app.post("/internal/tools/web-search", async (req, res) => {
|
||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||
res.status(403).json({ message: "forbidden" });
|
||||
@@ -328,7 +381,7 @@ app.post("/internal/tools/web-search", async (req, res) => {
|
||||
try {
|
||||
const response = await callBackendJson(
|
||||
"/api/v1/web-search",
|
||||
context.accessToken,
|
||||
context,
|
||||
payload,
|
||||
);
|
||||
res
|
||||
@@ -371,7 +424,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
|
||||
try {
|
||||
const response = await callBackendJson(
|
||||
"/api/v1/tianditu/geocode",
|
||||
context.accessToken,
|
||||
context,
|
||||
{ keyword },
|
||||
);
|
||||
res
|
||||
@@ -389,6 +442,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
|
||||
|
||||
app.use(
|
||||
"/api/v1/agent/chat",
|
||||
requireAgentAuth,
|
||||
buildChatRouter(
|
||||
sessionBridge,
|
||||
opencodeRuntime,
|
||||
|
||||
Reference in New Issue
Block a user