[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:
parent
a53c472d94
commit
ab012f58b0
24 changed files with 1892 additions and 214 deletions
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,6 @@ export class BootCodeflashServerStep extends BaseStep {
|
|||
sidebarProvider.sendMessage(message);
|
||||
},
|
||||
);
|
||||
|
||||
this._disposables.push(
|
||||
optimizeFunctionCommand,
|
||||
optimizeAllCommand,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
12
js/VSC-Extension/src/providers/AuthConfig.ts
Normal file
12
js/VSC-Extension/src/providers/AuthConfig.ts
Normal 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;
|
||||
472
js/VSC-Extension/src/providers/CodeflashAuthProvider.ts
Normal file
472
js/VSC-Extension/src/providers/CodeflashAuthProvider.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
8
js/cf-api/package-lock.json
generated
8
js/cf-api/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
196
js/cf-webapp/package-lock.json
generated
196
js/cf-webapp/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
266
js/cf-webapp/src/app/(auth)/codeflash/auth/action.ts
Normal file
266
js/cf-webapp/src/app/(auth)/codeflash/auth/action.ts
Normal 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" }
|
||||
}
|
||||
}
|
||||
360
js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx
Normal file
360
js/cf-webapp/src/app/(auth)/codeflash/auth/content.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
11
js/cf-webapp/src/app/(auth)/codeflash/auth/page.tsx
Normal file
11
js/cf-webapp/src/app/(auth)/codeflash/auth/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
4
js/common/package-lock.json
generated
4
js/common/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue