Skip to content

Commit ba6937a

Browse files
committed
add git
1 parent f093bb7 commit ba6937a

File tree

11 files changed

+580
-342
lines changed

11 files changed

+580
-342
lines changed

src/components/Auth/ApiKeyForm.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useState } from 'react';
2+
3+
export const ApiKeyForm = ({ submit, error, isLoading }:{
4+
submit : (v:{ apiKey:string })=>Promise<void>;
5+
error? : string;
6+
isLoading? : boolean;
7+
}) => {
8+
const [apiKey, setApiKey] = useState<string>("");
9+
const [localError, setLocalError] = useState<string | null>(null);
10+
11+
const handleSubmit = async (e: React.FormEvent) => {
12+
e.preventDefault();
13+
14+
if (!apiKey.trim()) {
15+
setLocalError("Please enter an API key");
16+
return;
17+
}
18+
19+
try {
20+
await submit({ apiKey: apiKey.trim() });
21+
} catch (err) {
22+
console.error("Failed to submit API key:", err);
23+
}
24+
};
25+
26+
return (
27+
<div className="flex items-center justify-center min-h-screen p-4">
28+
<div className="w-full max-w-md p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
29+
<h1 className="text-2xl font-bold text-center mb-6">
30+
API Key Required
31+
</h1>
32+
<p className="text-gray-600 dark:text-gray-400 mb-4">
33+
Please enter your Anthropic API key to continue.
34+
</p>
35+
36+
<form onSubmit={handleSubmit} className="space-y-4">
37+
<div>
38+
<label htmlFor="apiKey" className="block text-sm font-medium mb-1">
39+
Anthropic API Key
40+
</label>
41+
<input
42+
id="apiKey"
43+
type="password"
44+
value={apiKey}
45+
onChange={(e) => setApiKey(e.target.value)}
46+
className="w-full p-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
47+
placeholder="sk-ant-..."
48+
/>
49+
</div>
50+
51+
{localError && <div className="text-red-500 text-sm">{localError}</div>}
52+
{error && <div className="text-red-500 text-sm">{error}</div>}
53+
54+
<button
55+
type="submit"
56+
disabled={isLoading}
57+
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white rounded-md disabled:opacity-50"
58+
>
59+
{isLoading ? "Submitting..." : "Submit"}
60+
</button>
61+
</form>
62+
63+
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
64+
<p>
65+
Your API key is stored locally in your browser and is only used for
66+
communicating with the Anthropic API.
67+
</p>
68+
</div>
69+
</div>
70+
</div>
71+
);
72+
};
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, {
2+
createContext, useContext, useEffect, useState, useCallback, ReactNode
3+
} from 'react';
4+
import {
5+
startRegistration,
6+
startAuthentication,
7+
deriveKey,
8+
base64URLStringToBuffer,
9+
} from '@/webauthn';
10+
import { encryptData, decryptData } from '@/webauthn';
11+
12+
type SecureContext<T> = {
13+
isAuthenticated : boolean;
14+
encryptionKey : CryptoKey | null;
15+
values : T | null;
16+
login : () => Promise<void>;
17+
logout : () => void;
18+
setValues : (v:T) => Promise<void>;
19+
};
20+
21+
const Ctx = createContext<SecureContext<any> | undefined>(undefined);
22+
23+
type FallbackRender<T> = (props: {
24+
submit : (values:T)=>Promise<void>;
25+
error? : string;
26+
isLoading? : boolean;
27+
}) => ReactNode;
28+
29+
interface Props<T> {
30+
storageKey : string;
31+
fallback : FallbackRender<T>;
32+
children : (ctx: SecureContext<T>) => ReactNode;
33+
}
34+
35+
export function SecureFormProvider<T extends Record<string, any>>(
36+
{ storageKey, fallback, children }: Props<T>
37+
){
38+
const [encryptionKey, setKey] = useState<CryptoKey|null>(null);
39+
const [values, setVals] = useState<T|null>(null);
40+
const [error, setErr] = useState<string|null>(null);
41+
const [isLoading, setLoad] = useState(false);
42+
43+
/* ──────────────────────────────────────────────── */
44+
/* Helpers */
45+
/* ──────────────────────────────────────────────── */
46+
47+
const decryptFromStorage = useCallback(async (key:CryptoKey) => {
48+
const cipher = localStorage.getItem(`${storageKey}.data`);
49+
if (!cipher) return null;
50+
const json = await decryptData(key, cipher);
51+
return JSON.parse(json) as T;
52+
}, [storageKey]);
53+
54+
const encryptAndStore = useCallback(async (key:CryptoKey, v:T) => {
55+
const cipher = await encryptData(key, JSON.stringify(v));
56+
localStorage.setItem(`${storageKey}.data`, cipher);
57+
}, [storageKey]);
58+
59+
const deriveAndSetKey = useCallback(async (rawIdBase64:string) => {
60+
const buf = base64URLStringToBuffer(rawIdBase64);
61+
const key = await deriveKey(buf);
62+
setKey(key);
63+
return key;
64+
}, []);
65+
66+
/* ──────────────────────────────────────────────── */
67+
/* Auto‑login on mount */
68+
/* ──────────────────────────────────────────────── */
69+
70+
useEffect(() => {
71+
const auto = async () => {
72+
const id = localStorage.getItem('userIdentifier');
73+
const hasBlob = !!localStorage.getItem(`${storageKey}.data`);
74+
if (!id || !hasBlob) return;
75+
76+
setLoad(true);
77+
try {
78+
const assertion = await startAuthentication();
79+
const key = await deriveAndSetKey(assertion.rawId);
80+
const v = await decryptFromStorage(key);
81+
setVals(v);
82+
} catch (e){ console.error('[SecureForm] auto‑login failed', e); }
83+
finally { setLoad(false); }
84+
};
85+
auto();
86+
}, [storageKey, decryptFromStorage, deriveAndSetKey]);
87+
88+
/* ──────────────────────────────────────────────── */
89+
/* Public API */
90+
/* ──────────────────────────────────────────────── */
91+
92+
const login = useCallback(async () => {
93+
setLoad(true); setErr(null);
94+
try {
95+
const assertion = await startAuthentication();
96+
const key = await deriveAndSetKey(assertion.rawId);
97+
const v = await decryptFromStorage(key);
98+
setVals(v);
99+
} catch(e:any){ setErr(e.message || 'Login failed'); throw e; }
100+
finally { setLoad(false); }
101+
}, [decryptFromStorage, deriveAndSetKey]);
102+
103+
const logout = useCallback(() => {
104+
setKey(null);
105+
setVals(null);
106+
setErr(null);
107+
}, []);
108+
109+
const setValues = useCallback(async (v:T) => {
110+
if (!encryptionKey) throw new Error('Not authenticated');
111+
await encryptAndStore(encryptionKey, v);
112+
setVals(v);
113+
}, [encryptAndStore, encryptionKey]);
114+
115+
/* ──────────────────────────────────────────────── */
116+
/* Fallback submit handler */
117+
/* ──────────────────────────────────────────────── */
118+
119+
const submit = useCallback(async (formVals:T) => {
120+
setLoad(true); setErr(null);
121+
try {
122+
let key = encryptionKey;
123+
if (!key){
124+
// first‑time user? → register
125+
const cred = await startRegistration(storageKey);
126+
key = await deriveAndSetKey(cred.rawId);
127+
localStorage.setItem('userIdentifier', cred.rawId);
128+
}
129+
await encryptAndStore(key!, formVals);
130+
setVals(formVals);
131+
} catch(e:any){ setErr(e.message || 'Failed to save'); throw e; }
132+
finally { setLoad(false); }
133+
}, [encryptionKey, deriveAndSetKey, encryptAndStore]);
134+
135+
const ctxValue: SecureContext<T> = {
136+
isAuthenticated : !!encryptionKey,
137+
encryptionKey,
138+
values,
139+
login,
140+
logout,
141+
setValues,
142+
};
143+
144+
/* ──────────────────────────────────────────────── */
145+
/* Render */
146+
/* ──────────────────────────────────────────────── */
147+
148+
const needFallback = !encryptionKey || values === null;
149+
150+
return (
151+
<Ctx.Provider value={ctxValue}>
152+
{needFallback
153+
? fallback({ submit, error: error || undefined, isLoading })
154+
: children(ctxValue)}
155+
</Ctx.Provider>
156+
);
157+
}
158+
159+
/* Hook for consumers */
160+
export const useSecureForm = <T,>() => {
161+
const c = useContext(Ctx);
162+
if (!c) throw new Error('useSecureForm must be inside SecureFormProvider');
163+
return c as SecureContext<T>;
164+
};

src/components/KitchenSink.stories.tsx

Lines changed: 0 additions & 88 deletions
This file was deleted.

src/components/WebcontainerCodeEditor/Cursor.stories.tsx

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { useCallback, useRef, useState, useEffect } from "react";
2+
import React from "react";
13
import { Meta, StoryObj } from "@storybook/react";
24
import "xterm/css/xterm.css";
35
import Cursor from "./Cursor";
6+
import { SecureFormProvider } from "@/components/Auth/SecureFormProvider";
7+
import { ApiKeyForm } from "@/components/Auth/ApiKeyForm";
48

59
const agentMeta: Meta<typeof Cursor> = {
6-
title: "Cursor/Editor",
10+
title: "Editors/Cursor Clone",
711
component: Cursor,
812
parameters: {
913
layout: "fullscreen",
@@ -27,25 +31,37 @@ const agentMeta: Meta<typeof Cursor> = {
2731
},
2832
},
2933
},
30-
args: {
31-
messages: [
32-
{
33-
id: "1",
34-
type: "assistant_message",
35-
content: "Hello! I'm your coding assistant. How can I help you today?",
36-
timestamp: new Date(),
37-
},
38-
],
39-
setMessages: () => {},
40-
apiKey: "dummy-key",
41-
testResults: {},
42-
},
4334
};
4435

4536
type Story = StoryObj<typeof Cursor>;
4637

47-
export const Default: Story = {
38+
export const ApiKeyAsProp: Story = {
4839
args: {
40+
apiKey: "dummy-key",
4941
},
5042
};
43+
44+
export const ApiKeyFromSecureFormProvider: Story = {
45+
decorators: [
46+
(Story) => (
47+
<Story />
48+
),
49+
],
50+
render: () => {
51+
return (
52+
<SecureFormProvider<{apiKey: string}>
53+
storageKey="anthropic_api"
54+
fallback={(props) => <ApiKeyForm {...props} />}
55+
>
56+
{({ values, login }) => (
57+
<Cursor
58+
apiKey={values?.apiKey}
59+
onRequestApiKey={login}
60+
/>
61+
)}
62+
</SecureFormProvider>
63+
);
64+
},
65+
};
66+
5167
export default agentMeta;

0 commit comments

Comments
 (0)