[Feat] Sign in in VSC (#1926)

https://github.com/user-attachments/assets/54f34242-9152-4efb-ad4f-5edb2013f205



https://github.com/user-attachments/assets/3d4f7d53-4f73-4700-bb69-6238a08189c6

While testing the build, use the common package and install it in
cf-webapp, then change the base_url to localhost:3000.

---------

Co-authored-by: Kevin Turcios <106575910+KRRT7@users.noreply.github.com>
Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
Co-authored-by: ali <mohammed18200118@gmail.com>
Co-authored-by: mohammed ahmed <64513301+mohammedahmed18@users.noreply.github.com>
This commit is contained in:
HeshamHM28 2025-10-31 10:14:13 -07:00 committed by GitHub
parent a53c472d94
commit ab012f58b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1892 additions and 214 deletions

View file

@ -1,6 +1,5 @@
import { useEffect } from "react";
import { useStore } from "./store/root";
import ApiKeyForm from "./components/apiKeyError";
import { messageHandler } from "./utils/webviewMessageHandler";
// import OptimizeCurrentDiff from "./components/optimizeCurrentDiff";
// import CurrentFileFunctions from "./components/currentFileFunctions";
@ -8,6 +7,7 @@ import { vscode } from "./utils/vscode";
import ChatView from "./components/chatView";
import Tabs from "./components/tabs";
import OptimizationQueue from "./components/optimizationQueue";
import SignInForm from "./components/signInForm";
function App() {
const store = useStore();
@ -25,7 +25,7 @@ function App() {
}, []);
if (store.status == "apiKeyError") {
return <ApiKeyForm />;
return <SignInForm />;
}
return (

View file

@ -1,74 +0,0 @@
import { useEffect, useRef } from "react";
import { vscode } from "../utils/vscode";
import type { WebviewMessage } from "@codeflash/types";
import { useStore } from "../store/root";
const ApiKeyForm = () => {
const inputRef = useRef<HTMLInputElement>(null);
const loading = useStore((state) => state.isValidatingApiKey);
useEffect(() => {
const onApiKeyInvalid = (message: MessageEvent) => {
const sidebarMessage = message.data as WebviewMessage;
if (sidebarMessage.type == "apiKeyEnterInvalid") {
const current = inputRef.current;
if (!current) {
return;
}
current.value = "";
current.focus();
}
};
window.addEventListener("message", onApiKeyInvalid);
return () => {
window.removeEventListener("message", onApiKeyInvalid);
};
}, []);
useEffect(() => {
const current = inputRef.current;
if (!current) {
return;
}
current.focus();
}, []);
return (
<div className="api-key-container">
<h2>Codeflash API Key</h2>
<div className="input-group">
<input
type="text"
id="api-key"
placeholder="cf-******"
ref={inputRef}
onKeyDown={(e) => {
if (e.key == "Enter") {
const apiKey = inputRef.current?.value || "";
if (!apiKey?.trim()) {
return;
}
vscode.postMessage({
type: "apiKeyEnter",
payload: {
apiKey: apiKey.trim(),
},
});
}
}}
disabled={loading}
/>
{loading && <span className="codicon codicon-loading spin"></span>}
</div>
<p className="hint">
You can generate an API key from your{" "}
<a href="https://app.codeflash.ai/apikeys" target="_blank">
Codeflash account
</a>
.
</p>
</div>
);
};
export default ApiKeyForm;

View file

@ -0,0 +1,415 @@
import { useEffect, useRef, useState } from "react";
import { vscode } from "../utils/vscode";
import type { WebviewMessage } from "@codeflash/types";
import { useStore } from "../store/root";
import CodeflashLogo from "./logo";
const SignInForm = () => {
const inputRef = useRef<HTMLInputElement>(null);
const loading = useStore((state) => state.isValidatingApiKey);
const [activeTab, setActiveTab] = useState<"apiKey" | "signIn">("signIn");
const [isAuthInProgress, setIsAuthInProgress] = useState(false);
useEffect(() => {
const handleMessage = (message: MessageEvent) => {
const sidebarMessage = message.data as WebviewMessage;
switch (sidebarMessage.type) {
case "apiKeyEnterInvalid":
const current = inputRef.current;
if (current) {
current.value = "";
current.focus();
}
break;
case "authStarted":
setIsAuthInProgress(true);
break;
case "authCompleted":
case "authFailed":
case "authCancelled":
setIsAuthInProgress(false);
break;
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);
useEffect(() => {
if (activeTab === "apiKey") {
const current = inputRef.current;
if (current) {
current.focus();
}
}
}, [activeTab]);
const handleApiKeySubmit = () => {
const apiKey = inputRef.current?.value || "";
if (!apiKey?.trim()) {
return;
}
vscode.postMessage({
type: "apiKeyEnter",
payload: {
apiKey: apiKey.trim(),
},
});
};
const handleSignIn = () => {
vscode.postMessage({
type: "signIn",
});
};
const isDisabled = loading || isAuthInProgress;
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
padding: "20px",
overflow: "hidden",
background: "var(--vscode-editor-background)",
}}
>
{/* Gradient glow effect from top to bottom */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "100%",
background: "linear-gradient(to bottom, #FFC043, transparent 60%)",
opacity: 0.2,
pointerEvents: "none",
filter: "blur(60px)",
}}
/>
{/* Grid Background */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"linear-gradient(to right, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px), linear-gradient(to bottom, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px)",
backgroundSize: "24px 24px",
opacity: 0.03,
}}
/>
{/* Main Container */}
<div
style={{
position: "relative",
zIndex: 10,
width: "100%",
maxWidth: "380px",
textAlign: "center",
}}
>
{/* Logo */}
<div
style={{
fontSize: "32px",
fontWeight: "600",
marginBottom: "60px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
}}
>
<CodeflashLogo width={"200"} height={"200"} />
</div>
{/* Auth in Progress State */}
{isAuthInProgress ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
padding: "24px",
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "8px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "12px",
}}
>
<span
className="codicon codicon-loading spin"
style={{
fontSize: "20px",
color: "var(--vscode-foreground)",
}}
/>
<span
style={{
fontSize: "14px",
color: "var(--vscode-foreground)",
}}
>
Waiting for authentication...
</span>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "8px 0",
lineHeight: "1.5",
}}
>
Complete the authentication in your browser.
</p>
</div>
) : (
/* Buttons Container */
<div
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
marginBottom: "24px",
}}
>
{activeTab === "signIn" ? (
<>
<button
onClick={handleSignIn}
disabled={isDisabled}
style={{
position: "relative",
overflow: "hidden",
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
}}
onMouseEnter={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-hoverBackground)";
}}
onMouseLeave={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-background)";
}}
>
<div className="glowEffect modePulse"></div>
<span style={{ position: "relative", zIndex: 1 }}>
{loading ? "Signing in..." : "Sign in with CodeFlash"}
</span>
</button>
<button
onClick={() => setActiveTab("apiKey")}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "var(--vscode-button-secondaryBackground)",
color: "var(--vscode-button-secondaryForeground)",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
}}
onMouseEnter={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-secondaryHoverBackground)";
}}
onMouseLeave={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-secondaryBackground)";
}}
>
Use API Key
</button>
</>
) : (
<>
<div style={{ position: "relative", marginBottom: "8px" }}>
<input
type="text"
id="api-key"
placeholder="Enter your API key (cf-******)"
ref={inputRef}
style={{
width: "100%",
padding: "12px 36px 12px 14px",
fontSize: "14px",
background: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "6px",
color: "var(--vscode-input-foreground)",
outline: "none",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.5 : 1,
cursor: isDisabled ? "not-allowed" : "text",
transition: "border-color 0.2s",
textAlign: "left",
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleApiKeySubmit();
}
}}
onFocus={(e) => {
e.target.style.border =
"1px solid var(--vscode-focusBorder)";
}}
onBlur={(e) => {
e.target.style.border =
"1px solid var(--vscode-input-border)";
}}
disabled={isDisabled}
/>
{loading && (
<span
className="codicon codicon-loading spin"
style={{
position: "absolute",
right: "14px",
top: "50%",
transform: "translateY(-50%)",
color: "var(--vscode-foreground)",
fontSize: "16px",
}}
></span>
)}
</div>
<button
onClick={handleApiKeySubmit}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
}}
onMouseEnter={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-hoverBackground)";
}}
onMouseLeave={(e) => {
if (!isDisabled)
e.currentTarget.style.background =
"var(--vscode-button-background)";
}}
>
{loading ? "Saving..." : "Save"}
</button>
<button
onClick={() => setActiveTab("signIn")}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "transparent",
color: "var(--vscode-button-secondaryForeground)",
border: "none",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "opacity 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.5 : 0.7,
}}
onMouseEnter={(e) => {
if (!isDisabled) e.currentTarget.style.opacity = "1";
}}
onMouseLeave={(e) => {
if (!isDisabled) e.currentTarget.style.opacity = "0.7";
}}
>
Back
</button>
</>
)}
</div>
)}
{/* Footer Text */}
{activeTab === "apiKey" && !isAuthInProgress && (
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginTop: "16px",
}}
>
<a
href="https://app.codeflash.ai/apikeys"
target="_blank"
rel="noopener noreferrer"
style={{
color: "var(--vscode-textLink-foreground)",
textDecoration: "none",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = "underline";
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = "none";
}}
>
Get your API key
</a>
</p>
)}
</div>
</div>
);
};
export default SignInForm;

View file

@ -26,6 +26,11 @@ export type MessageType =
| "setActiveSidebarTab"
| "setApiKeyLoadingState"
| "cancelTask"
| "signIn"
| "authStarted"
| "authCompleted"
| "authFailed"
| "authCancelled"
| "submitInitForm";
export type QueueTaskItemStatus =
@ -75,7 +80,24 @@ export interface ShowMessageMessage extends WebviewMessage {
message: string;
};
}
export interface AuthStartedMessage {
type: "authStarted";
}
export interface AuthCompletedMessage {
type: "authCompleted";
}
export interface AuthCancelledMessage {
type: "authCancelled";
}
export interface AuthFailedMessage {
type: "authFailed";
payload: {
error: string;
};
}
export interface SubmitInitFormMessage extends WebviewMessage {
type: "submitInitForm";
payload: {
@ -209,6 +231,9 @@ export interface ApiKeyEnterMessage extends WebviewMessage {
apiKey: string;
};
}
export interface SignInMessage extends WebviewMessage {
type: "signIn";
}
export interface ViewDiffMessage extends WebviewMessage {
type: "viewDiff";
@ -315,6 +340,7 @@ export type IncomingWebviewMessage =
| OptimizeCurrentDiffMessage
| FilesInWorkspace
| RequestFunctionsMessage
| SignInMessage
| ChangeTaskFocusMessage
| SubmitInitFormMessage;
@ -329,6 +355,10 @@ export type OutgoingWebviewMessage =
| RecievedFunctionsMessage
| ChangeTaskFocusFromBackendMessage
| SetActiveSidebarTabMessage
| AuthStartedMessage
| AuthCompletedMessage
| AuthCancelledMessage
| AuthFailedMessage
| SetApiKeyLoadingStateMessage;
export type Suggestion = {

View file

@ -197,7 +197,6 @@ export class BootCodeflashServerStep extends BaseStep {
sidebarProvider.sendMessage(message);
},
);
this._disposables.push(
optimizeFunctionCommand,
optimizeAllCommand,

View file

@ -8,6 +8,7 @@ import { GlobalState, GlobalStateKey } from "./globalState";
import { InitWebviewProvider } from "./providers/InitWebviewProvider";
import { InitService } from "./services/initService";
import { LogsEventEmitter } from "./utils/logsEventEmitter";
import { CodeflashAuthProvider } from "./providers/CodeflashAuthProvider";
import { SentryLogger } from "./telemetry/sentry";
import { Telemetry } from "./telemetry/posthog";
import { getExtensionVersion } from "./utils/vscode";
@ -96,6 +97,8 @@ export async function activate(
// );
}
const optimizationEventEmitter = new LogsEventEmitter(globalState); // for emitting events from the lsp server logs to the sidebar, for tracking the running optimization progress
const codeflashAuth = new CodeflashAuthProvider(context);
addDisposable(context, codeflashAuth);
const showGlobalStateCommand = vscode.commands.registerCommand(
"codeflash.dev.showGlobalState",
async () => {

View file

@ -0,0 +1,12 @@
export const AUTH_TYPE = `auth0`;
export const AUTH_NAME = `Codeflash`;
export const BASE_URL = "https://app.codeflash.ai/";
export const AUTH_TIMEOUT = 5 * 60 * 1000; // 5 minutes
export const ERRORS = {
TIMED_OUT: "Authentication request timed out",
USER_CANCELLED: "User cancelled authentication",
AUTH_FAILED: "Authentication failed",
NETWORK_ERROR: "Network error during authentication",
} as const;

View file

@ -0,0 +1,472 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Logger } from "../utils";
import { Disposable } from "../utils/dispose";
import * as vscode from "vscode";
import crypto from "crypto";
import {
AUTH_NAME,
AUTH_TIMEOUT,
AUTH_TYPE,
BASE_URL,
ERRORS,
} from "./AuthConfig";
// interface SessionData {
// id: string;
// accessToken: string;
// account: {
// id: string;
// label: string;
// };
// scopes: readonly string[];
// }
/**
* Dedicated URI handler for OAuth callbacks
*/
export class AuthUriHandler
extends vscode.EventEmitter<vscode.Uri>
implements vscode.UriHandler
{
private readonly _pendingStates = new Set<string>();
private _codeExchangePromise:
| Promise<{ code: string; state: string }>
| undefined;
public handleUri(uri: vscode.Uri): void {
this.fire(uri);
}
public async waitForCallback(
expectedState: string,
cancellationToken?: vscode.CancellationToken,
): Promise<{ code: string; state: string }> {
this._pendingStates.add(expectedState);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(ERRORS.TIMED_OUT)), AUTH_TIMEOUT),
);
try {
const callbackPromise =
this._codeExchangePromise ||
new Promise<{ code: string; state: string }>((resolve, reject) => {
const disposable = this.event((uri) => {
try {
const result = this.handleCallback(uri);
if (result) {
disposable.dispose();
resolve(result);
}
} catch (error) {
disposable.dispose();
reject(error);
}
});
});
this._codeExchangePromise = callbackPromise;
const cancellationPromise = cancellationToken
? new Promise<never>((_, reject) => {
cancellationToken.onCancellationRequested(() => {
reject(new Error(ERRORS.USER_CANCELLED));
});
})
: new Promise<never>(() => {});
return await Promise.race([
callbackPromise,
timeoutPromise,
cancellationPromise,
]);
} finally {
this._pendingStates.delete(expectedState);
this._codeExchangePromise = undefined;
}
}
private handleCallback(
uri: vscode.Uri,
): { code: string; state: string } | null {
if (uri.path !== "/callback") {
return null;
}
const params = new URLSearchParams(uri.query);
const code = params.get("code");
const state = params.get("state");
if (!code || !state) {
throw new Error(ERRORS.AUTH_FAILED);
}
if (!this._pendingStates.has(state)) {
return null;
}
return { code, state };
}
}
export class CodeflashAuthProvider
extends Disposable
implements vscode.AuthenticationProvider
{
private readonly logger = new Logger("CodeflashAuthProvider");
private readonly _sessionChangeEmitter =
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
private readonly _uriHandler: AuthUriHandler;
// private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
constructor(private readonly context: vscode.ExtensionContext) {
super();
this._uriHandler = new AuthUriHandler();
//this._sessionsPromise = this.readSessions();
this._disposables.push(
vscode.authentication.registerAuthenticationProvider(
AUTH_TYPE,
AUTH_NAME,
this,
{ supportsMultipleAccounts: false },
),
vscode.window.registerUriHandler(this._uriHandler),
this._sessionChangeEmitter,
// this.context.secrets.onDidChange(() => this.checkForUpdates()),
);
}
get onDidChangeSessions() {
return this._sessionChangeEmitter.event;
}
/**
* Get the existing sessions
*/
public async getSessions(
_scopes?: readonly string[],
_options?: vscode.AuthenticationProviderSessionOptions,
): Promise<vscode.AuthenticationSession[]> {
// this.logger.info(
// `Getting sessions for ${scopes?.length ? scopes.join(",") : "all scopes"}...`,
// );
// const sessions = await this._sessionsPromise;
// const filteredSessions = scopes?.length
// ? sessions.filter((session) => this.scopesMatch(session.scopes, scopes))
// : sessions;
// this.logger.info(`Got ${filteredSessions.length} session(s)`);
// return filteredSessions;
return Promise.resolve([]);
}
/**
* Create a new auth session
*/
public async createSession(
scopes: readonly string[],
): Promise<vscode.AuthenticationSession> {
try {
this.logger.info("Starting authentication flow");
const { code, codeVerifier, redirectUri } =
await this.initiateLoginWithProgress();
// Exchange authorization code for access token
const apiKey = await this.exchangeCodeForToken(
code,
codeVerifier,
redirectUri,
);
// Create session
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: apiKey,
account: { id: apiKey, label: apiKey },
scopes,
};
// // Update stored sessions
// await this.addSession(session);
this.logger.info("Authentication successful");
return session;
} catch (error) {
this.handleAuthError(error);
throw error;
}
}
/**
* Remove an existing session
*/
public async removeSession(_sessionId: string): Promise<void> {}
// ============================================
// Private Helper Methods
// ============================================
private async initiateLoginWithProgress(): Promise<{
code: string;
state: string;
codeVerifier: string;
redirectUri: string;
}> {
return await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: vscode.l10n.t("Signing in to {0}...", "CodeFlash"),
cancellable: true,
},
async (_progress, token) => {
return await this.initiateLogin(token);
},
);
}
private async initiateLogin(
cancellationToken?: vscode.CancellationToken,
): Promise<{
code: string;
state: string;
codeVerifier: string;
redirectUri: string;
}> {
const publisher = this.context.extension.packageJSON.publisher;
const name = this.context.extension.packageJSON.name;
const redirectUri = `${vscode.env.uriScheme}://${publisher}.${name}/callback`;
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
const state = crypto.randomUUID();
await this.context.secrets.store("auth-code-verifier", codeVerifier);
await this.context.secrets.store("auth-state", state);
const clientId = "cf_vscode_app";
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
code_challenge: codeChallenge,
code_challenge_method: "sha256",
state,
originator: `codeflash_${vscode.env.appName}`,
});
const codeflashAuthUrl = `${BASE_URL}codeflash/auth?${params.toString()}`;
const opened = await vscode.env.openExternal(
vscode.Uri.parse(codeflashAuthUrl),
);
if (!opened) {
throw new Error("Failed to open browser for authentication");
}
this.logger.info(
"Browser opened for authentication, waiting for callback...",
);
try {
const result = await this._uriHandler.waitForCallback(
state,
cancellationToken,
);
const savedState = await this.context.secrets.get("auth-state");
if (result.state !== savedState) {
throw new Error("State validation failed");
}
return {
code: result.code,
state: result.state,
codeVerifier,
redirectUri,
};
} catch (error) {
// Clean up stored secrets on error
await this.context.secrets.delete("auth-code-verifier");
await this.context.secrets.delete("auth-state");
throw error;
}
}
private async exchangeCodeForToken(
code: string,
codeVerifier: string,
redirectUri: string,
): Promise<string> {
try {
const response = await fetch(`${BASE_URL}/codeflash/auth/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
client_id: "cf_vscode_app",
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("No access token in response");
}
return data.access_token;
} catch (error) {
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new Error(ERRORS.NETWORK_ERROR);
}
throw error;
}
}
// private async readSessions(): Promise<vscode.AuthenticationSession[]> {
// try {
// this.logger.info("Reading sessions from storage...");
// const stored = await this.context.secrets.get("codeflash-sessions");
// if (!stored) {
// this.logger.info("No stored sessions found");
// return [];
// }
// const sessions = JSON.parse(stored) as SessionData[];
// this.logger.info(`Loaded ${sessions.length} session(s)`);
// return sessions.map(this.sessionDataToSession);
// } catch (error) {
// this.logger.error("Failed to read sessions: " + String(error));
// return [];
// }
// }
// private async storeSessions(
// sessions: vscode.AuthenticationSession[],
// ): Promise<void> {
// this.logger.info(`Storing ${sessions.length} session(s)...`);
// this._sessionsPromise = Promise.resolve(sessions);
// const sessionData: SessionData[] = sessions.map((s) => ({
// id: s.id,
// accessToken: s.accessToken,
// account: s.account,
// scopes: s.scopes,
// }));
// await this.context.secrets.store(
// "codeflash-sessions",
// JSON.stringify(sessionData),
// );
// this.logger.info("Sessions stored successfully");
// }
// private async addSession(
// session: vscode.AuthenticationSession,
// ): Promise<void> {
// const sessions = await this._sessionsPromise;
// // Remove any existing session (single account mode)
// const removed = sessions.splice(0, sessions.length);
// sessions.push(session);
// await this.storeSessions(sessions);
// this._sessionChangeEmitter.fire({
// added: [session],
// removed,
// changed: [],
// });
// }
// private async checkForUpdates(): Promise<void> {
// const previousSessions = await this._sessionsPromise;
// this._sessionsPromise = this.readSessions();
// const storedSessions = await this._sessionsPromise;
// const added: vscode.AuthenticationSession[] = [];
// const removed: vscode.AuthenticationSession[] = [];
// storedSessions.forEach((session) => {
// if (!previousSessions.some((s) => s.id === session.id)) {
// this.logger.info("Adding session found in storage");
// added.push(session);
// }
// });
// previousSessions.forEach((session) => {
// if (!storedSessions.some((s) => s.id === session.id)) {
// this.logger.info("Removing session no longer in storage");
// removed.push(session);
// }
// });
// if (added.length || removed.length) {
// this._sessionChangeEmitter.fire({ added, removed, changed: [] });
// }
// }
// private sessionDataToSession(
// data: SessionData,
// ): vscode.AuthenticationSession {
// return {
// id: data.id,
// accessToken: data.accessToken,
// account: data.account,
// scopes: data.scopes,
// };
// }
private generateSessionId(): string {
return crypto
.getRandomValues(new Uint32Array(2))
.reduce((prev, curr) => prev + curr.toString(16), "");
}
private handleAuthError(error: unknown): void {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === ERRORS.USER_CANCELLED) {
vscode.window.showInformationMessage("Authentication cancelled.");
this.logger.info("User cancelled authentication");
return;
}
if (errorMessage === ERRORS.TIMED_OUT) {
vscode.window.showErrorMessage(
"Authentication request timed out. Please try again.",
);
this.logger.error("Authentication timed out");
return;
}
if (errorMessage === ERRORS.NETWORK_ERROR) {
vscode.window.showErrorMessage(
"Unable to connect to CodeFlash. Please check your internet connection.",
);
this.logger.error("Network error during authentication");
return;
}
vscode.window.showErrorMessage(
vscode.l10n.t("Sign in failed: {0}", errorMessage),
);
this.logger.error("Authentication failed: " + errorMessage);
}
}

View file

@ -38,6 +38,7 @@ import { GlobalStateKey } from "../globalState";
import { Disposable } from "../utils/dispose";
import { readFileSync, unlinkSync } from "fs";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";
import { AUTH_TYPE, ERRORS } from "./AuthConfig";
import { Telemetry } from "../telemetry/posthog";
export class SidebarProvider
@ -71,7 +72,6 @@ export class SidebarProvider
public resolveWebviewView(
webviewView: vscode.WebviewView,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_context: vscode.WebviewViewResolveContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -186,7 +186,7 @@ export class SidebarProvider
): Promise<void> => {
if (
!this.globalState.get(GlobalStateKey.UserID) &&
!["apiKeyEnter", "webviewReady"].includes(data.type) &&
!["apiKeyEnter", "webviewReady", "signIn"].includes(data.type) &&
this.initializedOnce
) {
this._logger.info(
@ -257,9 +257,70 @@ export class SidebarProvider
vscode.window.showWarningMessage(message);
}
break;
case "signIn":
await this.handleSignIn();
break;
}
};
private async handleSignIn(): Promise<void> {
this.sendMessage({
type: "authStarted",
});
this._logger.info("Starting OAuth authentication flow");
try {
const session = await vscode.authentication.getSession(AUTH_TYPE, [], {
createIfNone: true,
});
if (session?.accessToken) {
await this.handleApiKeyEnter(session.accessToken);
this.sendMessage({
type: "authCompleted",
});
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (
errorMessage.includes("User cancelled") ||
errorMessage.includes("Cancelled") ||
errorMessage === ERRORS.USER_CANCELLED
) {
this._logger.info("OAuth authentication cancelled by user");
this.sendMessage({
type: "authCancelled",
});
vscode.window.showInformationMessage("Authentication cancelled.");
} else if (
errorMessage.includes("timed out") ||
errorMessage === ERRORS.TIMED_OUT
) {
this._logger.error("OAuth authentication timed out");
this.sendMessage({
type: "authFailed",
payload: { error: "Authentication timed out" },
});
vscode.window.showErrorMessage(
"Authentication timed out. Please try again.",
);
} else {
this._logger.error(`OAuth authentication failed: ${errorMessage}`);
this.sendMessage({
type: "authFailed",
payload: { error: errorMessage },
});
vscode.window.showErrorMessage(
`Authentication failed: ${errorMessage}`,
);
}
}
}
async handleViewPatch(payload: ViewPatchMessage["payload"]): Promise<void> {
const { id, patchFile } = payload;
if (this.openedPathches.has(patchFile)) {
@ -539,6 +600,7 @@ export class SidebarProvider
},
});
}
addFunctionToQueue(
functionName: string,
uri?: vscode.Uri | string,
@ -991,33 +1053,6 @@ export class SidebarProvider
this.sendMessage(updateMessage);
}
// Commenting out badge update for now, since we dont show the optimizable functions list
// private updateViewBadge(): void {
// if (!this._view) {
// return;
// }
// try {
// // Set badge on the individual view (this shows on the activity bar icon)
// if (this._currentFunctionCount > 0) {
// this._view.badge = {
// tooltip: `${this._currentFunctionCount} optimizable function${this._currentFunctionCount === 1 ? "" : "s"} found`,
// value: this._currentFunctionCount,
// };
// } else {
// this._view.badge = undefined;
// }
// this._logger.debug(
// `Updated activity bar badge: ${this._currentFunctionCount} functions`,
// );
// } catch (error) {
// this._logger.warn(
// `Failed to update activity bar badge ${error instanceof Error ? error.message : String(error)}`,
// );
// }
// }
public async refreshAnalysis(): Promise<void> {
this._logger.debug("Manual analysis refresh requested");
if (this._view?.visible) {

View file

@ -13,7 +13,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "^1.0.21",
"@codeflash-ai/common": "^1.0.22",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/core": "^7.0.2",
@ -1077,9 +1077,9 @@
"license": "ISC"
},
"node_modules/@codeflash-ai/common": {
"version": "1.0.21",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.21/463cb9c4c096d7f91c691dea4408ed1019ad100e",
"integrity": "sha512-DevPmVW8WaIbsQ9Drz0MlmDrrHjDQjce4t/vBI29G/ax+Uo2+SQkz5pqyi+pOFVbyXJQmDKV8Xe4WQ49FX6Bag==",
"version": "1.0.22",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.22/26f248d8c6ced1d2b0bfbc9382db9313e2f1dc7d",
"integrity": "sha512-DgKkA+T1Uu/bnK+r2x7xMnJQE2HoyL/uDROuNSRIjnzdj0mZoAA0t5CRUYESwPlu+UBNLM+5St6spy5v/cqriA==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",

View file

@ -28,7 +28,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "^1.0.21",
"@codeflash-ai/common": "^1.0.22",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/core": "^7.0.2",

View file

@ -10,7 +10,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.3.0",
"@azure/msal-node": "^3.7.3",
"@codeflash-ai/common": "^1.0.21",
"@codeflash-ai/common": "^1.0.22",
"@hookform/resolvers": "^3.3.2",
"@monaco-editor/react": "^4.7.0",
"@prisma/client": "^6.7.0",
@ -37,10 +37,12 @@
"diff": "^8.0.2",
"framer-motion": "^12.12.1",
"github-markdown-css": "^5.4.0",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.381.0",
"marked": "^16.1.1",
"next": "^14.2.32",
"next-themes": "^0.3.0",
"node-ts-cache": "^4.4.0",
"node-ts-cache-storage-memory": "^4.4.0",
"pg": "^8.11.3",
"postcss": "^8",
@ -62,6 +64,7 @@
},
"devDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-react": "^4.3.1",
@ -704,9 +707,9 @@
}
},
"node_modules/@codeflash-ai/common": {
"version": "1.0.21",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.21/463cb9c4c096d7f91c691dea4408ed1019ad100e",
"integrity": "sha512-DevPmVW8WaIbsQ9Drz0MlmDrrHjDQjce4t/vBI29G/ax+Uo2+SQkz5pqyi+pOFVbyXJQmDKV8Xe4WQ49FX6Bag==",
"version": "1.0.22",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.22/26f248d8c6ced1d2b0bfbc9382db9313e2f1dc7d",
"integrity": "sha512-DgKkA+T1Uu/bnK+r2x7xMnJQE2HoyL/uDROuNSRIjnzdj0mZoAA0t5CRUYESwPlu+UBNLM+5St6spy5v/cqriA==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",
@ -1348,9 +1351,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@ -3159,9 +3162,9 @@
}
},
"node_modules/@prisma/client": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz",
"integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz",
"integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -3181,66 +3184,66 @@
}
},
"node_modules/@prisma/config": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz",
"integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz",
"integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
"effect": "3.16.12",
"effect": "3.18.4",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz",
"integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz",
"integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
"integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/fetch-engine": "6.17.1",
"@prisma/get-platform": "6.17.1"
"@prisma/debug": "6.18.0",
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"@prisma/fetch-engine": "6.18.0",
"@prisma/get-platform": "6.18.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz",
"integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==",
"version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz",
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz",
"integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
"integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.1",
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
"@prisma/get-platform": "6.17.1"
"@prisma/debug": "6.18.0",
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
"@prisma/get-platform": "6.18.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz",
"integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz",
"integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.17.1"
"@prisma/debug": "6.18.0"
}
},
"node_modules/@prisma/instrumentation": {
@ -4577,9 +4580,9 @@
}
},
"node_modules/@sentry/cli": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.56.1.tgz",
"integrity": "sha512-VDAIg+gmjNtJS5VUZQMDSK9RaKC9hYQi3PoXpNa+owNfQNk60bCi8z8jkbWRcKbNGn3V51WqvrQAqLoNAdPc9w==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.57.0.tgz",
"integrity": "sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@ -4596,20 +4599,20 @@
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.56.1",
"@sentry/cli-linux-arm": "2.56.1",
"@sentry/cli-linux-arm64": "2.56.1",
"@sentry/cli-linux-i686": "2.56.1",
"@sentry/cli-linux-x64": "2.56.1",
"@sentry/cli-win32-arm64": "2.56.1",
"@sentry/cli-win32-i686": "2.56.1",
"@sentry/cli-win32-x64": "2.56.1"
"@sentry/cli-darwin": "2.57.0",
"@sentry/cli-linux-arm": "2.57.0",
"@sentry/cli-linux-arm64": "2.57.0",
"@sentry/cli-linux-i686": "2.57.0",
"@sentry/cli-linux-x64": "2.57.0",
"@sentry/cli-win32-arm64": "2.57.0",
"@sentry/cli-win32-i686": "2.57.0",
"@sentry/cli-win32-x64": "2.57.0"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.56.1.tgz",
"integrity": "sha512-zfhT8MrvB5x/xRdIVGwg+sG0Cx3i0G6RH2zCrdQ/moWn8TfkwsM0O1k/AxpwbpcRfAHCkVb04CU/yKciKwg2KA==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.57.0.tgz",
"integrity": "sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==",
"license": "BSD-3-Clause",
"optional": true,
"os": [
@ -4620,9 +4623,9 @@
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.56.1.tgz",
"integrity": "sha512-fNB/Ng11HrkGOSEIDg+fc3zfTCV7q6kJddp6ndK3QlYFsCffRSnclaX1SMp+mqxdWkHqe1kkp85OY8G/x5uAWw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.57.0.tgz",
"integrity": "sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==",
"cpu": [
"arm"
],
@ -4638,9 +4641,9 @@
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.56.1.tgz",
"integrity": "sha512-AypXIwZvOMJb9RgjI/98hTAd06FcOjqjIm6G9IR0OI4pJCOcaAXz9NKXdJqxpZd7phSMJnD+Bx/8iYOUPeY73A==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.57.0.tgz",
"integrity": "sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==",
"cpu": [
"arm64"
],
@ -4656,9 +4659,9 @@
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.56.1.tgz",
"integrity": "sha512-vnH+WJEsUq7Lf7xc9udzE/M4hoDXXsniFFYr/7BvdnXtCQlNNaWFMXHbEDYAql3baIlHkWoG8cEHWuB/YKyniw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.57.0.tgz",
"integrity": "sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==",
"cpu": [
"x86",
"ia32"
@ -4675,9 +4678,9 @@
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.56.1.tgz",
"integrity": "sha512-3/BlKe5Vdnia36MeovghHJD8lbcum5TFIxLp+PSfH2sVb09+5Jo0L95oRTI2JkD8Fs+QNssvTqTxJj5eIo/n+A==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.57.0.tgz",
"integrity": "sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==",
"cpu": [
"x64"
],
@ -4693,9 +4696,9 @@
}
},
"node_modules/@sentry/cli-win32-arm64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.56.1.tgz",
"integrity": "sha512-Gg8RV7CV7Tz4fiR1EN1Af5AVhJsnEXiZvfvfQXI4lp51MKAhcxZIMtEfg9HaWsn3Dm/wgwYBinyeywfWbTXYDg==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.57.0.tgz",
"integrity": "sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==",
"cpu": [
"arm64"
],
@ -4709,9 +4712,9 @@
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.56.1.tgz",
"integrity": "sha512-6u6a060yC3i76Ze1apqgWr5luQSyhuD5ND84eWfh/UbddsEa42UHjoVHOiBwmpZqf/hvNZAtzLnE4NCvU4zOMg==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.57.0.tgz",
"integrity": "sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==",
"cpu": [
"x86",
"ia32"
@ -4726,9 +4729,9 @@
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.56.1",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.56.1.tgz",
"integrity": "sha512-11cdflajBrDWlRZqI9MOu7ok2vnPzFjKmbU3YvBYWQapNE+HHAsWdsRL/u/P1RmU62vj7Y42iSUcj6x1SNrdPw==",
"version": "2.57.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.57.0.tgz",
"integrity": "sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==",
"cpu": [
"x64"
],
@ -5317,6 +5320,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -6841,9 +6855,9 @@
}
},
"node_modules/browserslist": {
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
"funding": [
{
"type": "opencollective",
@ -6860,11 +6874,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
"electron-to-chromium": "^1.5.238",
"node-releases": "^2.0.26",
"update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
@ -7818,9 +7832,9 @@
}
},
"node_modules/effect": {
"version": "3.16.12",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
"version": "3.18.4",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@ -13406,15 +13420,15 @@
"peer": true
},
"node_modules/prisma": {
"version": "6.17.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz",
"integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==",
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz",
"integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.17.1",
"@prisma/engines": "6.17.1"
"@prisma/config": "6.18.0",
"@prisma/engines": "6.18.0"
},
"bin": {
"prisma": "build/index.js"
@ -16003,9 +16017,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [
{
"type": "opencollective",

View file

@ -20,7 +20,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.3.0",
"@azure/msal-node": "^3.7.3",
"@codeflash-ai/common": "^1.0.21",
"@codeflash-ai/common": "^1.0.22",
"@hookform/resolvers": "^3.3.2",
"@monaco-editor/react": "^4.7.0",
"@prisma/client": "^6.7.0",
@ -47,10 +47,12 @@
"diff": "^8.0.2",
"framer-motion": "^12.12.1",
"github-markdown-css": "^5.4.0",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.381.0",
"marked": "^16.1.1",
"next": "^14.2.32",
"next-themes": "^0.3.0",
"node-ts-cache": "^4.4.0",
"node-ts-cache-storage-memory": "^4.4.0",
"pg": "^8.11.3",
"postcss": "^8",
@ -72,6 +74,7 @@
},
"devDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-react": "^4.3.1",

View file

@ -0,0 +1,266 @@
"use server"
import { generateTokenForVsCode } from "@/app/(dashboard)/apikeys/tokenfuncs"
import { getUserId } from "@/app/utils/auth"
import crypto from "crypto"
import jwt from "jsonwebtoken"
import { CacheContainer } from "node-ts-cache"
import { MemoryStorage } from "node-ts-cache-storage-memory"
const RATE_LIMIT = 5
const RATE_LIMIT_WINDOW_MS = 60 * 1000
const rateLimitCache = new CacheContainer(new MemoryStorage())
// TODO:: Find a way to save it in Session
const JWT_SECRET = process.env.JWT_SECRET!
if (!JWT_SECRET) {
throw new Error("JWT_SECRET is not defined in environment variables")
}
interface OAuthStatePayload {
userId: string
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
type: "oauth_state"
}
interface AuthCodePayload {
userId: string
codeChallenge: string
codeChallengeMethod: string
redirectUri: string
clientId: string
type: "auth_code"
}
export async function isRateLimited(userId: string): Promise<boolean> {
const cacheKey = `rate_limit_vsc_signin_${userId}`
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
const now = Date.now()
if (!record || now - record.startTime > RATE_LIMIT_WINDOW_MS) {
await rateLimitCache.setItem(
cacheKey,
{ count: 1, startTime: now },
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 },
)
console.log(`Rate limit initialized for user: ${userId}`)
return false
}
if (record.count >= RATE_LIMIT) {
console.warn(`Rate limit exceeded for user: ${userId}, count: ${record.count}`)
return true
}
record.count++
await rateLimitCache.setItem(cacheKey, record, {
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
})
console.log(`Rate limit check passed for user: ${userId}, count: ${record.count}`)
return false
}
export async function createOAuthState(params: {
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
}): Promise<{ state: string; error?: string }> {
console.log("=== Creating OAuth State (JWT) ===")
console.log("Params:", {
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge.substring(0, 10) + "...",
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
})
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { state: "", error: "Unauthorized" }
}
console.log("User ID:", userId)
const limited = await isRateLimited(userId)
if (limited) {
console.error("Rate limit exceeded for user:", userId)
return { state: "", error: "Rate limit exceeded" }
}
const statePayload: OAuthStatePayload = {
userId,
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge,
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
type: "oauth_state",
}
const state = jwt.sign(statePayload, JWT_SECRET, {
expiresIn: "2m",
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("OAuth state JWT created successfully")
return { state }
} catch (error) {
console.error("Error creating OAuth state:", error)
return { state: "", error: "Internal server error" }
}
}
export async function authorizeOAuth(state: string): Promise<{
code?: string
redirectUri?: string
error?: string
}> {
console.log("=== Authorizing OAuth (JWT) ===")
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { error: "Unauthorized" }
}
console.log("User ID:", userId)
let oauthState: OAuthStatePayload
try {
oauthState = jwt.verify(state, JWT_SECRET) as OAuthStatePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired state" }
}
if (oauthState.type !== "oauth_state") {
console.error("Invalid token type:", oauthState.type)
return { error: "Invalid state token" }
}
console.log("OAuth state JWT verified successfully")
if (oauthState.userId !== userId) {
console.error("User mismatch:", { expected: oauthState.userId, actual: userId })
return { error: "User mismatch" }
}
const authCodePayload: AuthCodePayload = {
userId,
codeChallenge: oauthState.codeChallenge,
codeChallengeMethod: oauthState.codeChallengeMethod,
redirectUri: oauthState.redirectUri,
clientId: oauthState.clientId,
type: "auth_code",
}
const code = jwt.sign(authCodePayload, JWT_SECRET, {
expiresIn: "2m",
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("Authorization code JWT created successfully")
return {
code,
redirectUri: oauthState.redirectUri,
}
} catch (error) {
console.error("Error authorizing OAuth:", error)
return { error: "Internal server error" }
}
}
interface TokenExchangeParams {
code: string
codeVerifier: string
redirectUri: string
clientId: string
}
export async function exchangeCodeForToken(
params: TokenExchangeParams,
): Promise<{ accessToken?: string; error?: string }> {
console.log("=== Exchanging Code for Token (JWT) ===")
console.log("Params:", {
codeVerifier: params.codeVerifier.substring(0, 10) + "...",
redirectUri: params.redirectUri,
clientId: params.clientId,
})
try {
let codeData: AuthCodePayload
try {
codeData = jwt.verify(params.code, JWT_SECRET) as AuthCodePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired authorization code" }
}
if (codeData.type !== "auth_code") {
console.error("Invalid token type:", codeData.type)
return { error: "Invalid authorization code" }
}
console.log("✓ Authorization code JWT verified successfully!")
console.log("Code data:", {
userId: codeData.userId,
redirectUri: codeData.redirectUri,
clientId: codeData.clientId,
})
if (codeData.clientId !== params.clientId) {
console.error("Client ID mismatch:", { expected: codeData.clientId, actual: params.clientId })
return { error: "Client ID mismatch" }
}
if (codeData.redirectUri !== params.redirectUri) {
console.error("Redirect URI mismatch:", {
expected: codeData.redirectUri,
actual: params.redirectUri,
})
return { error: "Redirect URI mismatch" }
}
console.log("Computing code challenge...")
const computedChallenge = crypto
.createHash(codeData.codeChallengeMethod)
.update(params.codeVerifier)
.digest("base64url")
if (computedChallenge !== codeData.codeChallenge) {
console.error("Code verifier validation failed")
return { error: "Code verifier validation failed" }
}
console.log("✓ PKCE validation successful")
console.log("Generating API token for userId:", codeData.userId)
try {
const apiKey = await generateTokenForVsCode(codeData.userId)
console.log("API token generated successfully")
console.log("=== Token Exchange Completed Successfully ===")
return { accessToken: apiKey.token }
} catch (tokenError: unknown) {
if (tokenError instanceof Error && tokenError.message === "NEXT_REDIRECT") {
console.error("Caught redirect error during token generation")
return { error: "Authentication required" }
}
console.error("Error generating token:", tokenError)
return { error: "Failed to generate API token" }
}
} catch (error) {
console.error("=== Token Exchange Failed ===")
console.error("Error:", error)
return { error: "Internal server error" }
}
}

View file

@ -0,0 +1,360 @@
"use client"
import LogoBox from "@/components/dashboard/logo-box"
import { useState, useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Loading } from "@/components/ui/loading"
import { authorizeOAuth, createOAuthState } from "./action"
export default function CodeFlashAuthContent() {
const [isLoading, setIsLoading] = useState(false)
const [isCheckingAuth, setIsCheckingAuth] = useState(true)
const [error, setError] = useState<string | null>(null)
const [step, setStep] = useState<"checking" | "ready" | "authorizing" | "waiting">("checking")
const [hasAuthenticated, setHasAuthenticated] = useState(false)
const searchParams = useSearchParams()
const router = useRouter()
useEffect(() => {
// Check if user already authenticated in this session
const authenticated = sessionStorage.getItem("oauth_authenticated")
if (authenticated === "true") {
setHasAuthenticated(true)
setStep("waiting")
setIsCheckingAuth(false)
return
}
const checkAuth = async () => {
setStep("checking")
try {
// Validate OAuth parameters
const responseType = searchParams.get("response_type")
const clientId = searchParams.get("client_id")
const redirectUri = searchParams.get("redirect_uri")
const codeChallenge = searchParams.get("code_challenge")
const codeChallengeMethod = searchParams.get("code_challenge_method")
const state = searchParams.get("state")
if (responseType !== "code") {
setError("Invalid request parameters")
return
}
if (!clientId || clientId !== "cf_vscode_app") {
setError("Invalid client application")
return
}
if (!redirectUri) {
setError("Invalid redirect destination")
return
}
if (!codeChallenge || !codeChallengeMethod) {
setError("Missing security parameters")
return
}
if (!state) {
setError("Missing request identifier")
return
}
// Create OAuth state
const result = await createOAuthState({
redirectUri,
codeChallenge,
codeChallengeMethod,
clientId,
})
if (result.error) {
if (result.error === "Unauthorized") {
const currentPath = window.location.pathname + window.location.search
router.replace(`/login?returnTo=${encodeURIComponent(currentPath)}`)
return
}
setError(
result.error === "Rate limit exceeded"
? "Too many authentication attempts. Please try again later."
: "An error occurred. Please try again.",
)
return
}
// Store the internal state and original VS Code state
sessionStorage.setItem("oauth_internal_state", result.state)
sessionStorage.setItem("oauth_vscode_state", state)
setStep("ready")
} catch (err) {
console.error("Error checking authentication:", err)
setError("An unexpected error occurred. Please try again.")
} finally {
setIsCheckingAuth(false)
}
}
checkAuth()
}, [router, searchParams])
const handleAuthenticate = async () => {
// Prevent multiple authentications
if (hasAuthenticated) {
return
}
setIsLoading(true)
setError(null)
setStep("authorizing")
try {
const internalState = sessionStorage.getItem("oauth_internal_state")
const vscodeState = sessionStorage.getItem("oauth_vscode_state")
if (!internalState || !vscodeState) {
setError("Session expired. Please refresh the page and try again.")
setIsLoading(false)
setStep("ready")
return
}
const result = await authorizeOAuth(internalState)
if (result.error) {
setError(
result.error === "Rate limit exceeded"
? "Too many authentication attempts. Please try again later."
: "Authentication failed. Please try again.",
)
setIsLoading(false)
setStep("ready")
return
}
if (!result.code || !result.redirectUri) {
setError("Authentication failed. Please try again.")
setIsLoading(false)
setStep("ready")
return
}
// Mark as authenticated
sessionStorage.setItem("oauth_authenticated", "true")
setHasAuthenticated(true)
// Clean up OAuth state
sessionStorage.removeItem("oauth_internal_state")
sessionStorage.removeItem("oauth_vscode_state")
// Redirect back to VS Code with code and state
const redirectUrl = new URL(result.redirectUri)
redirectUrl.searchParams.set("code", result.code)
redirectUrl.searchParams.set("state", vscodeState)
setStep("waiting")
setIsLoading(false)
// Redirect immediately
window.location.href = redirectUrl.toString()
} catch (err) {
console.error("Error authorizing:", err)
setError("An error occurred. Please try again.")
setIsLoading(false)
setStep("ready")
}
}
if (isCheckingAuth || step === "checking") {
return <Loading />
}
return (
<div className="min-h-screen bg-gradient-to-b from-primary/10 via-primary/5 to-background relative">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:24px_24px]" />
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative z-10">
<div className="mb-16">
<LogoBox />
</div>
<div className="max-w-md w-full">
<div className="bg-card border border-border rounded-2xl shadow-xl overflow-hidden">
<div className="bg-gradient-to-r from-[#007ACC]/5 to-[#007ACC]/10 px-6 py-4 border-b border-border">
<div className="flex items-center justify-center gap-3">
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 128 128"
>
<defs>
<linearGradient
id="vscodeGradient"
x1="63.922"
x2="63.922"
y1=".33"
y2="127.67"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset="1" stopColor="#fff" stopOpacity="0" />
</linearGradient>
</defs>
<mask
id="vscodeMask"
width="128"
height="128"
x="0"
y="0"
maskUnits="userSpaceOnUse"
style={{ maskType: "alpha" }}
>
<path
fill="#fff"
fillRule="evenodd"
d="M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64L1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68l50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z"
clipRule="evenodd"
/>
</mask>
<g mask="url(#vscodeMask)">
<path
fill="#0065A9"
d="M123.471 13.82L97.097 1.12A7.973 7.973 0 0 0 88 2.668L1.662 81.387a5.333 5.333 0 0 0 .006 7.887l7.052 6.411a5.333 5.333 0 0 0 6.811.303l103.971-78.875c3.488-2.646 8.498-.158 8.498 4.22v-.306a8.001 8.001 0 0 0-4.529-7.208Z"
/>
<path
fill="#007ACC"
d="m123.471 114.181l-26.374 12.698A7.973 7.973 0 0 1 88 125.333L1.662 46.613a5.333 5.333 0 0 1 .006-7.887l7.052-6.411a5.333 5.333 0 0 1 6.811-.303l103.971 78.874c3.488 2.647 8.498.159 8.498-4.219v.306a8.001 8.001 0 0 1-4.529 7.208Z"
/>
<path
fill="#1F9CF0"
d="M97.098 126.882A7.977 7.977 0 0 1 88 125.333c2.952 2.952 8 .861 8-3.314V5.98c0-4.175-5.048-6.266-8-3.313a7.977 7.977 0 0 1 9.098-1.549L123.467 13.8A8 8 0 0 1 128 21.01v85.982a8 8 0 0 1-4.533 7.21l-26.369 12.681Z"
/>
<path
fill="url(#vscodeGradient)"
fillRule="evenodd"
d="M90.69 127.126a7.968 7.968 0 0 0 6.349-.244l26.353-12.681a8 8 0 0 0 4.53-7.21V21.009a8 8 0 0 0-4.53-7.21L97.039 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026l-21.974-16.68a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.336 5.336 0 0 0-.006 7.888L20.718 64L1.662 81.386a5.335 5.335 0 0 0 .006 7.888l7.048 6.411a5.328 5.328 0 0 0 6.807.303l21.975-16.681l50.45 46.026a7.959 7.959 0 0 0 2.742 1.793Zm5.252-92.184L57.662 64l38.28 29.057V34.943Z"
clipRule="evenodd"
opacity=".25"
/>
</g>
</svg>
</div>
<div className="text-left">
<p className="text-sm font-semibold text-foreground">
Visual Studio Code Extension
</p>
</div>
</div>
</div>
{error ? (
<div className="p-8 space-y-6">
<div className="w-20 h-20 bg-amber-500/10 rounded-2xl flex items-center justify-center mx-auto relative">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-amber-600 dark:text-amber-500"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<div className="space-y-3 text-center">
<h2 className="text-2xl font-bold text-foreground">Authentication Error</h2>
<p className="text-sm text-muted-foreground leading-relaxed">{error}</p>
</div>
</div>
) : step === "waiting" || hasAuthenticated ? (
<div className="p-8 space-y-6">
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center mx-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-primary"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
<div className="space-y-2 text-center">
<h2 className="text-xl font-bold text-foreground">Go to VS Code</h2>
</div>
</div>
) : (
<div className="p-8 space-y-6">
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mx-auto">
<svg
className="w-6 h-6 text-primary"
fill="none"
strokeWidth="2"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
</div>
<div className="space-y-2 text-center">
<h1 className="text-2xl font-bold text-foreground">
Authenticate for CodeFlash Extension
</h1>
<p className="text-sm text-muted-foreground leading-relaxed">
CodeFlash requires authentication to associate with the Visual Studio Code
extension on your machine.
</p>
</div>
<button
onClick={handleAuthenticate}
disabled={isLoading}
className="w-full px-6 py-3.5 bg-primary hover:bg-primary/90 active:scale-[0.99] text-primary-foreground font-semibold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100 shadow-sm"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Authenticating...
</span>
) : (
"Authenticate with CodeFlash"
)}
</button>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server"
import { exchangeCodeForToken } from "../../action"
export async function POST(request: NextRequest) {
console.log("=== Token Exchange Request Started ===")
try {
const body = await request.json()
console.log("Request body:", {
grant_type: body.grant_type,
client_id: body.client_id,
redirect_uri: body.redirect_uri,
has_code: !!body.code,
has_code_verifier: !!body.code_verifier,
code_length: body.code?.length,
code_verifier_length: body.code_verifier?.length,
})
const { grant_type, code, redirect_uri, code_verifier, client_id } = body
// Validate grant type
if (grant_type !== "authorization_code") {
console.error("Invalid grant type:", grant_type)
return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 })
}
// Validate required parameters
if (!code || !redirect_uri || !code_verifier || !client_id) {
console.error("Missing required parameters:", {
has_code: !!code,
has_redirect_uri: !!redirect_uri,
has_code_verifier: !!code_verifier,
has_client_id: !!client_id,
})
return NextResponse.json(
{ error: "invalid_request", error_description: "Missing required parameters" },
{ status: 400 },
)
}
console.log("Exchanging code for token...")
const result = await exchangeCodeForToken({
code,
codeVerifier: code_verifier,
redirectUri: redirect_uri,
clientId: client_id,
})
if (result.error) {
console.error("Token exchange failed:", result.error)
return NextResponse.json(
{ error: "invalid_grant", error_description: result.error },
{ status: 400 },
)
}
console.log("Token exchange successful, access_token length:", result.accessToken?.length)
console.log("=== Token Exchange Request Completed Successfully ===")
return NextResponse.json({
access_token: result.accessToken,
token_type: "Bearer",
})
} catch (error) {
console.error("=== Token Exchange Request Failed ===")
console.error("Error type:", error instanceof Error ? error.constructor.name : typeof error)
console.error("Error message:", error instanceof Error ? error.message : String(error))
console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
console.error("Full error object:", error)
return NextResponse.json(
{ error: "server_error", error_description: "Internal server error" },
{ status: 500 },
)
}
}

View file

@ -0,0 +1,11 @@
import { Suspense } from "react"
import { Loading } from "@/components/ui/loading"
import CodeFlashAuthContent from "./content"
export default function CodeFlashAuthPage() {
return (
<Suspense fallback={<Loading />}>
<CodeFlashAuthContent />
</Suspense>
)
}

View file

@ -6,6 +6,7 @@ import { ApiKeyTable } from "./api-key-table"
import { type cf_api_keys, PrismaClient } from "@prisma/client"
import PostHogClient from "@/lib/posthog"
import { ApiKeysClient } from "./api-keys-client"
import { VS_CODE_KEY_NAME } from "@codeflash-ai/common"
const prisma = new PrismaClient()
@ -18,7 +19,7 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
const userId = session.user.sub
console.log("USER ID:", session.user)
const apiKeys: cf_api_keys[] = await prisma.cf_api_keys.findMany({
where: { user_id: userId },
where: { user_id: userId, name: { not: VS_CODE_KEY_NAME } },
})
const posthog = PostHogClient()

View file

@ -1,7 +1,11 @@
"use server"
import { getSession } from "@auth0/nextjs-auth0"
import { redirect } from "next/navigation"
import { deleteAPIKeyById, safeGenAndStoreAPITokenHash } from "@codeflash-ai/common"
import {
deleteAPIKeyById,
genAndStoreAPITokenHashForVSC,
safeGenAndStoreAPITokenHash,
} from "@codeflash-ai/common"
import { TokenLimitExceededError } from "./token-error"
export async function generateToken(
@ -27,7 +31,25 @@ export async function generateToken(
}
}
}
export async function generateTokenForVsCode(userId: string): Promise<{
success: boolean
token: string | undefined
err: string | undefined
}> {
try {
const token: string = await genAndStoreAPITokenHashForVSC(userId)
return { success: true, token, err: undefined }
} catch (error) {
if (error instanceof Error && error.message === "Token limit exceeded") {
return { success: false, err: new TokenLimitExceededError().message, token: undefined }
}
return {
success: false,
err: "Failed to generate API key. Please try again.",
token: undefined,
}
}
}
export async function deleteAPIKey(id: number): Promise<void> {
const user = await getSession()
if (user == null) {

View file

@ -11,7 +11,6 @@ import {
import { type NextRequest, NextResponse } from "next/server"
import { createOrUpdateUser, hasCompletedOnboarding } from "@codeflash-ai/common"
import { trackUserLogin } from "@/lib/analytics/tracking"
import { cookies } from "next/headers"
// THIS IS THE KEY CHANGE - Your afterCallback was empty!
const afterCallback: AfterCallbackAppRoute = async (req: NextRequest, session: Session) => {
@ -71,9 +70,12 @@ const afterCallback: AfterCallbackAppRoute = async (req: NextRequest, session: S
console.warn("[Auth] Failed to parse state:", e)
}
}
// check if the path is codeflash/auth/[token]
const isAuthPath = intendedDestination.startsWith("/codeflash/auth/")
console.log(`[Auth] isAuthPath: ${isAuthPath}`)
// Handle onboarding redirect
if (!completedOnboarding) {
if (!completedOnboarding && !isAuthPath) {
session.returnTo = "/onboarding"
} else {
session.returnTo = intendedDestination

View file

@ -4,7 +4,7 @@ import { usePathname } from "next/navigation"
import { Sidebar } from "./dashboard/sidebar"
import { Breadcrumb } from "./dashboard/bread-crumb"
const HIDDEN_PAGES = ["/onboarding"]
const HIDDEN_PAGES = ["/onboarding", "/codeflash/auth"]
export function ConditionalLayout({ children, user }: { children: React.ReactNode; user?: any }) {
const pathname = usePathname()

View file

@ -1,12 +1,12 @@
{
"name": "@codeflash-ai/common",
"version": "1.0.21",
"version": "1.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codeflash-ai/common",
"version": "1.0.21",
"version": "1.0.22",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",

View file

@ -1,6 +1,6 @@
{
"name": "@codeflash-ai/common",
"version": "1.0.21",
"version": "1.0.22",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"repository": {

View file

@ -8,7 +8,7 @@ async function generateRandomAPIKey(): Promise<string> {
const token = (await randomBytesAsync(48)).toString("base64url")
return "cf-" + token
}
export const VS_CODE_KEY_NAME = "vsc-ext-3Yg1NRCS@6"
export async function hashApiKey(token: string) {
// TODO: Consider stripping the cf- prefix from the token before hashing. This would reduce the chance of
// a collision, and would also make it harder to brute force the token.
@ -41,7 +41,36 @@ export async function genAndStoreAPITokenHash(keyName: string, userId: string):
})
return token
}
export async function genAndStoreAPITokenHashForVSC(userId: string) {
const token: string = await generateRandomAPIKey()
const hashedToken = await hashApiKey(token)
const data = await prisma.cf_api_keys.findFirst({
where: {
user_id: userId,
name: VS_CODE_KEY_NAME,
},
})
if (data) {
await prisma.cf_api_keys.update({
where: {
id: data.id,
},
data: {
key: hashedToken,
suffix: token.slice(-4),
},
})
} else
await prisma.cf_api_keys.create({
data: {
key: hashedToken,
user_id: userId,
suffix: token.slice(-4),
name: VS_CODE_KEY_NAME,
},
})
return token
}
export async function userForAPIKey(key: string): Promise<null | string> {
// TODO: Add a rate limiter to prevent brute force attacks.
const hashedToken = await hashApiKey(key)
@ -91,6 +120,7 @@ export async function canInsertMoreTokens(userId: string): Promise<boolean> {
const tokenCount = await prisma.cf_api_keys.count({
where: {
user_id: userId,
name: { not: VS_CODE_KEY_NAME },
},
})
return tokenCount < 30