Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1

# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
This commit is contained in:
cliffhall 2026-07-03 17:39:07 -04:00
commit cfbdfdf2bd
4 changed files with 15921 additions and 1202 deletions

View file

@ -0,0 +1,30 @@
import { describe, it, expect, vi } from "vitest";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import userEvent from "@testing-library/user-event";
import { ReAuthBanner } from "./ReAuthBanner";
describe("ReAuthBanner", () => {
it("renders message and action buttons", async () => {
const user = userEvent.setup();
const onReauthenticate = vi.fn();
const onDismiss = vi.fn();
renderWithMantine(
<ReAuthBanner
message="Sign in again to continue."
onReauthenticate={onReauthenticate}
onDismiss={onDismiss}
/>,
);
expect(screen.getByText("Re-authentication required")).toBeInTheDocument();
expect(screen.getByText("Sign in again to continue.")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Re-authenticate" }));
expect(onReauthenticate).toHaveBeenCalledOnce();
const [, closeButton] = screen.getAllByRole("button");
await user.click(closeButton);
expect(onDismiss).toHaveBeenCalledOnce();
});
});

View file

@ -0,0 +1,32 @@
import { Alert, Button, Group, Text } from "@mantine/core";
export interface ReAuthBannerProps {
message: string;
onReauthenticate: () => void;
onDismiss: () => void;
}
export function ReAuthBanner({
message,
onReauthenticate,
onDismiss,
}: ReAuthBannerProps) {
return (
<Alert
color="red"
variant="reauth"
title="Re-authentication required"
withCloseButton
onClose={onDismiss}
>
<Group justify="space-between" align="center" wrap="nowrap" gap="md">
<Text component="span" size="sm">
{message}
</Text>
<Button size="xs" variant="filled" onClick={onReauthenticate}>
Re-authenticate
</Button>
</Group>
</Alert>
);
}

View file

@ -0,0 +1,120 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { AuthChallenge } from "@inspector/core/auth/challenge.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { StepUpAuthModal } from "./StepUpAuthModal";
const stepUpChallenge: AuthChallenge = {
reason: "insufficient_scope",
requiredScopes: ["weather:read"],
authorizationScopes: ["mcp", "tools:read", "weather:read"],
context: { toolName: "get_temp" },
};
describe("StepUpAuthModal", () => {
it("does not render when closed", () => {
renderWithMantine(
<StepUpAuthModal
opened={false}
challenge={stepUpChallenge}
onAuthorize={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(
screen.queryByText(/Additional permissions required/i),
).not.toBeInTheDocument();
});
it("lists additional scopes from the challenge, not the SEP-2350 union", () => {
renderWithMantine(
<StepUpAuthModal
opened
challenge={stepUpChallenge}
authorizationScopes={stepUpChallenge.authorizationScopes}
onAuthorize={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(
screen.getByText(/Additional permissions required/i),
).toBeInTheDocument();
expect(screen.getByText(/get_temp/)).toBeInTheDocument();
expect(screen.getByText("weather:read")).toBeInTheDocument();
expect(screen.queryByText("tools:read")).not.toBeInTheDocument();
expect(screen.queryByText("mcp")).not.toBeInTheDocument();
expect(
screen.getByText(
/redirected to authorize, then returned to the inspector/i,
),
).toBeInTheDocument();
});
it("uses EMA copy when enterpriseManaged is true", () => {
renderWithMantine(
<StepUpAuthModal
opened
enterpriseManaged
challenge={stepUpChallenge}
onAuthorize={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(
screen.getByText(/Additional organization permissions required/i),
).toBeInTheDocument();
expect(
screen.getByText(/enterprise identity provider/i),
).toBeInTheDocument();
expect(
screen.queryByText(
/redirected to authorize, then returned to the inspector/i,
),
).not.toBeInTheDocument();
});
it("falls back to requiredScopes when authorizationScopes is empty", () => {
renderWithMantine(
<StepUpAuthModal
opened
challenge={{
reason: "insufficient_scope",
requiredScopes: ["admin:write"],
}}
onAuthorize={vi.fn()}
onCancel={vi.fn()}
/>,
);
expect(screen.getByText("admin:write")).toBeInTheDocument();
});
it("calls onAuthorize when Authorize is clicked", async () => {
const user = userEvent.setup();
const onAuthorize = vi.fn();
renderWithMantine(
<StepUpAuthModal
opened
challenge={stepUpChallenge}
onAuthorize={onAuthorize}
onCancel={vi.fn()}
/>,
);
await user.click(screen.getByRole("button", { name: /^Authorize$/ }));
expect(onAuthorize).toHaveBeenCalledOnce();
});
it("calls onCancel when Cancel is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderWithMantine(
<StepUpAuthModal
opened
challenge={stepUpChallenge}
onAuthorize={vi.fn()}
onCancel={onCancel}
/>,
);
await user.click(screen.getByRole("button", { name: /^Cancel$/ }));
expect(onCancel).toHaveBeenCalledOnce();
});
});

View file

@ -0,0 +1,88 @@
import { Button, Group, List, Modal, Stack, Text } from "@mantine/core";
import type { AuthChallenge } from "@inspector/core/auth/challenge.js";
import {
stepUpAdditionalScopes,
stepUpConfirmMessage,
stepUpFollowUpMessage,
stepUpModalTitle,
} from "@inspector/core/auth/oauthUx.js";
export interface StepUpAuthModalProps {
opened: boolean;
challenge: AuthChallenge | null;
/** Effective consent scope set (SEP-2350 union). */
authorizationScopes?: string[];
/** Enterprise-managed (EMA) server — organization/IdP copy instead of resource AS. */
enterpriseManaged?: boolean;
onAuthorize: () => void | Promise<void>;
onCancel: () => void;
}
const Actions = Group.withProps({ justify: "flex-end", gap: "sm", mt: "md" });
export function StepUpAuthModal({
opened,
challenge,
authorizationScopes,
enterpriseManaged,
onAuthorize,
onCancel,
}: StepUpAuthModalProps) {
const additionalScopes = challenge ? stepUpAdditionalScopes(challenge) : [];
const ema = enterpriseManaged === true;
const handleAuthorize = () => void onAuthorize();
return (
<Modal
opened={opened}
onClose={onCancel}
size="md"
centered
title={stepUpModalTitle({ enterpriseManaged: ema })}
>
<Stack gap="md">
<Text size="sm">
{challenge
? stepUpConfirmMessage(challenge, { enterpriseManaged: ema })
: stepUpConfirmMessage(
{ reason: "insufficient_scope" },
{ enterpriseManaged: ema },
)}{" "}
{stepUpFollowUpMessage({ enterpriseManaged: ema })}
</Text>
{additionalScopes.length > 0 ? (
<Stack gap="xs">
<Text size="sm" fw={600}>
{ema
? "Additional permissions needed"
: "Additional scopes needed"}
</Text>
<List size="sm" spacing="xs">
{additionalScopes.map((scope) => (
<List.Item key={scope}>
<Text component="span" ff="monospace" size="sm">
{scope}
</Text>
</List.Item>
))}
</List>
{!ema &&
authorizationScopes &&
authorizationScopes.length > additionalScopes.length ? (
<Text size="xs" c="dimmed">
The authorization server may also show scopes you already
granted during sign-in.
</Text>
) : null}
</Stack>
) : null}
<Actions>
<Button variant="default" onClick={onCancel}>
Cancel
</Button>
<Button onClick={handleAuthorize}>Authorize</Button>
</Actions>
</Stack>
</Modal>
);
}