feat: remove authentication (#1)

This commit is contained in:
Sam Denty 2024-09-26 17:45:41 +01:00 committed by GitHub
parent 6fb59d2bc5
commit 2a29fbbe82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 27 additions and 597 deletions

7
.gitignore vendored
View File

@ -21,3 +21,10 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/.cache
/build
.env*
*.vars
.wrangler
_worker.bundle

16
.vscode/launch.json vendored
View File

@ -1,16 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}

View File

@ -29,17 +29,10 @@ pnpm install
ANTHROPIC_API_KEY=XXX ANTHROPIC_API_KEY=XXX
``` ```
Optionally, you an set the debug level or disable authentication: Optionally, you an set the debug level:
``` ```
VITE_LOG_LEVEL=debug VITE_LOG_LEVEL=debug
VITE_DISABLE_AUTH=1
```
If you want to run authentication against a local StackBlitz instance, add:
```
VITE_CLIENT_ORIGIN=https://local.stackblitz.com:3000
``` ```
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore. **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.

View File

@ -9,7 +9,6 @@ import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client'; import { SendButton } from './SendButton.client';
import styles from './BaseChat.module.scss'; import styles from './BaseChat.module.scss';
import { useLoaderData } from '@remix-run/react';
interface BaseChatProps { interface BaseChatProps {
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined; textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
@ -59,7 +58,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
ref, ref,
) => { ) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const { avatar } = useLoaderData<{ avatar?: string }>();
return ( return (
<div <div
@ -96,7 +94,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1" className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
messages={messages} messages={messages}
isStreaming={isStreaming} isStreaming={isStreaming}
avatar={avatar}
/> />
) : null; ) : null;
}} }}

View File

@ -9,11 +9,10 @@ interface MessagesProps {
className?: string; className?: string;
isStreaming?: boolean; isStreaming?: boolean;
messages?: Message[]; messages?: Message[];
avatar?: string;
} }
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => { export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
const { id, isStreaming = false, messages = [], avatar } = props; const { id, isStreaming = false, messages = [] } = props;
return ( return (
<div id={id} ref={ref} className={props.className}> <div id={id} ref={ref} className={props.className}>
@ -36,11 +35,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
> >
{isUserMessage && ( {isUserMessage && (
<div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start"> <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
{avatar ? (
<img className="w-full h-full object-cover" src={avatar} />
) : (
<div className="i-ph:user-fill text-xl"></div> <div className="i-ph:user-fill text-xl"></div>
)}
</div> </div>
)} )}
<div className="grid grid-col-1 w-full"> <div className="grid grid-col-1 w-full">

View File

@ -1,41 +0,0 @@
import { json, redirect, type LoaderFunctionArgs, type TypedResponse } from '@remix-run/cloudflare';
import { isAuthenticated, type Session } from './sessions';
type RequestArgs = Pick<LoaderFunctionArgs, 'request' | 'context'>;
export async function loadWithAuth<T extends RequestArgs>(
args: T,
handler: (args: T, session: Session) => Promise<Response>,
) {
return handleWithAuth(args, handler, (response) => redirect('/login', response));
}
export async function actionWithAuth<T extends RequestArgs>(
args: T,
handler: (args: T, session: Session) => Promise<TypedResponse>,
) {
return await handleWithAuth(args, handler, (response) => json({}, { status: 401, ...response }));
}
async function handleWithAuth<T extends RequestArgs, R extends TypedResponse>(
args: T,
handler: (args: T, session: Session) => Promise<R>,
fallback: (partial: ResponseInit) => R,
) {
const { request, context } = args;
const { session, response } = await isAuthenticated(request, context.cloudflare.env);
if (session == null && !import.meta.env.VITE_DISABLE_AUTH) {
return fallback(response);
}
const handlerResponse = await handler(args, session || {});
if (response) {
for (const [key, value] of Object.entries(response.headers)) {
handlerResponse.headers.append(key, value);
}
}
return handlerResponse;
}

View File

@ -1,240 +0,0 @@
import { createCookieSessionStorage, redirect, type Session as RemixSession } from '@remix-run/cloudflare';
import { decodeJwt } from 'jose';
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch';
import { logger } from '~/utils/logger';
import type { Identity } from '~/lib/analytics';
import { decrypt, encrypt } from '~/lib/crypto';
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
const DEV_PAYLOAD_SECRET = import.meta.env.DEV ? '2zAyrhjcdFeXk0YEDzilMXbdrGAiR+8ACIUgFNfjLaI=' : undefined;
const TOKEN_KEY = 't';
const EXPIRES_KEY = 'e';
const USER_ID_KEY = 'u';
const SEGMENT_KEY = 's';
const AVATAR_KEY = 'a';
const ENCRYPTED_KEY = 'd';
interface PrivateSession {
[TOKEN_KEY]: string;
[EXPIRES_KEY]: number;
[USER_ID_KEY]?: string;
[SEGMENT_KEY]?: string;
}
interface PublicSession {
[ENCRYPTED_KEY]: string;
[AVATAR_KEY]?: string;
}
export interface Session {
userId?: string;
segmentWriteKey?: string;
avatar?: string;
}
export async function isAuthenticated(request: Request, env: Env) {
const { session, sessionStorage } = await getSession(request, env);
const sessionData: PrivateSession | null = await decryptSessionData(env, session.get(ENCRYPTED_KEY));
const header = async (cookie: Promise<string>) => ({ headers: { 'Set-Cookie': await cookie } });
const destroy = () => header(sessionStorage.destroySession(session));
if (sessionData?.[TOKEN_KEY] == null) {
return { session: null, response: await destroy() };
}
const expiresAt = sessionData[EXPIRES_KEY] ?? 0;
if (Date.now() < expiresAt) {
return { session: getSessionData(session, sessionData) };
}
logger.debug('Renewing token');
let data: Awaited<ReturnType<typeof refreshToken>> | null = null;
try {
data = await refreshToken(sessionData[TOKEN_KEY]);
} catch (error) {
// we can ignore the error here because it's handled below
logger.error(error);
}
if (data != null) {
const expiresAt = cookieExpiration(data.expires_in, data.created_at);
const newSessionData = { ...sessionData, [EXPIRES_KEY]: expiresAt };
const encryptedData = await encryptSessionData(env, newSessionData);
session.set(ENCRYPTED_KEY, encryptedData);
return {
session: getSessionData(session, newSessionData),
response: await header(sessionStorage.commitSession(session)),
};
} else {
return { session: null, response: await destroy() };
}
}
export async function createUserSession(
request: Request,
env: Env,
tokens: { refresh: string; expires_in: number; created_at: number },
identity?: Identity,
): Promise<ResponseInit> {
const { session, sessionStorage } = await getSession(request, env);
const expiresAt = cookieExpiration(tokens.expires_in, tokens.created_at);
const sessionData: PrivateSession = {
[TOKEN_KEY]: tokens.refresh,
[EXPIRES_KEY]: expiresAt,
[USER_ID_KEY]: identity?.userId ?? undefined,
[SEGMENT_KEY]: identity?.segmentWriteKey ?? undefined,
};
const encryptedData = await encryptSessionData(env, sessionData);
session.set(ENCRYPTED_KEY, encryptedData);
session.set(AVATAR_KEY, identity?.avatar);
return {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session, {
maxAge: 3600 * 24 * 30, // 1 month
}),
},
};
}
function getSessionStorage(cloudflareEnv: Env) {
return createCookieSessionStorage<PublicSession>({
cookie: {
name: '__session',
httpOnly: true,
path: '/',
secrets: [DEV_SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
secure: import.meta.env.PROD,
},
});
}
export async function logout(request: Request, env: Env) {
const { session, sessionStorage } = await getSession(request, env);
const sessionData = await decryptSessionData(env, session.get(ENCRYPTED_KEY));
if (sessionData) {
revokeToken(sessionData[TOKEN_KEY]);
}
return redirect('/login', {
headers: {
'Set-Cookie': await sessionStorage.destroySession(session),
},
});
}
export function validateAccessToken(access: string) {
const jwtPayload = decodeJwt(access);
return jwtPayload.bolt === true;
}
function getSessionData(session: RemixSession<PublicSession>, data: PrivateSession): Session {
return {
userId: data?.[USER_ID_KEY],
segmentWriteKey: data?.[SEGMENT_KEY],
avatar: session.get(AVATAR_KEY),
};
}
async function getSession(request: Request, env: Env) {
const sessionStorage = getSessionStorage(env);
const cookie = request.headers.get('Cookie');
return { session: await sessionStorage.getSession(cookie), sessionStorage };
}
async function refreshToken(refresh: string): Promise<{ expires_in: number; created_at: number }> {
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/token`, {
method: 'POST',
body: urlParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, refresh_token: refresh }),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
const body = await response.json();
if (!response.ok) {
throw new Error(`Unable to refresh token\n${response.status} ${JSON.stringify(body)}`);
}
const { access_token: access } = body;
if (!validateAccessToken(access)) {
throw new Error('User is no longer authorized for Bolt');
}
return body;
}
function cookieExpiration(expireIn: number, createdAt: number) {
return (expireIn + createdAt - 10 * 60) * 1000;
}
async function revokeToken(refresh?: string) {
if (refresh == null) {
return;
}
try {
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/revoke`, {
method: 'POST',
body: urlParams({
token: refresh,
token_type_hint: 'refresh_token',
client_id: CLIENT_ID,
}),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
if (!response.ok) {
throw new Error(`Unable to revoke token: ${response.status}`);
}
} catch (error) {
logger.debug(error);
return;
}
}
function urlParams(data: Record<string, string>) {
const encoded = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
encoded.append(key, value);
}
return encoded;
}
async function decryptSessionData(env: Env, encryptedData?: string) {
const decryptedData = encryptedData ? await decrypt(payloadSecret(env), encryptedData) : undefined;
const sessionData: PrivateSession | null = JSON.parse(decryptedData ?? 'null');
return sessionData;
}
async function encryptSessionData(env: Env, sessionData: PrivateSession) {
return await encrypt(payloadSecret(env), JSON.stringify(sessionData));
}
function payloadSecret(env: Env) {
return DEV_PAYLOAD_SECRET || env.PAYLOAD_SECRET;
}

View File

@ -1,38 +0,0 @@
import { CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch';
export interface Identity {
userId?: string | null;
guestId?: string | null;
segmentWriteKey?: string | null;
avatar?: string;
}
const MESSAGE_PREFIX = 'Bolt';
export enum AnalyticsTrackEvent {
MessageSent = `${MESSAGE_PREFIX} Message Sent`,
MessageComplete = `${MESSAGE_PREFIX} Message Complete`,
ChatCreated = `${MESSAGE_PREFIX} Chat Created`,
}
export async function identifyUser(access: string): Promise<Identity | undefined> {
const response = await doRequest(`${CLIENT_ORIGIN}/api/identify`, {
method: 'GET',
headers: { authorization: `Bearer ${access}` },
});
const body = await response.json();
if (!response.ok) {
return undefined;
}
// convert numerical identity values to strings
const stringified = Object.entries(body).map(([key, value]) => [
key,
typeof value === 'number' ? value.toString() : value,
]);
return Object.fromEntries(stringified) as Identity;
}

View File

@ -1,4 +0,0 @@
export function forgetAuth() {
// FIXME: use dedicated method
localStorage.removeItem('__wc_api_tokens__');
}

View File

@ -1,2 +0,0 @@
export const CLIENT_ID = 'bolt';
export const CLIENT_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://stackblitz.com';

View File

@ -116,7 +116,7 @@ export class FilesStore {
async #init() { async #init() {
const webcontainer = await this.#webcontainer; const webcontainer = await this.#webcontainer;
webcontainer.watchPaths( webcontainer.internal.watchPaths(
{ include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true }, { include: [`${WORK_DIR}/**`], exclude: ['**/node_modules', '.git'], includeContent: true },
bufferWatchEvents(100, this.#processEventBuffer.bind(this)), bufferWatchEvents(100, this.#processEventBuffer.bind(this)),
); );

View File

@ -1,6 +1,5 @@
import { WebContainer } from '@webcontainer/api'; import { WebContainer } from '@webcontainer/api';
import { WORK_DIR_NAME } from '~/utils/constants'; import { WORK_DIR_NAME } from '~/utils/constants';
import { forgetAuth } from '~/lib/auth';
interface WebContainerContext { interface WebContainerContext {
loaded: boolean; loaded: boolean;
@ -23,7 +22,6 @@ if (!import.meta.env.SSR) {
import.meta.hot?.data.webcontainer ?? import.meta.hot?.data.webcontainer ??
Promise.resolve() Promise.resolve()
.then(() => { .then(() => {
forgetAuth();
return WebContainer.boot({ workdirName: WORK_DIR_NAME }); return WebContainer.boot({ workdirName: WORK_DIR_NAME });
}) })
.then((webcontainer) => { .then((webcontainer) => {

View File

@ -1,17 +1,14 @@
import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare'; import { json, type MetaFunction } from '@remix-run/cloudflare';
import { ClientOnly } from 'remix-utils/client-only'; import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat'; import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client'; import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header'; import { Header } from '~/components/header/Header';
import { loadWithAuth } from '~/lib/.server/auth';
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
}; };
export async function loader(args: LoaderFunctionArgs) { export const loader = () => json({});
return loadWithAuth(args, async (_args, session) => json({ avatar: session.avatar }));
}
export default function Index() { export default function Index() {
return ( return (

View File

@ -1,12 +1,11 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'; import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { actionWithAuth } from '~/lib/.server/auth';
import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants'; import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts'; import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text'; import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
import SwitchableStream from '~/lib/.server/llm/switchable-stream'; import SwitchableStream from '~/lib/.server/llm/switchable-stream';
export async function action(args: ActionFunctionArgs) { export async function action(args: ActionFunctionArgs) {
return actionWithAuth(args, chatAction); return chatAction(args);
} }
async function chatAction({ context, request }: ActionFunctionArgs) { async function chatAction({ context, request }: ActionFunctionArgs) {

View File

@ -1,6 +1,5 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'; import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { StreamingTextResponse, parseStreamPart } from 'ai'; import { StreamingTextResponse, parseStreamPart } from 'ai';
import { actionWithAuth } from '~/lib/.server/auth';
import { streamText } from '~/lib/.server/llm/stream-text'; import { streamText } from '~/lib/.server/llm/stream-text';
import { stripIndents } from '~/utils/stripIndent'; import { stripIndents } from '~/utils/stripIndent';
@ -8,7 +7,7 @@ const encoder = new TextEncoder();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
export async function action(args: ActionFunctionArgs) { export async function action(args: ActionFunctionArgs) {
return actionWithAuth(args, enhancerAction); return enhancerAction(args);
} }
async function enhancerAction({ context, request }: ActionFunctionArgs) { async function enhancerAction({ context, request }: ActionFunctionArgs) {

View File

@ -1,9 +1,8 @@
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
import { default as IndexRoute } from './_index'; import { default as IndexRoute } from './_index';
import { loadWithAuth } from '~/lib/.server/auth';
export async function loader(args: LoaderFunctionArgs) { export async function loader(args: LoaderFunctionArgs) {
return loadWithAuth(args, async (_args, session) => json({ id: args.params.id, avatar: session.avatar })); return json({ id: args.params.id });
} }
export default IndexRoute; export default IndexRoute;

View File

@ -1,201 +0,0 @@
import {
json,
redirect,
redirectDocument,
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from '@remix-run/cloudflare';
import { useFetcher, useLoaderData } from '@remix-run/react';
import { useEffect, useState } from 'react';
import { LoadingDots } from '~/components/ui/LoadingDots';
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
import { identifyUser } from '~/lib/analytics';
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch';
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
import { logger } from '~/utils/logger';
export async function loader({ request, context }: LoaderFunctionArgs) {
const { session, response } = await isAuthenticated(request, context.cloudflare.env);
if (session != null) {
return redirect('/', response);
}
const url = new URL(request.url);
return json(
{
redirected: url.searchParams.has('code') || url.searchParams.has('error'),
},
response,
);
}
export async function action({ request, context }: ActionFunctionArgs) {
const formData = await request.formData();
const payload = {
access: String(formData.get('access')),
refresh: String(formData.get('refresh')),
};
let response: Awaited<ReturnType<typeof doRequest>> | undefined;
try {
response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, {
headers: { authorization: `Bearer ${payload.access}` },
});
if (!response.ok) {
throw await response.json();
}
} catch (error) {
logger.warn('Authentication failed');
logger.warn(error);
return json({ error: 'invalid-token' as const }, { status: 401 });
}
const boltEnabled = validateAccessToken(payload.access);
if (!boltEnabled) {
return json({ error: 'bolt-access' as const }, { status: 401 });
}
const identity = await identifyUser(payload.access);
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }, identity);
return redirectDocument('/', init);
}
type LoginState =
| {
kind: 'error';
error: string;
description: string;
}
| { kind: 'pending' };
const ERRORS = {
'bolt-access': 'You do not have access to Bolt.',
'invalid-token': 'Authentication failed.',
};
export default function Login() {
const { redirected } = useLoaderData<typeof loader>();
useEffect(() => {
if (!import.meta.hot?.data.wcAuth) {
auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN });
}
if (import.meta.hot) {
import.meta.hot.data.wcAuth = true;
}
}, []);
return (
<div className="min-h-screen flex items-center justify-center">
{redirected ? (
<LoadingDots text="Authenticating" />
) : (
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
</div>
<LoginForm />
<p className="mt-4 text-sm text-center text-gray-600">
By using Bolt, you agree to the collection of usage data for analytics.
</p>
</div>
)}
</div>
);
}
function LoginForm() {
const [login, setLogin] = useState<LoginState | null>(null);
const fetcher = useFetcher<typeof action>();
useEffect(() => {
auth.logout({ ignoreRevokeError: true });
}, []);
useEffect(() => {
if (fetcher.data?.error) {
auth.logout({ ignoreRevokeError: true });
setLogin({
kind: 'error' as const,
...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] },
});
}
}, [fetcher.data]);
async function attemptLogin() {
startAuthFlow();
function startAuthFlow() {
auth.startAuthFlow({ popup: true });
Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => {
if (error) {
setLogin({ kind: 'error', ...error });
} else {
onTokens();
}
});
}
async function onTokens() {
const tokens = auth.tokens()!;
fetcher.submit(tokens, {
method: 'POST',
});
setLogin({ kind: 'pending' });
}
}
return (
<>
<button
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"
onClick={attemptLogin}
disabled={login?.kind === 'pending'}
>
{login?.kind === 'pending' ? 'Authenticating...' : 'Continue with StackBlitz'}
</button>
{login?.kind === 'error' && (
<div>
<h2>
<code>{login.error}</code>
</h2>
<p>{login.description}</p>
</div>
)}
</>
);
}
interface AuthError {
error: string;
description: string;
}
function authEvent(auth: AuthAPI, event: 'logged-out'): Promise<void>;
function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise<AuthError>;
function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') {
return new Promise((resolve) => {
const unsubscribe = auth.on(event as any, (arg: any) => {
unsubscribe();
resolve(arg);
});
});
}

View File

@ -1,10 +0,0 @@
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { logout } from '~/lib/.server/sessions';
export async function loader({ request, context }: LoaderFunctionArgs) {
return logout(request, context.cloudflare.env);
}
export default function Logout() {
return '';
}

View File

@ -50,7 +50,7 @@
"@remix-run/react": "^2.10.2", "@remix-run/react": "^2.10.2",
"@uiw/codemirror-theme-vscode": "^4.23.0", "@uiw/codemirror-theme-vscode": "^4.23.0",
"@unocss/reset": "^0.61.0", "@unocss/reset": "^0.61.0",
"@webcontainer/api": "^1.3.0-internal.2", "@webcontainer/api": "1.3.0-internal.10",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
@ -76,13 +76,16 @@
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@blitz/eslint-plugin": "0.1.0",
"@cloudflare/workers-types": "^4.20240620.0", "@cloudflare/workers-types": "^4.20240620.0",
"@remix-run/dev": "^2.10.0", "@remix-run/dev": "^2.10.0",
"@types/diff": "^5.2.1", "@types/diff": "^5.2.1",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-ci": "^3.0.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.3.2",
"typescript": "^5.5.2", "typescript": "^5.5.2",
"unified": "^11.0.5", "unified": "^11.0.5",
"unocss": "^0.61.3", "unocss": "^0.61.3",
@ -90,12 +93,9 @@
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-optimize-css-modules": "^1.1.0", "vite-plugin-optimize-css-modules": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.1",
"wrangler": "^3.63.2", "wrangler": "^3.63.2",
"zod": "^3.23.8", "zod": "^3.23.8"
"@blitz/eslint-plugin": "0.1.0",
"is-ci": "^3.0.1",
"prettier": "^3.3.2",
"vitest": "^2.0.1"
}, },
"resolutions": { "resolutions": {
"@typescript-eslint/utils": "^8.0.0-alpha.30" "@typescript-eslint/utils": "^8.0.0-alpha.30"

View File

@ -93,8 +93,8 @@ importers:
specifier: ^0.61.0 specifier: ^0.61.0
version: 0.61.3 version: 0.61.3
'@webcontainer/api': '@webcontainer/api':
specifier: ^1.3.0-internal.2 specifier: 1.3.0-internal.10
version: 1.3.0-internal.2 version: 1.3.0-internal.10
'@xterm/addon-fit': '@xterm/addon-fit':
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0) version: 0.10.0(@xterm/xterm@5.5.0)
@ -1956,8 +1956,8 @@ packages:
'@web3-storage/multipart-parser@1.0.0': '@web3-storage/multipart-parser@1.0.0':
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
'@webcontainer/api@1.3.0-internal.2': '@webcontainer/api@1.3.0-internal.10':
resolution: {integrity: sha512-lLSlSehbuYc9E7ecK+tMRX4BbWETNX1OgRlS+NerQh3X3sHNbxLD86eScEMAiA5VBnUeSnLtLe7eC/ftM8fR3Q==} resolution: {integrity: sha512-iuqjuDX2uADiJMYZok7+tJqVCJYZ+tU2NwVtxlvakRWSSmIFBGrJ38pD0C5igaOnBV8C9kGDjCE6B03SvLtN4Q==}
'@xterm/addon-fit@0.10.0': '@xterm/addon-fit@0.10.0':
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
@ -7127,7 +7127,7 @@ snapshots:
'@web3-storage/multipart-parser@1.0.0': {} '@web3-storage/multipart-parser@1.0.0': {}
'@webcontainer/api@1.3.0-internal.2': {} '@webcontainer/api@1.3.0-internal.10': {}
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
dependencies: dependencies:

View File

@ -1,5 +1,3 @@
interface Env { interface Env {
ANTHROPIC_API_KEY: string; ANTHROPIC_API_KEY: string;
SESSION_SECRET: string;
PAYLOAD_SECRET: string;
} }