Sam Denty commited on
Commit
2a29fbb
·
unverified ·
1 Parent(s): 6fb59d2

feat: remove authentication (#1)

Browse files
.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 or disable authentication:
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 = [], avatar } = props;
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
- {avatar ? (
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 LoaderFunctionArgs, 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
- 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 async function loader(args: LoaderFunctionArgs) {
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 actionWithAuth(args, chatAction);
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 actionWithAuth(args, enhancerAction);
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 loadWithAuth(args, async (_args, session) => json({ id: args.params.id, avatar: session.avatar }));
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": "^1.3.0-internal.2",
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: ^1.3.0-internal.2
97
- version: 1.3.0-internal.2
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].2':
1960
- resolution: {integrity: sha512-lLSlSehbuYc9E7ecK+tMRX4BbWETNX1OgRlS+NerQh3X3sHNbxLD86eScEMAiA5VBnUeSnLtLe7eC/ftM8fR3Q==}
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].2': {}
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
  }