Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/website/app/api/agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ function readSourceFiles(
}

export async function POST(req: Request) {
const authHeader = req.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return Response.json(
{ error: "Unauthorized" },
{ status: 401 },
);
}
Comment on lines +69 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd examples/website && find . -name "route.ts" -type f | head -5

Repository: Recoupable-com/Recoup-Bash

Length of output: 146


🏁 Script executed:

cat -n examples/website/app/api/agent/route.ts | head -100

Repository: Recoupable-com/Recoup-Bash

Length of output: 4183


🌐 Web query:

Privy @privy-io/server-auth verifyAuthToken API

💡 Result:

In @privy-io/server-auth, you verify a Privy access token (JWT) with:

const verifiedClaims = await privy.utils().auth().verifyAuthToken(authToken);
  • Input: authToken: string (the user’s access token, typically from Authorization: Bearer <token>). [1]
  • Returns (on success): an object with:
    • appId, userId (user’s Privy DID), issuer (always privy.io), issuedAt, expiration, sessionId. [1]
  • Throws (on failure): if the token is expired/invalid or for a different app. [1]
  • By default it fetches your app’s verification key from Privy; you can avoid that call by initializing the client with jwtVerificationKey copied from the Privy Dashboard. [1]

Notes:

  • @privy-io/server-auth is deprecated; Privy recommends @privy-io/node for new work, but the token-verification flow is the same conceptually. [2]

Sources: Privy token verification guide (includes verifyAuthToken example + return fields) [1]; Privy @privy-io/server-auth deprecation note [2].


Bearer token is never verified server-side — any arbitrary string bypasses this check.

The route only checks that an Authorization header starting with "Bearer " exists, but never validates the token itself. Any caller can send Authorization: Bearer fake and access the endpoint. Privy's @privy-io/server-auth provides verifyAuthToken(token) to validate the token server-side and extract claims; without it, this auth gate provides no real protection.

🤖 Prompt for AI Agents
In `@examples/website/app/api/agent/route.ts` around lines 69 - 75, The current
auth gate only checks authHeader startsWith "Bearer " but never validates the
token; update the route handler to import and call verifyAuthToken (from
`@privy-io/server-auth`), extract the raw token by stripping the "Bearer " prefix
from authHeader, await verifyAuthToken(token), and on any verification error or
falsy result return Response.json({ error: "Unauthorized" }, { status: 401 });
otherwise continue request handling and use the returned claims as needed;
ensure the handler is async if it isn’t already and handle exceptions from
verifyAuthToken to return 401.


const { messages } = await req.json();
const lastUserMessage = messages
.filter((m: { role: string }) => m.role === "user")
Expand Down
10 changes: 7 additions & 3 deletions examples/website/app/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ function getTheme(isDark: boolean) {
};
}

export default function TerminalComponent() {
export default function TerminalComponent({
getAccessToken,
}: {
getAccessToken: () => Promise<string | null>;
}) {
const terminalRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -47,7 +51,7 @@ export default function TerminalComponent() {

// Create commands
const { aboutCmd, installCmd, githubCmd } = createStaticCommands();
const agentCmd = createAgentCommand(term);
const agentCmd = createAgentCommand(term, getAccessToken);

// Files from DOM
const files = {
Expand Down Expand Up @@ -110,7 +114,7 @@ export default function TerminalComponent() {
colorSchemeQuery.removeEventListener("change", onColorSchemeChange);
term.dispose();
};
}, []);
}, [getAccessToken]);

return (
<div
Expand Down
20 changes: 18 additions & 2 deletions examples/website/app/components/terminal-parts/agent-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ function formatForTerminal(text: string): string {
return text.replace(/\t/g, " ").replace(/\r?\n/g, "\r\n");
}

export function createAgentCommand(term: TerminalWriter) {
export function createAgentCommand(
term: TerminalWriter,
getAccessToken: () => Promise<string | null>,
) {
const agentMessages: UIMessage[] = [];
let messageIdCounter = 0;

Expand Down Expand Up @@ -49,9 +52,22 @@ export function createAgentCommand(term: TerminalWriter) {
});

try {
const token = await getAccessToken();
if (!token) {
agentMessages.pop();
return {
stdout: "",
stderr: "Error: Not authenticated. Please log in and try again.\n",
exitCode: 1,
};
}

const response = await fetch("/api/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ messages: agentMessages }),
});

Expand Down
3 changes: 2 additions & 1 deletion examples/website/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { GeistMono } from "geist/font/mono";
import { Analytics } from "@vercel/analytics/next"
import Providers from "./providers";
import "./globals.css";

export const metadata: Metadata = {
Expand Down Expand Up @@ -34,7 +35,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`${GeistMono.variable} antialiased`}>
{children}
<Providers>{children}</Providers>
<Analytics/>
</body>
</html>
Expand Down
47 changes: 46 additions & 1 deletion examples/website/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { usePrivy } from "@privy-io/react-auth";
import TerminalComponent from "./components/Terminal";
import { TerminalData } from "./components/TerminalData";

Expand Down Expand Up @@ -94,18 +95,62 @@ const NOSCRIPT_CONTENT = `

export default function Home() {
const [mounted, setMounted] = useState(false);
const { ready, authenticated, login, getAccessToken } = usePrivy();

useEffect(() => {
setMounted(true);
}, []);

if (!mounted || !ready) {
return (
<>
<noscript>
<pre>{NOSCRIPT_CONTENT}</pre>
</noscript>
<TerminalData />
</>
);
}

if (!authenticated) {
return (
<>
<TerminalData />
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
fontFamily: "var(--font-geist-mono), monospace",
}}
>
<button
onClick={login}
style={{
background: "none",
border: "1px solid currentColor",
color: "inherit",
padding: "12px 24px",
fontSize: "16px",
fontFamily: "inherit",
cursor: "pointer",
}}
>
Log in to continue
</button>
</div>
</>
);
}

return (
<>
<noscript>
<pre>{NOSCRIPT_CONTENT}</pre>
</noscript>
<TerminalData />
{mounted ? <TerminalComponent /> : null}
<TerminalComponent getAccessToken={getAccessToken} />
<a href="https://vercel.com" target="_blank" hidden id="credits">
Created by Vercel Labs
</a>
Expand Down
15 changes: 15 additions & 0 deletions examples/website/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";
import { PrivyProvider } from "@privy-io/react-auth";

export default function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
config={{
loginMethods: ["email"],
}}
>
{children}
</PrivyProvider>
);
Comment on lines +4 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing env var will silently pass undefined as appId.

The non-null assertion (!) only silences TypeScript; if NEXT_PUBLIC_PRIVY_APP_ID is unset at runtime, PrivyProvider receives undefined. Consider adding a guard or throwing a descriptive error early.

Proposed fix
+const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
+if (!PRIVY_APP_ID) {
+  throw new Error("NEXT_PUBLIC_PRIVY_APP_ID environment variable is required");
+}
+
 export default function Providers({ children }: { children: React.ReactNode }) {
   return (
-    <PrivyProvider appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}>
+    <PrivyProvider appId={PRIVY_APP_ID}>
       {children}
     </PrivyProvider>
   );
 }
🤖 Prompt for AI Agents
In `@examples/website/app/providers.tsx` around lines 4 - 9, Providers currently
passes process.env.NEXT_PUBLIC_PRIVY_APP_ID! into PrivyProvider which uses a
TypeScript non-null assertion but can still be undefined at runtime; update
Providers to read process.env.NEXT_PUBLIC_PRIVY_APP_ID into a local const,
validate it (if falsy) and throw a clear Error (or return an error UI) before
rendering PrivyProvider so PrivyProvider never receives undefined, and remove
the non-null assertion usage; reference Providers, PrivyProvider, and
process.env.NEXT_PUBLIC_PRIVY_APP_ID when making the change.

}
1 change: 1 addition & 0 deletions examples/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"lint": "eslint"
},
"dependencies": {
"@privy-io/react-auth": "^3.13.1",
"@vercel/analytics": "^1.6.1",
"@vercel/sandbox": "^1.4.1",
"ai": "^6.0.66",
Expand Down
Loading