diff --git a/mcp/src/pages/connections-page.tsx b/mcp/src/pages/connections-page.tsx index 64ee55269c..626702cb25 100644 --- a/mcp/src/pages/connections-page.tsx +++ b/mcp/src/pages/connections-page.tsx @@ -2,8 +2,9 @@ import { isValidElement } from "hono/jsx"; /** * `/connections` — where a user sees which AI apps hold a grant to their Artcraft account and - * disconnects them. The app itself has no sessions screen, so this page is the only place that - * can show "Claude is connected". A pure function of its view model; everything is escaped. + * disconnects them, and where they mint personal tokens for integrations that cannot run the + * OAuth flow themselves. The app itself has no sessions screen, so this page is the only place + * that can show "Claude is connected". A pure function of its view model; everything is escaped. */ export interface ConnectionView { @@ -13,6 +14,21 @@ export interface ConnectionView { readonly createdAt: string; } +export interface PersonalTokenView { + readonly id: string; + readonly label: string; + readonly hint: string; + readonly createdAt: string; + readonly expiresAt: string; +} + +/** Shown exactly once, in the response to the request that created it. */ +export interface NewTokenView { + readonly label: string; + readonly secret: string; + readonly expiresAt: string; +} + export interface ConnectionsView { readonly csrfToken: string; readonly scriptNonce: string; @@ -24,6 +40,8 @@ export interface ConnectionsView { readonly signedIn?: { readonly username: string; readonly connections: readonly ConnectionView[]; + readonly personalTokens: readonly PersonalTokenView[]; + readonly newToken?: NewTokenView; }; } @@ -34,6 +52,9 @@ export const CONNECTIONS_FORM_FIELDS = { password: "password", googleCredential: "google_credential", grantId: "grant_id", + tokenLabel: "token_label", + tokenLifetime: "token_lifetime", + tokenId: "token_id", } as const; export const CONNECTIONS_PATHS = { @@ -41,8 +62,14 @@ export const CONNECTIONS_PATHS = { signIn: "/connections/sign-in", signOut: "/connections/sign-out", revoke: "/connections/revoke", + createToken: "/connections/tokens/create", + revokeToken: "/connections/tokens/revoke", } as const; +/** The lifetimes a user can pick, in days; the form value is the key. */ +export const TOKEN_LIFETIME_DAYS = ["30", "90"] as const; +export type TokenLifetimeDays = (typeof TOKEN_LIFETIME_DAYS)[number]; + export function renderConnectionsPage(view: ConnectionsView): string { const element: unknown = ; if (!isValidElement(element)) throw new Error("connections page did not render to a JSX node"); @@ -77,7 +104,7 @@ function ConnectionsPage({ view }: { view: ConnectionsView }) { )} - {view.googleClientId && !view.signedIn ? ( + {view.googleClientId ? ( <> @@ -98,25 +125,9 @@ function SignIn({ view }: { view: ConnectionsView }) {

- - - {view.googleClientId ? ( -
-
- -

or sign in with your password

-
- ) : null} + + + {view.googleClientId ? : null}