mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Fix agent-auth review issues and docs build
- Grants no longer expire at the session TTL (expiresAt: null); agent lifetime clocks govern access, so long-lived agents keep their grants. - list_users rejects an over-max limit with constraint_violated instead of silently clamping (fail loud); default limit stays bounded by max. - Discovery route auth schema uses adaptSchema for type-correct build. - Add missing agent-auth entry to docs APP_ICONS Record<AppId> map, which was breaking @hexclave/docs build (and build/lint/docker CI). Co-Authored-By: madison@stack-auth.com <madison.w.kennedy@gmail.com>
This commit is contained in:
parent
5e6e1165c3
commit
e26f326d8c
@ -1,5 +1,5 @@
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { jsonSchema, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
|
||||
import { adaptSchema, jsonSchema, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
|
||||
import { StatusError } from "@hexclave/shared/dist/utils/errors";
|
||||
import { AGENT_AUTH_AGENTS_PATH, AGENT_AUTH_CAPABILITY_EXECUTE_PATH, AGENT_AUTH_DISCOVERY_PATH, AGENT_AUTH_MODES, AGENT_AUTH_PROTOCOL_VERSION, AGENT_AUTH_REGISTER_PATH } from "@/lib/agent-auth/constants";
|
||||
import { AGENT_CAPABILITIES } from "@/lib/agent-auth/capabilities";
|
||||
@ -12,9 +12,9 @@ function buildDiscoveryUrl(baseUrl: string, path: string) {
|
||||
export const GET = createSmartRouteHandler({
|
||||
request: yupObject({
|
||||
auth: yupObject({
|
||||
type: yupString(),
|
||||
user: jsonSchema,
|
||||
project: jsonSchema,
|
||||
type: adaptSchema,
|
||||
user: adaptSchema,
|
||||
project: adaptSchema,
|
||||
}).nullable(),
|
||||
}),
|
||||
response: yupObject({
|
||||
|
||||
@ -42,8 +42,8 @@ export const GET = createSmartRouteHandler({
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200 as const,
|
||||
bodyType: "json" as const,
|
||||
statusCode: 200,
|
||||
bodyType: "json",
|
||||
body: {
|
||||
agent: serializeAgent(agent),
|
||||
},
|
||||
|
||||
@ -134,7 +134,7 @@ export const POST = createSmartRouteHandler({
|
||||
status: "ACTIVE",
|
||||
constraints: capability.constraints == null ? undefined : JSON.parse(JSON.stringify(capability.constraints)),
|
||||
grantedByProjectUserId: existingUser.projectUserId,
|
||||
expiresAt: sessionExpiresAt,
|
||||
expiresAt: null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@ -159,8 +159,8 @@ export const POST = createSmartRouteHandler({
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 201 as const,
|
||||
bodyType: "json" as const,
|
||||
statusCode: 201,
|
||||
bodyType: "json",
|
||||
body: {
|
||||
host_id: host.id,
|
||||
agent_id: agent.id,
|
||||
|
||||
@ -32,8 +32,8 @@ export const GET = createSmartRouteHandler({
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200 as const,
|
||||
bodyType: "json" as const,
|
||||
statusCode: 200,
|
||||
bodyType: "json",
|
||||
body: {
|
||||
agents: agents.map(serializeAgent),
|
||||
},
|
||||
|
||||
@ -104,9 +104,10 @@ function assertConstraintSatisfied(field: string, value: unknown, constraint: Ag
|
||||
}
|
||||
|
||||
function applyConstraintsForListUsers(input: { limit?: number }, constraints: AgentCapabilityGrantConstraints | null | undefined) {
|
||||
const requestedLimit = input.limit;
|
||||
if (constraints == null || Object.keys(constraints).length === 0) {
|
||||
return {
|
||||
limit: input.limit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT,
|
||||
limit: requestedLimit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
@ -117,10 +118,25 @@ function applyConstraintsForListUsers(input: { limit?: number }, constraints: Ag
|
||||
}
|
||||
|
||||
const limitConstraint = constraints.limit;
|
||||
if (limitConstraint == null) return input;
|
||||
const resolvedLimit = clampNumber(input.limit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT, limitConstraint.min, limitConstraint.max);
|
||||
assertConstraintSatisfied("limit", resolvedLimit, limitConstraint);
|
||||
return { limit: resolvedLimit };
|
||||
if (limitConstraint == null) {
|
||||
return {
|
||||
limit: requestedLimit ?? AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
if (requestedLimit != null) {
|
||||
assertConstraintSatisfied("limit", requestedLimit, limitConstraint);
|
||||
return { limit: requestedLimit };
|
||||
}
|
||||
|
||||
if (limitConstraint.min != null && limitConstraint.max != null && limitConstraint.min > limitConstraint.max) {
|
||||
throwConstraintViolation("constraint_violated");
|
||||
}
|
||||
|
||||
const defaultLimit = AGENT_AUTH_DEFAULT_LIST_USERS_LIMIT;
|
||||
const boundedLimit = clampNumber(defaultLimit, limitConstraint.min, limitConstraint.max);
|
||||
assertConstraintSatisfied("limit", boundedLimit, limitConstraint);
|
||||
return { limit: boundedLimit };
|
||||
}
|
||||
|
||||
export function validateAgentCapabilityInput<TInput extends Record<string, unknown>>(
|
||||
|
||||
@ -124,6 +124,22 @@ describe("agent auth", () => {
|
||||
expect(listUsersResponse.status).toBe(200);
|
||||
expect(listUsersResponse.body.result.users).toHaveLength(1);
|
||||
|
||||
const invalidListUsersResponse = await niceBackendFetch("/api/latest/agent-auth/capabilities/execute", {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${agentJwt}`,
|
||||
},
|
||||
body: {
|
||||
capability: "list_users",
|
||||
input: {
|
||||
limit: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(invalidListUsersResponse.status).toBe(400);
|
||||
expect(invalidListUsersResponse.body).toBe("constraint_violated");
|
||||
|
||||
const projectInfoResponse = await niceBackendFetch("/api/latest/agent-auth/capabilities/execute", {
|
||||
accessType: "server",
|
||||
method: "POST",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { ALL_APPS, AppId } from "@hexclave/shared/dist/apps/apps-config";
|
||||
import { AppIcon, appSquarePaddingExpression, appSquareWidthExpression } from "@hexclave/shared/dist/apps/apps-ui";
|
||||
import { BarChart3, ClipboardList, Code, CreditCard, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import { BarChart3, ClipboardList, Code, CreditCard, Fingerprint, Headset, KeyRound, Mail, Mails, MousePointerClick, PlayCircle, Rocket, ShieldCheck, ShieldEllipsis, Sparkles, Triangle, Tv, UserCog, Users, Vault, Webhook } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
@ -16,6 +16,7 @@ const APP_URL_OVERRIDES: Partial<Record<AppId, string>> = {
|
||||
// Icon mapping for docs (no Next.js Image dependencies)
|
||||
const APP_ICONS: Record<AppId, React.FunctionComponent<React.SVGProps<SVGSVGElement>>> = {
|
||||
authentication: ShieldEllipsis,
|
||||
"agent-auth": Fingerprint,
|
||||
teams: Users,
|
||||
rbac: UserCog,
|
||||
"api-keys": KeyRound,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user