Sam Denty
commited on
feat: remove authentication (#1)
Browse files- .gitignore +7 -0
- .vscode/launch.json +0 -16
- README.md +1 -8
- app/components/chat/BaseChat.tsx +0 -3
- app/components/chat/Messages.client.tsx +2 -7
- app/lib/.server/auth.ts +0 -41
- app/lib/.server/sessions.ts +0 -240
- app/lib/analytics.ts +0 -38
- app/lib/auth.ts +0 -4
- app/lib/constants.ts +0 -2
- app/lib/stores/files.ts +1 -1
- app/lib/webcontainer/index.ts +0 -2
- app/routes/_index.tsx +2 -5
- app/routes/api.chat.ts +1 -2
- app/routes/api.enhancer.ts +1 -2
- app/routes/chat.$id.tsx +1 -2
- app/routes/login.tsx +0 -201
- app/routes/logout.tsx +0 -10
- package.json +6 -6
- pnpm-lock.yaml +5 -5
- worker-configuration.d.ts +0 -2
.gitignore
CHANGED
@@ -21,3 +21,10 @@ dist-ssr
|
|
21 |
*.njsproj
|
22 |
*.sln
|
23 |
*.sw?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
*.njsproj
|
22 |
*.sln
|
23 |
*.sw?
|
24 |
+
|
25 |
+
/.cache
|
26 |
+
/build
|
27 |
+
.env*
|
28 |
+
*.vars
|
29 |
+
.wrangler
|
30 |
+
_worker.bundle
|
.vscode/launch.json
DELETED
@@ -1,16 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"version": "0.2.0",
|
3 |
-
"configurations": [
|
4 |
-
{
|
5 |
-
"type": "node",
|
6 |
-
"request": "launch",
|
7 |
-
"name": "Debug Current Test File",
|
8 |
-
"autoAttachChildProcesses": true,
|
9 |
-
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
10 |
-
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
11 |
-
"args": ["run", "${relativeFile}"],
|
12 |
-
"smartStep": true,
|
13 |
-
"console": "integratedTerminal"
|
14 |
-
}
|
15 |
-
]
|
16 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
@@ -29,17 +29,10 @@ pnpm install
|
|
29 |
ANTHROPIC_API_KEY=XXX
|
30 |
```
|
31 |
|
32 |
-
Optionally, you an set the debug level
|
33 |
|
34 |
```
|
35 |
VITE_LOG_LEVEL=debug
|
36 |
-
VITE_DISABLE_AUTH=1
|
37 |
-
```
|
38 |
-
|
39 |
-
If you want to run authentication against a local StackBlitz instance, add:
|
40 |
-
|
41 |
-
```
|
42 |
-
VITE_CLIENT_ORIGIN=https://local.stackblitz.com:3000
|
43 |
```
|
44 |
|
45 |
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
|
|
29 |
ANTHROPIC_API_KEY=XXX
|
30 |
```
|
31 |
|
32 |
+
Optionally, you an set the debug level:
|
33 |
|
34 |
```
|
35 |
VITE_LOG_LEVEL=debug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
```
|
37 |
|
38 |
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
app/components/chat/BaseChat.tsx
CHANGED
@@ -9,7 +9,6 @@ import { Messages } from './Messages.client';
|
|
9 |
import { SendButton } from './SendButton.client';
|
10 |
|
11 |
import styles from './BaseChat.module.scss';
|
12 |
-
import { useLoaderData } from '@remix-run/react';
|
13 |
|
14 |
interface BaseChatProps {
|
15 |
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
@@ -59,7 +58,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
59 |
ref,
|
60 |
) => {
|
61 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
62 |
-
const { avatar } = useLoaderData<{ avatar?: string }>();
|
63 |
|
64 |
return (
|
65 |
<div
|
@@ -96,7 +94,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
96 |
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
97 |
messages={messages}
|
98 |
isStreaming={isStreaming}
|
99 |
-
avatar={avatar}
|
100 |
/>
|
101 |
) : null;
|
102 |
}}
|
|
|
9 |
import { SendButton } from './SendButton.client';
|
10 |
|
11 |
import styles from './BaseChat.module.scss';
|
|
|
12 |
|
13 |
interface BaseChatProps {
|
14 |
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
|
|
58 |
ref,
|
59 |
) => {
|
60 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
|
|
61 |
|
62 |
return (
|
63 |
<div
|
|
|
94 |
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
95 |
messages={messages}
|
96 |
isStreaming={isStreaming}
|
|
|
97 |
/>
|
98 |
) : null;
|
99 |
}}
|
app/components/chat/Messages.client.tsx
CHANGED
@@ -9,11 +9,10 @@ interface MessagesProps {
|
|
9 |
className?: string;
|
10 |
isStreaming?: boolean;
|
11 |
messages?: Message[];
|
12 |
-
avatar?: string;
|
13 |
}
|
14 |
|
15 |
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
16 |
-
const { id, isStreaming = false, messages = []
|
17 |
|
18 |
return (
|
19 |
<div id={id} ref={ref} className={props.className}>
|
@@ -36,11 +35,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|
36 |
>
|
37 |
{isUserMessage && (
|
38 |
<div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
|
39 |
-
|
40 |
-
<img className="w-full h-full object-cover" src={avatar} />
|
41 |
-
) : (
|
42 |
-
<div className="i-ph:user-fill text-xl"></div>
|
43 |
-
)}
|
44 |
</div>
|
45 |
)}
|
46 |
<div className="grid grid-col-1 w-full">
|
|
|
9 |
className?: string;
|
10 |
isStreaming?: boolean;
|
11 |
messages?: Message[];
|
|
|
12 |
}
|
13 |
|
14 |
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
15 |
+
const { id, isStreaming = false, messages = [] } = props;
|
16 |
|
17 |
return (
|
18 |
<div id={id} ref={ref} className={props.className}>
|
|
|
35 |
>
|
36 |
{isUserMessage && (
|
37 |
<div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
|
38 |
+
<div className="i-ph:user-fill text-xl"></div>
|
|
|
|
|
|
|
|
|
39 |
</div>
|
40 |
)}
|
41 |
<div className="grid grid-col-1 w-full">
|
app/lib/.server/auth.ts
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
import { json, redirect, type LoaderFunctionArgs, type TypedResponse } from '@remix-run/cloudflare';
|
2 |
-
import { isAuthenticated, type Session } from './sessions';
|
3 |
-
|
4 |
-
type RequestArgs = Pick<LoaderFunctionArgs, 'request' | 'context'>;
|
5 |
-
|
6 |
-
export async function loadWithAuth<T extends RequestArgs>(
|
7 |
-
args: T,
|
8 |
-
handler: (args: T, session: Session) => Promise<Response>,
|
9 |
-
) {
|
10 |
-
return handleWithAuth(args, handler, (response) => redirect('/login', response));
|
11 |
-
}
|
12 |
-
|
13 |
-
export async function actionWithAuth<T extends RequestArgs>(
|
14 |
-
args: T,
|
15 |
-
handler: (args: T, session: Session) => Promise<TypedResponse>,
|
16 |
-
) {
|
17 |
-
return await handleWithAuth(args, handler, (response) => json({}, { status: 401, ...response }));
|
18 |
-
}
|
19 |
-
|
20 |
-
async function handleWithAuth<T extends RequestArgs, R extends TypedResponse>(
|
21 |
-
args: T,
|
22 |
-
handler: (args: T, session: Session) => Promise<R>,
|
23 |
-
fallback: (partial: ResponseInit) => R,
|
24 |
-
) {
|
25 |
-
const { request, context } = args;
|
26 |
-
const { session, response } = await isAuthenticated(request, context.cloudflare.env);
|
27 |
-
|
28 |
-
if (session == null && !import.meta.env.VITE_DISABLE_AUTH) {
|
29 |
-
return fallback(response);
|
30 |
-
}
|
31 |
-
|
32 |
-
const handlerResponse = await handler(args, session || {});
|
33 |
-
|
34 |
-
if (response) {
|
35 |
-
for (const [key, value] of Object.entries(response.headers)) {
|
36 |
-
handlerResponse.headers.append(key, value);
|
37 |
-
}
|
38 |
-
}
|
39 |
-
|
40 |
-
return handlerResponse;
|
41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/lib/.server/sessions.ts
DELETED
@@ -1,240 +0,0 @@
|
|
1 |
-
import { createCookieSessionStorage, redirect, type Session as RemixSession } from '@remix-run/cloudflare';
|
2 |
-
import { decodeJwt } from 'jose';
|
3 |
-
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
4 |
-
import { request as doRequest } from '~/lib/fetch';
|
5 |
-
import { logger } from '~/utils/logger';
|
6 |
-
import type { Identity } from '~/lib/analytics';
|
7 |
-
import { decrypt, encrypt } from '~/lib/crypto';
|
8 |
-
|
9 |
-
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
|
10 |
-
const DEV_PAYLOAD_SECRET = import.meta.env.DEV ? '2zAyrhjcdFeXk0YEDzilMXbdrGAiR+8ACIUgFNfjLaI=' : undefined;
|
11 |
-
|
12 |
-
const TOKEN_KEY = 't';
|
13 |
-
const EXPIRES_KEY = 'e';
|
14 |
-
const USER_ID_KEY = 'u';
|
15 |
-
const SEGMENT_KEY = 's';
|
16 |
-
const AVATAR_KEY = 'a';
|
17 |
-
const ENCRYPTED_KEY = 'd';
|
18 |
-
|
19 |
-
interface PrivateSession {
|
20 |
-
[TOKEN_KEY]: string;
|
21 |
-
[EXPIRES_KEY]: number;
|
22 |
-
[USER_ID_KEY]?: string;
|
23 |
-
[SEGMENT_KEY]?: string;
|
24 |
-
}
|
25 |
-
|
26 |
-
interface PublicSession {
|
27 |
-
[ENCRYPTED_KEY]: string;
|
28 |
-
[AVATAR_KEY]?: string;
|
29 |
-
}
|
30 |
-
|
31 |
-
export interface Session {
|
32 |
-
userId?: string;
|
33 |
-
segmentWriteKey?: string;
|
34 |
-
avatar?: string;
|
35 |
-
}
|
36 |
-
|
37 |
-
export async function isAuthenticated(request: Request, env: Env) {
|
38 |
-
const { session, sessionStorage } = await getSession(request, env);
|
39 |
-
|
40 |
-
const sessionData: PrivateSession | null = await decryptSessionData(env, session.get(ENCRYPTED_KEY));
|
41 |
-
|
42 |
-
const header = async (cookie: Promise<string>) => ({ headers: { 'Set-Cookie': await cookie } });
|
43 |
-
const destroy = () => header(sessionStorage.destroySession(session));
|
44 |
-
|
45 |
-
if (sessionData?.[TOKEN_KEY] == null) {
|
46 |
-
return { session: null, response: await destroy() };
|
47 |
-
}
|
48 |
-
|
49 |
-
const expiresAt = sessionData[EXPIRES_KEY] ?? 0;
|
50 |
-
|
51 |
-
if (Date.now() < expiresAt) {
|
52 |
-
return { session: getSessionData(session, sessionData) };
|
53 |
-
}
|
54 |
-
|
55 |
-
logger.debug('Renewing token');
|
56 |
-
|
57 |
-
let data: Awaited<ReturnType<typeof refreshToken>> | null = null;
|
58 |
-
|
59 |
-
try {
|
60 |
-
data = await refreshToken(sessionData[TOKEN_KEY]);
|
61 |
-
} catch (error) {
|
62 |
-
// we can ignore the error here because it's handled below
|
63 |
-
logger.error(error);
|
64 |
-
}
|
65 |
-
|
66 |
-
if (data != null) {
|
67 |
-
const expiresAt = cookieExpiration(data.expires_in, data.created_at);
|
68 |
-
|
69 |
-
const newSessionData = { ...sessionData, [EXPIRES_KEY]: expiresAt };
|
70 |
-
const encryptedData = await encryptSessionData(env, newSessionData);
|
71 |
-
|
72 |
-
session.set(ENCRYPTED_KEY, encryptedData);
|
73 |
-
|
74 |
-
return {
|
75 |
-
session: getSessionData(session, newSessionData),
|
76 |
-
response: await header(sessionStorage.commitSession(session)),
|
77 |
-
};
|
78 |
-
} else {
|
79 |
-
return { session: null, response: await destroy() };
|
80 |
-
}
|
81 |
-
}
|
82 |
-
|
83 |
-
export async function createUserSession(
|
84 |
-
request: Request,
|
85 |
-
env: Env,
|
86 |
-
tokens: { refresh: string; expires_in: number; created_at: number },
|
87 |
-
identity?: Identity,
|
88 |
-
): Promise<ResponseInit> {
|
89 |
-
const { session, sessionStorage } = await getSession(request, env);
|
90 |
-
|
91 |
-
const expiresAt = cookieExpiration(tokens.expires_in, tokens.created_at);
|
92 |
-
|
93 |
-
const sessionData: PrivateSession = {
|
94 |
-
[TOKEN_KEY]: tokens.refresh,
|
95 |
-
[EXPIRES_KEY]: expiresAt,
|
96 |
-
[USER_ID_KEY]: identity?.userId ?? undefined,
|
97 |
-
[SEGMENT_KEY]: identity?.segmentWriteKey ?? undefined,
|
98 |
-
};
|
99 |
-
|
100 |
-
const encryptedData = await encryptSessionData(env, sessionData);
|
101 |
-
session.set(ENCRYPTED_KEY, encryptedData);
|
102 |
-
session.set(AVATAR_KEY, identity?.avatar);
|
103 |
-
|
104 |
-
return {
|
105 |
-
headers: {
|
106 |
-
'Set-Cookie': await sessionStorage.commitSession(session, {
|
107 |
-
maxAge: 3600 * 24 * 30, // 1 month
|
108 |
-
}),
|
109 |
-
},
|
110 |
-
};
|
111 |
-
}
|
112 |
-
|
113 |
-
function getSessionStorage(cloudflareEnv: Env) {
|
114 |
-
return createCookieSessionStorage<PublicSession>({
|
115 |
-
cookie: {
|
116 |
-
name: '__session',
|
117 |
-
httpOnly: true,
|
118 |
-
path: '/',
|
119 |
-
secrets: [DEV_SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
|
120 |
-
secure: import.meta.env.PROD,
|
121 |
-
},
|
122 |
-
});
|
123 |
-
}
|
124 |
-
|
125 |
-
export async function logout(request: Request, env: Env) {
|
126 |
-
const { session, sessionStorage } = await getSession(request, env);
|
127 |
-
|
128 |
-
const sessionData = await decryptSessionData(env, session.get(ENCRYPTED_KEY));
|
129 |
-
|
130 |
-
if (sessionData) {
|
131 |
-
revokeToken(sessionData[TOKEN_KEY]);
|
132 |
-
}
|
133 |
-
|
134 |
-
return redirect('/login', {
|
135 |
-
headers: {
|
136 |
-
'Set-Cookie': await sessionStorage.destroySession(session),
|
137 |
-
},
|
138 |
-
});
|
139 |
-
}
|
140 |
-
|
141 |
-
export function validateAccessToken(access: string) {
|
142 |
-
const jwtPayload = decodeJwt(access);
|
143 |
-
|
144 |
-
return jwtPayload.bolt === true;
|
145 |
-
}
|
146 |
-
|
147 |
-
function getSessionData(session: RemixSession<PublicSession>, data: PrivateSession): Session {
|
148 |
-
return {
|
149 |
-
userId: data?.[USER_ID_KEY],
|
150 |
-
segmentWriteKey: data?.[SEGMENT_KEY],
|
151 |
-
avatar: session.get(AVATAR_KEY),
|
152 |
-
};
|
153 |
-
}
|
154 |
-
|
155 |
-
async function getSession(request: Request, env: Env) {
|
156 |
-
const sessionStorage = getSessionStorage(env);
|
157 |
-
const cookie = request.headers.get('Cookie');
|
158 |
-
|
159 |
-
return { session: await sessionStorage.getSession(cookie), sessionStorage };
|
160 |
-
}
|
161 |
-
|
162 |
-
async function refreshToken(refresh: string): Promise<{ expires_in: number; created_at: number }> {
|
163 |
-
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/token`, {
|
164 |
-
method: 'POST',
|
165 |
-
body: urlParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, refresh_token: refresh }),
|
166 |
-
headers: {
|
167 |
-
'content-type': 'application/x-www-form-urlencoded',
|
168 |
-
},
|
169 |
-
});
|
170 |
-
|
171 |
-
const body = await response.json();
|
172 |
-
|
173 |
-
if (!response.ok) {
|
174 |
-
throw new Error(`Unable to refresh token\n${response.status} ${JSON.stringify(body)}`);
|
175 |
-
}
|
176 |
-
|
177 |
-
const { access_token: access } = body;
|
178 |
-
|
179 |
-
if (!validateAccessToken(access)) {
|
180 |
-
throw new Error('User is no longer authorized for Bolt');
|
181 |
-
}
|
182 |
-
|
183 |
-
return body;
|
184 |
-
}
|
185 |
-
|
186 |
-
function cookieExpiration(expireIn: number, createdAt: number) {
|
187 |
-
return (expireIn + createdAt - 10 * 60) * 1000;
|
188 |
-
}
|
189 |
-
|
190 |
-
async function revokeToken(refresh?: string) {
|
191 |
-
if (refresh == null) {
|
192 |
-
return;
|
193 |
-
}
|
194 |
-
|
195 |
-
try {
|
196 |
-
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/revoke`, {
|
197 |
-
method: 'POST',
|
198 |
-
body: urlParams({
|
199 |
-
token: refresh,
|
200 |
-
token_type_hint: 'refresh_token',
|
201 |
-
client_id: CLIENT_ID,
|
202 |
-
}),
|
203 |
-
headers: {
|
204 |
-
'content-type': 'application/x-www-form-urlencoded',
|
205 |
-
},
|
206 |
-
});
|
207 |
-
|
208 |
-
if (!response.ok) {
|
209 |
-
throw new Error(`Unable to revoke token: ${response.status}`);
|
210 |
-
}
|
211 |
-
} catch (error) {
|
212 |
-
logger.debug(error);
|
213 |
-
return;
|
214 |
-
}
|
215 |
-
}
|
216 |
-
|
217 |
-
function urlParams(data: Record<string, string>) {
|
218 |
-
const encoded = new URLSearchParams();
|
219 |
-
|
220 |
-
for (const [key, value] of Object.entries(data)) {
|
221 |
-
encoded.append(key, value);
|
222 |
-
}
|
223 |
-
|
224 |
-
return encoded;
|
225 |
-
}
|
226 |
-
|
227 |
-
async function decryptSessionData(env: Env, encryptedData?: string) {
|
228 |
-
const decryptedData = encryptedData ? await decrypt(payloadSecret(env), encryptedData) : undefined;
|
229 |
-
const sessionData: PrivateSession | null = JSON.parse(decryptedData ?? 'null');
|
230 |
-
|
231 |
-
return sessionData;
|
232 |
-
}
|
233 |
-
|
234 |
-
async function encryptSessionData(env: Env, sessionData: PrivateSession) {
|
235 |
-
return await encrypt(payloadSecret(env), JSON.stringify(sessionData));
|
236 |
-
}
|
237 |
-
|
238 |
-
function payloadSecret(env: Env) {
|
239 |
-
return DEV_PAYLOAD_SECRET || env.PAYLOAD_SECRET;
|
240 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/lib/analytics.ts
DELETED
@@ -1,38 +0,0 @@
|
|
1 |
-
import { CLIENT_ORIGIN } from '~/lib/constants';
|
2 |
-
import { request as doRequest } from '~/lib/fetch';
|
3 |
-
|
4 |
-
export interface Identity {
|
5 |
-
userId?: string | null;
|
6 |
-
guestId?: string | null;
|
7 |
-
segmentWriteKey?: string | null;
|
8 |
-
avatar?: string;
|
9 |
-
}
|
10 |
-
|
11 |
-
const MESSAGE_PREFIX = 'Bolt';
|
12 |
-
|
13 |
-
export enum AnalyticsTrackEvent {
|
14 |
-
MessageSent = `${MESSAGE_PREFIX} Message Sent`,
|
15 |
-
MessageComplete = `${MESSAGE_PREFIX} Message Complete`,
|
16 |
-
ChatCreated = `${MESSAGE_PREFIX} Chat Created`,
|
17 |
-
}
|
18 |
-
|
19 |
-
export async function identifyUser(access: string): Promise<Identity | undefined> {
|
20 |
-
const response = await doRequest(`${CLIENT_ORIGIN}/api/identify`, {
|
21 |
-
method: 'GET',
|
22 |
-
headers: { authorization: `Bearer ${access}` },
|
23 |
-
});
|
24 |
-
|
25 |
-
const body = await response.json();
|
26 |
-
|
27 |
-
if (!response.ok) {
|
28 |
-
return undefined;
|
29 |
-
}
|
30 |
-
|
31 |
-
// convert numerical identity values to strings
|
32 |
-
const stringified = Object.entries(body).map(([key, value]) => [
|
33 |
-
key,
|
34 |
-
typeof value === 'number' ? value.toString() : value,
|
35 |
-
]);
|
36 |
-
|
37 |
-
return Object.fromEntries(stringified) as Identity;
|
38 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/lib/auth.ts
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
export function forgetAuth() {
|
2 |
-
// FIXME: use dedicated method
|
3 |
-
localStorage.removeItem('__wc_api_tokens__');
|
4 |
-
}
|
|
|
|
|
|
|
|
|
|
app/lib/constants.ts
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
export const CLIENT_ID = 'bolt';
|
2 |
-
export const CLIENT_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://stackblitz.com';
|
|
|
|
|
|
app/lib/stores/files.ts
CHANGED
@@ -116,7 +116,7 @@ export class FilesStore {
|
|
116 |
async #init() {
|
117 |
const webcontainer = await this.#webcontainer;
|
118 |
|
119 |
-
webcontainer.watchPaths(
|
120 |
{ include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },
|
121 |
bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
|
122 |
);
|
|
|
116 |
async #init() {
|
117 |
const webcontainer = await this.#webcontainer;
|
118 |
|
119 |
+
webcontainer.internal.watchPaths(
|
120 |
{ include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },
|
121 |
bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
|
122 |
);
|
app/lib/webcontainer/index.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import { WebContainer } from '@webcontainer/api';
|
2 |
import { WORK_DIR_NAME } from '~/utils/constants';
|
3 |
-
import { forgetAuth } from '~/lib/auth';
|
4 |
|
5 |
interface WebContainerContext {
|
6 |
loaded: boolean;
|
@@ -23,7 +22,6 @@ if (!import.meta.env.SSR) {
|
|
23 |
import.meta.hot?.data.webcontainer ??
|
24 |
Promise.resolve()
|
25 |
.then(() => {
|
26 |
-
forgetAuth();
|
27 |
return WebContainer.boot({ workdirName: WORK_DIR_NAME });
|
28 |
})
|
29 |
.then((webcontainer) => {
|
|
|
1 |
import { WebContainer } from '@webcontainer/api';
|
2 |
import { WORK_DIR_NAME } from '~/utils/constants';
|
|
|
3 |
|
4 |
interface WebContainerContext {
|
5 |
loaded: boolean;
|
|
|
22 |
import.meta.hot?.data.webcontainer ??
|
23 |
Promise.resolve()
|
24 |
.then(() => {
|
|
|
25 |
return WebContainer.boot({ workdirName: WORK_DIR_NAME });
|
26 |
})
|
27 |
.then((webcontainer) => {
|
app/routes/_index.tsx
CHANGED
@@ -1,17 +1,14 @@
|
|
1 |
-
import { json, type
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { BaseChat } from '~/components/chat/BaseChat';
|
4 |
import { Chat } from '~/components/chat/Chat.client';
|
5 |
import { Header } from '~/components/header/Header';
|
6 |
-
import { loadWithAuth } from '~/lib/.server/auth';
|
7 |
|
8 |
export const meta: MetaFunction = () => {
|
9 |
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
10 |
};
|
11 |
|
12 |
-
export
|
13 |
-
return loadWithAuth(args, async (_args, session) => json({ avatar: session.avatar }));
|
14 |
-
}
|
15 |
|
16 |
export default function Index() {
|
17 |
return (
|
|
|
1 |
+
import { json, type MetaFunction } from '@remix-run/cloudflare';
|
2 |
import { ClientOnly } from 'remix-utils/client-only';
|
3 |
import { BaseChat } from '~/components/chat/BaseChat';
|
4 |
import { Chat } from '~/components/chat/Chat.client';
|
5 |
import { Header } from '~/components/header/Header';
|
|
|
6 |
|
7 |
export const meta: MetaFunction = () => {
|
8 |
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
9 |
};
|
10 |
|
11 |
+
export const loader = () => json({});
|
|
|
|
|
12 |
|
13 |
export default function Index() {
|
14 |
return (
|
app/routes/api.chat.ts
CHANGED
@@ -1,12 +1,11 @@
|
|
1 |
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
2 |
-
import { actionWithAuth } from '~/lib/.server/auth';
|
3 |
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
|
4 |
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
|
5 |
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
|
6 |
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
7 |
|
8 |
export async function action(args: ActionFunctionArgs) {
|
9 |
-
return
|
10 |
}
|
11 |
|
12 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
|
|
1 |
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
|
|
2 |
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
|
3 |
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
|
4 |
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
|
5 |
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
6 |
|
7 |
export async function action(args: ActionFunctionArgs) {
|
8 |
+
return chatAction(args);
|
9 |
}
|
10 |
|
11 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
app/routes/api.enhancer.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
2 |
import { StreamingTextResponse, parseStreamPart } from 'ai';
|
3 |
-
import { actionWithAuth } from '~/lib/.server/auth';
|
4 |
import { streamText } from '~/lib/.server/llm/stream-text';
|
5 |
import { stripIndents } from '~/utils/stripIndent';
|
6 |
|
@@ -8,7 +7,7 @@ const encoder = new TextEncoder();
|
|
8 |
const decoder = new TextDecoder();
|
9 |
|
10 |
export async function action(args: ActionFunctionArgs) {
|
11 |
-
return
|
12 |
}
|
13 |
|
14 |
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
|
|
1 |
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
2 |
import { StreamingTextResponse, parseStreamPart } from 'ai';
|
|
|
3 |
import { streamText } from '~/lib/.server/llm/stream-text';
|
4 |
import { stripIndents } from '~/utils/stripIndent';
|
5 |
|
|
|
7 |
const decoder = new TextDecoder();
|
8 |
|
9 |
export async function action(args: ActionFunctionArgs) {
|
10 |
+
return enhancerAction(args);
|
11 |
}
|
12 |
|
13 |
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
app/routes/chat.$id.tsx
CHANGED
@@ -1,9 +1,8 @@
|
|
1 |
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
2 |
import { default as IndexRoute } from './_index';
|
3 |
-
import { loadWithAuth } from '~/lib/.server/auth';
|
4 |
|
5 |
export async function loader(args: LoaderFunctionArgs) {
|
6 |
-
return
|
7 |
}
|
8 |
|
9 |
export default IndexRoute;
|
|
|
1 |
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
2 |
import { default as IndexRoute } from './_index';
|
|
|
3 |
|
4 |
export async function loader(args: LoaderFunctionArgs) {
|
5 |
+
return json({ id: args.params.id });
|
6 |
}
|
7 |
|
8 |
export default IndexRoute;
|
app/routes/login.tsx
DELETED
@@ -1,201 +0,0 @@
|
|
1 |
-
import {
|
2 |
-
json,
|
3 |
-
redirect,
|
4 |
-
redirectDocument,
|
5 |
-
type ActionFunctionArgs,
|
6 |
-
type LoaderFunctionArgs,
|
7 |
-
} from '@remix-run/cloudflare';
|
8 |
-
import { useFetcher, useLoaderData } from '@remix-run/react';
|
9 |
-
import { useEffect, useState } from 'react';
|
10 |
-
import { LoadingDots } from '~/components/ui/LoadingDots';
|
11 |
-
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
12 |
-
import { identifyUser } from '~/lib/analytics';
|
13 |
-
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
14 |
-
import { request as doRequest } from '~/lib/fetch';
|
15 |
-
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
|
16 |
-
import { logger } from '~/utils/logger';
|
17 |
-
|
18 |
-
export async function loader({ request, context }: LoaderFunctionArgs) {
|
19 |
-
const { session, response } = await isAuthenticated(request, context.cloudflare.env);
|
20 |
-
|
21 |
-
if (session != null) {
|
22 |
-
return redirect('/', response);
|
23 |
-
}
|
24 |
-
|
25 |
-
const url = new URL(request.url);
|
26 |
-
|
27 |
-
return json(
|
28 |
-
{
|
29 |
-
redirected: url.searchParams.has('code') || url.searchParams.has('error'),
|
30 |
-
},
|
31 |
-
response,
|
32 |
-
);
|
33 |
-
}
|
34 |
-
|
35 |
-
export async function action({ request, context }: ActionFunctionArgs) {
|
36 |
-
const formData = await request.formData();
|
37 |
-
|
38 |
-
const payload = {
|
39 |
-
access: String(formData.get('access')),
|
40 |
-
refresh: String(formData.get('refresh')),
|
41 |
-
};
|
42 |
-
|
43 |
-
let response: Awaited<ReturnType<typeof doRequest>> | undefined;
|
44 |
-
|
45 |
-
try {
|
46 |
-
response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, {
|
47 |
-
headers: { authorization: `Bearer ${payload.access}` },
|
48 |
-
});
|
49 |
-
|
50 |
-
if (!response.ok) {
|
51 |
-
throw await response.json();
|
52 |
-
}
|
53 |
-
} catch (error) {
|
54 |
-
logger.warn('Authentication failed');
|
55 |
-
logger.warn(error);
|
56 |
-
|
57 |
-
return json({ error: 'invalid-token' as const }, { status: 401 });
|
58 |
-
}
|
59 |
-
|
60 |
-
const boltEnabled = validateAccessToken(payload.access);
|
61 |
-
|
62 |
-
if (!boltEnabled) {
|
63 |
-
return json({ error: 'bolt-access' as const }, { status: 401 });
|
64 |
-
}
|
65 |
-
|
66 |
-
const identity = await identifyUser(payload.access);
|
67 |
-
|
68 |
-
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
69 |
-
|
70 |
-
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }, identity);
|
71 |
-
|
72 |
-
return redirectDocument('/', init);
|
73 |
-
}
|
74 |
-
|
75 |
-
type LoginState =
|
76 |
-
| {
|
77 |
-
kind: 'error';
|
78 |
-
error: string;
|
79 |
-
description: string;
|
80 |
-
}
|
81 |
-
| { kind: 'pending' };
|
82 |
-
|
83 |
-
const ERRORS = {
|
84 |
-
'bolt-access': 'You do not have access to Bolt.',
|
85 |
-
'invalid-token': 'Authentication failed.',
|
86 |
-
};
|
87 |
-
|
88 |
-
export default function Login() {
|
89 |
-
const { redirected } = useLoaderData<typeof loader>();
|
90 |
-
|
91 |
-
useEffect(() => {
|
92 |
-
if (!import.meta.hot?.data.wcAuth) {
|
93 |
-
auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN });
|
94 |
-
}
|
95 |
-
|
96 |
-
if (import.meta.hot) {
|
97 |
-
import.meta.hot.data.wcAuth = true;
|
98 |
-
}
|
99 |
-
}, []);
|
100 |
-
|
101 |
-
return (
|
102 |
-
<div className="min-h-screen flex items-center justify-center">
|
103 |
-
{redirected ? (
|
104 |
-
<LoadingDots text="Authenticating" />
|
105 |
-
) : (
|
106 |
-
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
|
107 |
-
<div>
|
108 |
-
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
109 |
-
</div>
|
110 |
-
<LoginForm />
|
111 |
-
<p className="mt-4 text-sm text-center text-gray-600">
|
112 |
-
By using Bolt, you agree to the collection of usage data for analytics.
|
113 |
-
</p>
|
114 |
-
</div>
|
115 |
-
)}
|
116 |
-
</div>
|
117 |
-
);
|
118 |
-
}
|
119 |
-
|
120 |
-
function LoginForm() {
|
121 |
-
const [login, setLogin] = useState<LoginState | null>(null);
|
122 |
-
|
123 |
-
const fetcher = useFetcher<typeof action>();
|
124 |
-
|
125 |
-
useEffect(() => {
|
126 |
-
auth.logout({ ignoreRevokeError: true });
|
127 |
-
}, []);
|
128 |
-
|
129 |
-
useEffect(() => {
|
130 |
-
if (fetcher.data?.error) {
|
131 |
-
auth.logout({ ignoreRevokeError: true });
|
132 |
-
|
133 |
-
setLogin({
|
134 |
-
kind: 'error' as const,
|
135 |
-
...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] },
|
136 |
-
});
|
137 |
-
}
|
138 |
-
}, [fetcher.data]);
|
139 |
-
|
140 |
-
async function attemptLogin() {
|
141 |
-
startAuthFlow();
|
142 |
-
|
143 |
-
function startAuthFlow() {
|
144 |
-
auth.startAuthFlow({ popup: true });
|
145 |
-
|
146 |
-
Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => {
|
147 |
-
if (error) {
|
148 |
-
setLogin({ kind: 'error', ...error });
|
149 |
-
} else {
|
150 |
-
onTokens();
|
151 |
-
}
|
152 |
-
});
|
153 |
-
}
|
154 |
-
|
155 |
-
async function onTokens() {
|
156 |
-
const tokens = auth.tokens()!;
|
157 |
-
|
158 |
-
fetcher.submit(tokens, {
|
159 |
-
method: 'POST',
|
160 |
-
});
|
161 |
-
|
162 |
-
setLogin({ kind: 'pending' });
|
163 |
-
}
|
164 |
-
}
|
165 |
-
|
166 |
-
return (
|
167 |
-
<>
|
168 |
-
<button
|
169 |
-
className="w-full text-white bg-accent-600 hover:bg-accent-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
|
170 |
-
onClick={attemptLogin}
|
171 |
-
disabled={login?.kind === 'pending'}
|
172 |
-
>
|
173 |
-
{login?.kind === 'pending' ? 'Authenticating...' : 'Continue with StackBlitz'}
|
174 |
-
</button>
|
175 |
-
{login?.kind === 'error' && (
|
176 |
-
<div>
|
177 |
-
<h2>
|
178 |
-
<code>{login.error}</code>
|
179 |
-
</h2>
|
180 |
-
<p>{login.description}</p>
|
181 |
-
</div>
|
182 |
-
)}
|
183 |
-
</>
|
184 |
-
);
|
185 |
-
}
|
186 |
-
|
187 |
-
interface AuthError {
|
188 |
-
error: string;
|
189 |
-
description: string;
|
190 |
-
}
|
191 |
-
|
192 |
-
function authEvent(auth: AuthAPI, event: 'logged-out'): Promise<void>;
|
193 |
-
function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise<AuthError>;
|
194 |
-
function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') {
|
195 |
-
return new Promise((resolve) => {
|
196 |
-
const unsubscribe = auth.on(event as any, (arg: any) => {
|
197 |
-
unsubscribe();
|
198 |
-
resolve(arg);
|
199 |
-
});
|
200 |
-
});
|
201 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routes/logout.tsx
DELETED
@@ -1,10 +0,0 @@
|
|
1 |
-
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
|
2 |
-
import { logout } from '~/lib/.server/sessions';
|
3 |
-
|
4 |
-
export async function loader({ request, context }: LoaderFunctionArgs) {
|
5 |
-
return logout(request, context.cloudflare.env);
|
6 |
-
}
|
7 |
-
|
8 |
-
export default function Logout() {
|
9 |
-
return '';
|
10 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package.json
CHANGED
@@ -50,7 +50,7 @@
|
|
50 |
"@remix-run/react": "^2.10.2",
|
51 |
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
52 |
"@unocss/reset": "^0.61.0",
|
53 |
-
"@webcontainer/api": "
|
54 |
"@xterm/addon-fit": "^0.10.0",
|
55 |
"@xterm/addon-web-links": "^0.11.0",
|
56 |
"@xterm/xterm": "^5.5.0",
|
@@ -76,13 +76,16 @@
|
|
76 |
"unist-util-visit": "^5.0.0"
|
77 |
},
|
78 |
"devDependencies": {
|
|
|
79 |
"@cloudflare/workers-types": "^4.20240620.0",
|
80 |
"@remix-run/dev": "^2.10.0",
|
81 |
"@types/diff": "^5.2.1",
|
82 |
"@types/react": "^18.2.20",
|
83 |
"@types/react-dom": "^18.2.7",
|
84 |
"fast-glob": "^3.3.2",
|
|
|
85 |
"node-fetch": "^3.3.2",
|
|
|
86 |
"typescript": "^5.5.2",
|
87 |
"unified": "^11.0.5",
|
88 |
"unocss": "^0.61.3",
|
@@ -90,12 +93,9 @@
|
|
90 |
"vite-plugin-node-polyfills": "^0.22.0",
|
91 |
"vite-plugin-optimize-css-modules": "^1.1.0",
|
92 |
"vite-tsconfig-paths": "^4.3.2",
|
|
|
93 |
"wrangler": "^3.63.2",
|
94 |
-
"zod": "^3.23.8"
|
95 |
-
"@blitz/eslint-plugin": "0.1.0",
|
96 |
-
"is-ci": "^3.0.1",
|
97 |
-
"prettier": "^3.3.2",
|
98 |
-
"vitest": "^2.0.1"
|
99 |
},
|
100 |
"resolutions": {
|
101 |
"@typescript-eslint/utils": "^8.0.0-alpha.30"
|
|
|
50 |
"@remix-run/react": "^2.10.2",
|
51 |
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
52 |
"@unocss/reset": "^0.61.0",
|
53 |
+
"@webcontainer/api": "1.3.0-internal.10",
|
54 |
"@xterm/addon-fit": "^0.10.0",
|
55 |
"@xterm/addon-web-links": "^0.11.0",
|
56 |
"@xterm/xterm": "^5.5.0",
|
|
|
76 |
"unist-util-visit": "^5.0.0"
|
77 |
},
|
78 |
"devDependencies": {
|
79 |
+
"@blitz/eslint-plugin": "0.1.0",
|
80 |
"@cloudflare/workers-types": "^4.20240620.0",
|
81 |
"@remix-run/dev": "^2.10.0",
|
82 |
"@types/diff": "^5.2.1",
|
83 |
"@types/react": "^18.2.20",
|
84 |
"@types/react-dom": "^18.2.7",
|
85 |
"fast-glob": "^3.3.2",
|
86 |
+
"is-ci": "^3.0.1",
|
87 |
"node-fetch": "^3.3.2",
|
88 |
+
"prettier": "^3.3.2",
|
89 |
"typescript": "^5.5.2",
|
90 |
"unified": "^11.0.5",
|
91 |
"unocss": "^0.61.3",
|
|
|
93 |
"vite-plugin-node-polyfills": "^0.22.0",
|
94 |
"vite-plugin-optimize-css-modules": "^1.1.0",
|
95 |
"vite-tsconfig-paths": "^4.3.2",
|
96 |
+
"vitest": "^2.0.1",
|
97 |
"wrangler": "^3.63.2",
|
98 |
+
"zod": "^3.23.8"
|
|
|
|
|
|
|
|
|
99 |
},
|
100 |
"resolutions": {
|
101 |
"@typescript-eslint/utils": "^8.0.0-alpha.30"
|
pnpm-lock.yaml
CHANGED
@@ -93,8 +93,8 @@ importers:
|
|
93 |
specifier: ^0.61.0
|
94 |
version: 0.61.3
|
95 |
'@webcontainer/api':
|
96 |
-
specifier:
|
97 |
-
version: 1.3.0-internal.
|
98 |
'@xterm/addon-fit':
|
99 |
specifier: ^0.10.0
|
100 |
version: 0.10.0(@xterm/[email protected])
|
@@ -1956,8 +1956,8 @@ packages:
|
|
1956 |
'@web3-storage/[email protected]':
|
1957 |
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
|
1958 |
|
1959 |
-
'@webcontainer/[email protected].
|
1960 |
-
resolution: {integrity: sha512-
|
1961 |
|
1962 |
'@xterm/[email protected]':
|
1963 |
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
@@ -7127,7 +7127,7 @@ snapshots:
|
|
7127 |
|
7128 |
'@web3-storage/[email protected]': {}
|
7129 |
|
7130 |
-
'@webcontainer/[email protected].
|
7131 |
|
7132 |
'@xterm/[email protected](@xterm/[email protected])':
|
7133 |
dependencies:
|
|
|
93 |
specifier: ^0.61.0
|
94 |
version: 0.61.3
|
95 |
'@webcontainer/api':
|
96 |
+
specifier: 1.3.0-internal.10
|
97 |
+
version: 1.3.0-internal.10
|
98 |
'@xterm/addon-fit':
|
99 |
specifier: ^0.10.0
|
100 |
version: 0.10.0(@xterm/[email protected])
|
|
|
1956 |
'@web3-storage/[email protected]':
|
1957 |
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
|
1958 |
|
1959 |
+
'@webcontainer/[email protected].10':
|
1960 |
+
resolution: {integrity: sha512-iuqjuDX2uADiJMYZok7+tJqVCJYZ+tU2NwVtxlvakRWSSmIFBGrJ38pD0C5igaOnBV8C9kGDjCE6B03SvLtN4Q==}
|
1961 |
|
1962 |
'@xterm/[email protected]':
|
1963 |
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
|
|
7127 |
|
7128 |
'@web3-storage/[email protected]': {}
|
7129 |
|
7130 |
+
'@webcontainer/[email protected].10': {}
|
7131 |
|
7132 |
'@xterm/[email protected](@xterm/[email protected])':
|
7133 |
dependencies:
|
worker-configuration.d.ts
CHANGED
@@ -1,5 +1,3 @@
|
|
1 |
interface Env {
|
2 |
ANTHROPIC_API_KEY: string;
|
3 |
-
SESSION_SECRET: string;
|
4 |
-
PAYLOAD_SECRET: string;
|
5 |
}
|
|
|
1 |
interface Env {
|
2 |
ANTHROPIC_API_KEY: string;
|
|
|
|
|
3 |
}
|