feat(auth): AgentID sign-in button on the web login page (#1467)

## What?

Adds a "Continue with AgentID" button to the web app's login page, matching the existing Google/GitHub buttons (same `ExternalAuthButton` pattern, PostHog `login_attempt` capture, last-used badge).

- `packages/lib/auth.ts`: adds the `genericOAuthClient` plugin — generic OAuth providers sign in via `signIn.oauth2({ providerId })`, not `signIn.social`.
- `apps/web/app/(auth)/login/page.tsx`: the button, gated the same way as the other social buttons — always shown on cloud (`NEXT_PUBLIC_HOST_ID === "supermemory"`), opt-in elsewhere via `NEXT_PUBLIC_AGENTID_AUTH_ENABLED` (added to `.env.example`).

## Why?

Companion to supermemoryai/mono#2908, which registers an `agentid` generic OAuth provider (OIDC against auth.agentid.com) on the API so agents can authenticate with their AgentID identity. The consumer app talks to the same better-auth server, so it gets the same sign-in option. mono#2916 additionally auto-invites the agent's verified human owner to the agent's workspace.

Requires mono#2908 to be deployed for the button to work; until then the API rejects the unknown provider and the page shows its normal error state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches authentication entry points and OAuth client configuration; risk is moderate because it extends login surface area but follows existing social sign-in patterns and is feature-flagged.
>
> **Overview**
> Adds **Continue with AgentID** on the web login page, using the same `ExternalAuthButton` flow as Google/GitHub (PostHog `login_attempt`, last-used badge, loading/error handling).
>
> The button calls **`signIn.oauth2({ providerId: "agentid" })`** instead of `signIn.social`, enabled by registering **`genericOAuthClient`** on the shared better-auth client in `packages/lib/auth.ts`.
>
> Visibility matches other social providers: shown on cloud when `NEXT_PUBLIC_HOST_ID === "supermemory"`, or elsewhere when **`NEXT_PUBLIC_AGENTID_AUTH_ENABLED`** is set (documented in `.env.example`). Depends on the API registering the `agentid` generic OAuth provider.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 90a32786a3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Dhravya 2026-08-16 23:20:38 +00:00
parent d14b209f7c
commit 5d2b5855fe
No known key found for this signature in database
GPG key ID: 135A27003CF4F6CB
3 changed files with 77 additions and 1 deletions

View file

@ -1,4 +1,5 @@
NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai
NEXT_PUBLIC_POSTHOG_KEY=
EXA_API_KEY=
XAI_API_KEY=
XAI_API_KEY=
NEXT_PUBLIC_AGENTID_AUTH_ENABLED=

View file

@ -591,6 +591,79 @@ export default function LoginPage() {
/>
</div>
) : null}
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
process.env.NEXT_PUBLIC_AGENTID_AUTH_ENABLED ? (
<div className="w-full">
<LastUsedBadge show={lastUsedMethod === "agentid"} />
<ExternalAuthButton
authIcon={
<svg
className="size-4 sm:size-5 text-foreground"
fill="none"
height="25"
viewBox="0 0 24 25"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<title>AgentID</title>
<rect
height="11"
rx="2.5"
stroke="currentColor"
strokeWidth="1.8"
width="14"
x="5"
y="8.21"
/>
<path
d="M12 8.21V4.71M12 4.71a1.5 1.5 0 1 0-.01-3 1.5 1.5 0 0 0 .01 3Z"
stroke="currentColor"
strokeWidth="1.8"
/>
<circle
cx="9.25"
cy="13.21"
fill="currentColor"
r="1.25"
/>
<circle
cx="14.75"
cy="13.21"
fill="currentColor"
r="1.25"
/>
<path
d="M9 16.21h6"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="1.8"
/>
</svg>
}
authProvider="AgentID"
className="w-full"
disabled={Boolean(loadingMessage)}
onClick={() => {
if (loadingMessage) return
setIsLoading(true)
posthog.capture("login_attempt", {
method: "social",
provider: "agentid",
})
setPendingLoginMethod("agentid")
signIn
.oauth2({
callbackURL: getCallbackURL(),
providerId: "agentid",
})
.catch((err: unknown) => {
setError(getErrorMessage(err))
setIsLoading(false)
})
}}
/>
</div>
) : null}
</div>
<TextSeparator

View file

@ -3,6 +3,7 @@ import {
anonymousClient,
apiKeyClient,
emailOTPClient,
genericOAuthClient,
magicLinkClient,
organizationClient,
usernameClient,
@ -19,6 +20,7 @@ export const authClient = createAuthClient({
usernameClient(),
magicLinkClient(),
emailOTPClient(),
genericOAuthClient(),
apiKeyClient(),
adminClient(),
organizationClient(),