Roberto Vidal
commited on
feat: oauth-based login (#7)
Browse files- README.md +6 -0
- package.json +1 -2
- packages/bolt/README.md +3 -4
- packages/bolt/app/components/header/Header.tsx +5 -1
- packages/bolt/app/lib/.server/login.ts +27 -5
- packages/bolt/app/lib/.server/sessions.ts +138 -27
- packages/bolt/app/lib/auth.ts +4 -0
- packages/bolt/app/lib/constants.ts +2 -0
- packages/bolt/app/lib/fetch.ts +14 -0
- packages/bolt/app/lib/webcontainer/index.ts +5 -1
- packages/bolt/app/routes/api.chat.ts +6 -1
- packages/bolt/app/routes/api.enhancer.ts +6 -1
- packages/bolt/app/routes/login.tsx +156 -54
- packages/bolt/app/routes/logout.tsx +10 -0
- packages/bolt/app/utils/logger.ts +1 -1
- packages/bolt/package.json +3 -0
- pnpm-lock.yaml +136 -5
README.md
CHANGED
@@ -32,6 +32,12 @@ cd bolt
|
|
32 |
pnpm i
|
33 |
```
|
34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
### Development
|
36 |
|
37 |
To start developing the Bolt UI:
|
|
|
32 |
pnpm i
|
33 |
```
|
34 |
|
35 |
+
3. Optionally, init git hooks:
|
36 |
+
|
37 |
+
```bash
|
38 |
+
pnpmx husky
|
39 |
+
```
|
40 |
+
|
41 |
### Development
|
42 |
|
43 |
To start developing the Bolt UI:
|
package.json
CHANGED
@@ -6,8 +6,7 @@
|
|
6 |
"playground:dev": "pnpm run --filter=playground dev",
|
7 |
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
8 |
"test": "pnpm run -r test",
|
9 |
-
"typecheck": "pnpm run -r typecheck"
|
10 |
-
"prepare": "husky"
|
11 |
},
|
12 |
"commitlint": {
|
13 |
"extends": [
|
|
|
6 |
"playground:dev": "pnpm run --filter=playground dev",
|
7 |
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
8 |
"test": "pnpm run -r test",
|
9 |
+
"typecheck": "pnpm run -r typecheck"
|
|
|
10 |
},
|
11 |
"commitlint": {
|
12 |
"extends": [
|
packages/bolt/README.md
CHANGED
@@ -36,12 +36,11 @@ Optionally, you an set the debug level:
|
|
36 |
VITE_LOG_LEVEL=debug
|
37 |
```
|
38 |
|
39 |
-
If you want to
|
40 |
-
|
41 |
```
|
42 |
-
|
43 |
-
LOGIN_PASSWORD=XXX
|
44 |
```
|
|
|
45 |
|
46 |
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
47 |
|
|
|
36 |
VITE_LOG_LEVEL=debug
|
37 |
```
|
38 |
|
39 |
+
If you want to run authentication against a local StackBlitz instance, add:
|
|
|
40 |
```
|
41 |
+
VITE_CLIENT_ORIGIN=https://local.stackblitz.com:3000
|
|
|
42 |
```
|
43 |
+
`
|
44 |
|
45 |
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
46 |
|
packages/bolt/app/components/header/Header.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import { ClientOnly } from 'remix-utils/client-only';
|
2 |
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
|
|
3 |
|
4 |
export function Header() {
|
5 |
return (
|
@@ -7,8 +8,11 @@ export function Header() {
|
|
7 |
<div className="flex items-center gap-2">
|
8 |
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
9 |
</div>
|
10 |
-
<div className="ml-auto">
|
11 |
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
|
|
|
|
|
|
12 |
</div>
|
13 |
</header>
|
14 |
);
|
|
|
1 |
import { ClientOnly } from 'remix-utils/client-only';
|
2 |
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
3 |
+
import { IconButton } from '~/components/ui/IconButton';
|
4 |
|
5 |
export function Header() {
|
6 |
return (
|
|
|
8 |
<div className="flex items-center gap-2">
|
9 |
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
10 |
</div>
|
11 |
+
<div className="ml-auto flex gap-2">
|
12 |
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
13 |
+
<a href="/logout">
|
14 |
+
<IconButton icon="i-ph:sign-out" />
|
15 |
+
</a>
|
16 |
</div>
|
17 |
</header>
|
18 |
);
|
packages/bolt/app/lib/.server/login.ts
CHANGED
@@ -8,12 +8,34 @@ export function verifyPassword(password: string, cloudflareEnv: Env) {
|
|
8 |
return password === loginPassword;
|
9 |
}
|
10 |
|
11 |
-
|
12 |
-
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
13 |
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
}
|
17 |
|
18 |
-
return
|
19 |
}
|
|
|
8 |
return password === loginPassword;
|
9 |
}
|
10 |
|
11 |
+
type RequestArgs = Pick<LoaderFunctionArgs, 'request' | 'context'>;
|
|
|
12 |
|
13 |
+
export async function handleAuthRequest<T extends RequestArgs>(args: T, body: object = {}) {
|
14 |
+
const { request, context } = args;
|
15 |
+
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
16 |
+
|
17 |
+
if (authenticated) {
|
18 |
+
return json(body, response);
|
19 |
+
}
|
20 |
+
|
21 |
+
return redirect('/login', response);
|
22 |
+
}
|
23 |
+
|
24 |
+
export async function handleWithAuth<T extends RequestArgs>(args: T, handler: (args: T) => Promise<Response>) {
|
25 |
+
const { request, context } = args;
|
26 |
+
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
27 |
+
|
28 |
+
if (authenticated) {
|
29 |
+
const handlerResponse = await handler(args);
|
30 |
+
|
31 |
+
if (response) {
|
32 |
+
for (const [key, value] of Object.entries(response.headers)) {
|
33 |
+
handlerResponse.headers.append(key, value);
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
return handlerResponse;
|
38 |
}
|
39 |
|
40 |
+
return json({}, { status: 401 });
|
41 |
}
|
packages/bolt/app/lib/.server/sessions.ts
CHANGED
@@ -1,31 +1,89 @@
|
|
1 |
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
2 |
-
import {
|
|
|
|
|
|
|
3 |
|
4 |
-
const
|
5 |
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
cookie: {
|
9 |
name: '__session',
|
10 |
httpOnly: true,
|
11 |
path: '/',
|
12 |
-
|
13 |
-
|
14 |
-
secure: false,
|
15 |
},
|
16 |
});
|
17 |
}
|
18 |
|
19 |
-
export async function getSession(request: Request, env: Env) {
|
20 |
-
const sessionStorage = createSessionStorage(env);
|
21 |
-
const cookie = request.headers.get('Cookie');
|
22 |
-
|
23 |
-
return { session: await sessionStorage.getSession(cookie), sessionStorage };
|
24 |
-
}
|
25 |
-
|
26 |
export async function logout(request: Request, env: Env) {
|
27 |
const { session, sessionStorage } = await getSession(request, env);
|
28 |
|
|
|
|
|
29 |
return redirect('/login', {
|
30 |
headers: {
|
31 |
'Set-Cookie': await sessionStorage.destroySession(session),
|
@@ -33,23 +91,76 @@ export async function logout(request: Request, env: Env) {
|
|
33 |
});
|
34 |
}
|
35 |
|
36 |
-
export
|
37 |
-
const
|
38 |
-
|
|
|
39 |
|
40 |
-
return
|
41 |
}
|
42 |
|
43 |
-
|
44 |
-
const
|
|
|
45 |
|
46 |
-
session.
|
|
|
47 |
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
}),
|
53 |
-
}
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
}
|
|
|
1 |
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
|
2 |
+
import { request as doRequest } from '~/lib/fetch';
|
3 |
+
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
4 |
+
import { logger } from '~/utils/logger';
|
5 |
+
import { decode } from 'jsonwebtoken';
|
6 |
|
7 |
+
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
|
8 |
|
9 |
+
interface SessionData {
|
10 |
+
refresh: string;
|
11 |
+
expiresAt: number;
|
12 |
+
}
|
13 |
+
|
14 |
+
export async function isAuthenticated(request: Request, env: Env) {
|
15 |
+
const { session, sessionStorage } = await getSession(request, env);
|
16 |
+
const token = session.get('refresh');
|
17 |
+
|
18 |
+
const header = async (cookie: Promise<string>) => ({ headers: { 'Set-Cookie': await cookie } });
|
19 |
+
const destroy = () => header(sessionStorage.destroySession(session));
|
20 |
+
|
21 |
+
if (token == null) {
|
22 |
+
return { authenticated: false as const, response: await destroy() };
|
23 |
+
}
|
24 |
+
|
25 |
+
const expiresAt = session.get('expiresAt') ?? 0;
|
26 |
+
|
27 |
+
if (Date.now() < expiresAt) {
|
28 |
+
return { authenticated: true as const };
|
29 |
+
}
|
30 |
+
|
31 |
+
let data: Awaited<ReturnType<typeof refreshToken>> | null = null;
|
32 |
+
|
33 |
+
try {
|
34 |
+
data = await refreshToken(token);
|
35 |
+
} catch {
|
36 |
+
// ignore
|
37 |
+
}
|
38 |
+
|
39 |
+
if (data != null) {
|
40 |
+
const expiresAt = cookieExpiration(data.expires_in, data.created_at);
|
41 |
+
session.set('expiresAt', expiresAt);
|
42 |
+
|
43 |
+
return { authenticated: true as const, response: await header(sessionStorage.commitSession(session)) };
|
44 |
+
} else {
|
45 |
+
return { authenticated: false as const, response: await destroy() };
|
46 |
+
}
|
47 |
+
}
|
48 |
+
|
49 |
+
export async function createUserSession(
|
50 |
+
request: Request,
|
51 |
+
env: Env,
|
52 |
+
tokens: { refresh: string; expires_in: number; created_at: number },
|
53 |
+
): Promise<ResponseInit> {
|
54 |
+
const { session, sessionStorage } = await getSession(request, env);
|
55 |
+
|
56 |
+
const expiresAt = cookieExpiration(tokens.expires_in, tokens.created_at);
|
57 |
+
|
58 |
+
session.set('refresh', tokens.refresh);
|
59 |
+
session.set('expiresAt', expiresAt);
|
60 |
+
|
61 |
+
return {
|
62 |
+
headers: {
|
63 |
+
'Set-Cookie': await sessionStorage.commitSession(session, {
|
64 |
+
maxAge: 3600 * 24 * 30, // 1 month
|
65 |
+
}),
|
66 |
+
},
|
67 |
+
};
|
68 |
+
}
|
69 |
+
|
70 |
+
function getSessionStorage(cloudflareEnv: Env) {
|
71 |
+
return createCookieSessionStorage<SessionData>({
|
72 |
cookie: {
|
73 |
name: '__session',
|
74 |
httpOnly: true,
|
75 |
path: '/',
|
76 |
+
secrets: [DEV_SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
|
77 |
+
secure: import.meta.env.PROD,
|
|
|
78 |
},
|
79 |
});
|
80 |
}
|
81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
export async function logout(request: Request, env: Env) {
|
83 |
const { session, sessionStorage } = await getSession(request, env);
|
84 |
|
85 |
+
revokeToken(session.get('refresh'));
|
86 |
+
|
87 |
return redirect('/login', {
|
88 |
headers: {
|
89 |
'Set-Cookie': await sessionStorage.destroySession(session),
|
|
|
91 |
});
|
92 |
}
|
93 |
|
94 |
+
export function validateAccessToken(access: string) {
|
95 |
+
const jwtPayload = decode(access);
|
96 |
+
|
97 |
+
const boltEnabled = typeof jwtPayload === 'object' && jwtPayload != null && jwtPayload.bolt === true;
|
98 |
|
99 |
+
return boltEnabled;
|
100 |
}
|
101 |
|
102 |
+
async function getSession(request: Request, env: Env) {
|
103 |
+
const sessionStorage = getSessionStorage(env);
|
104 |
+
const cookie = request.headers.get('Cookie');
|
105 |
|
106 |
+
return { session: await sessionStorage.getSession(cookie), sessionStorage };
|
107 |
+
}
|
108 |
|
109 |
+
async function refreshToken(refresh: string): Promise<{ expires_in: number; created_at: number }> {
|
110 |
+
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/token`, {
|
111 |
+
method: 'POST',
|
112 |
+
body: urlParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, refresh_token: refresh }),
|
113 |
+
});
|
114 |
+
|
115 |
+
const body = await response.json();
|
116 |
+
|
117 |
+
if (!response.ok) {
|
118 |
+
throw new Error(`Unable to refresh token\n${JSON.stringify(body)}`);
|
119 |
+
}
|
120 |
+
|
121 |
+
const { access_token: access } = body;
|
122 |
+
|
123 |
+
if (!validateAccessToken(access)) {
|
124 |
+
throw new Error('User is no longer authorized for Bolt');
|
125 |
+
}
|
126 |
+
|
127 |
+
return body;
|
128 |
+
}
|
129 |
+
|
130 |
+
function cookieExpiration(expireIn: number, createdAt: number) {
|
131 |
+
return (expireIn + createdAt - 10 * 60) * 1000;
|
132 |
+
}
|
133 |
+
|
134 |
+
async function revokeToken(refresh?: string) {
|
135 |
+
if (refresh == null) {
|
136 |
+
return;
|
137 |
+
}
|
138 |
+
|
139 |
+
try {
|
140 |
+
const response = await doRequest(`${CLIENT_ORIGIN}/oauth/revoke`, {
|
141 |
+
method: 'POST',
|
142 |
+
body: urlParams({
|
143 |
+
token: refresh,
|
144 |
+
token_type_hint: 'refresh_token',
|
145 |
+
client_id: CLIENT_ID,
|
146 |
}),
|
147 |
+
});
|
148 |
+
|
149 |
+
if (!response.ok) {
|
150 |
+
throw new Error(`Unable to revoke token: ${response.status}`);
|
151 |
+
}
|
152 |
+
} catch (error) {
|
153 |
+
logger.debug(error);
|
154 |
+
return;
|
155 |
+
}
|
156 |
+
}
|
157 |
+
|
158 |
+
function urlParams(data: Record<string, string>) {
|
159 |
+
const encoded = new URLSearchParams();
|
160 |
+
|
161 |
+
for (const [key, value] of Object.entries(data)) {
|
162 |
+
encoded.append(key, value);
|
163 |
+
}
|
164 |
+
|
165 |
+
return encoded;
|
166 |
}
|
packages/bolt/app/lib/auth.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function forgetAuth() {
|
2 |
+
// FIXME: use dedicated method
|
3 |
+
localStorage.removeItem('__wc_api_tokens__');
|
4 |
+
}
|
packages/bolt/app/lib/constants.ts
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
export const CLIENT_ID = 'bolt';
|
2 |
+
export const CLIENT_ORIGIN = import.meta.env.VITE_CLIENT_ORIGIN ?? 'https://stackblitz.com';
|
packages/bolt/app/lib/fetch.ts
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
type CommonRequest = Omit<RequestInit, 'body'> & { body?: URLSearchParams };
|
2 |
+
|
3 |
+
export async function request(url: string, init?: CommonRequest) {
|
4 |
+
if (import.meta.env.DEV) {
|
5 |
+
const nodeFetch = await import('node-fetch');
|
6 |
+
const https = await import('node:https');
|
7 |
+
|
8 |
+
const agent = url.startsWith('https') ? new https.Agent({ rejectUnauthorized: false }) : undefined;
|
9 |
+
|
10 |
+
return nodeFetch.default(url, { ...init, agent });
|
11 |
+
}
|
12 |
+
|
13 |
+
return fetch(url, init);
|
14 |
+
}
|
packages/bolt/app/lib/webcontainer/index.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import { WebContainer } from '@webcontainer/api';
|
2 |
import { WORK_DIR_NAME } from '~/utils/constants';
|
|
|
3 |
|
4 |
interface WebContainerContext {
|
5 |
loaded: boolean;
|
@@ -21,7 +22,10 @@ if (!import.meta.env.SSR) {
|
|
21 |
webcontainer =
|
22 |
import.meta.hot?.data.webcontainer ??
|
23 |
Promise.resolve()
|
24 |
-
.then(() =>
|
|
|
|
|
|
|
25 |
.then((webcontainer) => {
|
26 |
webcontainerContext.loaded = true;
|
27 |
return webcontainer;
|
|
|
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;
|
|
|
22 |
webcontainer =
|
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) => {
|
30 |
webcontainerContext.loaded = true;
|
31 |
return webcontainer;
|
packages/bolt/app/routes/api.chat.ts
CHANGED
@@ -4,8 +4,13 @@ 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(
|
|
|
|
|
|
|
|
|
9 |
const { messages } = await request.json<{ messages: Messages }>();
|
10 |
|
11 |
const stream = new SwitchableStream();
|
|
|
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 |
+
import { handleWithAuth } from '~/lib/.server/login';
|
8 |
|
9 |
+
export async function action(args: ActionFunctionArgs) {
|
10 |
+
return handleWithAuth(args, chatAction);
|
11 |
+
}
|
12 |
+
|
13 |
+
async function chatAction({ context, request }: ActionFunctionArgs) {
|
14 |
const { messages } = await request.json<{ messages: Messages }>();
|
15 |
|
16 |
const stream = new SwitchableStream();
|
packages/bolt/app/routes/api.enhancer.ts
CHANGED
@@ -1,12 +1,17 @@
|
|
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 |
|
6 |
const encoder = new TextEncoder();
|
7 |
const decoder = new TextDecoder();
|
8 |
|
9 |
-
export async function action(
|
|
|
|
|
|
|
|
|
10 |
const { message } = await request.json<{ message: string }>();
|
11 |
|
12 |
try {
|
|
|
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 { handleWithAuth } from '~/lib/.server/login';
|
5 |
import { stripIndents } from '~/utils/stripIndent';
|
6 |
|
7 |
const encoder = new TextEncoder();
|
8 |
const decoder = new TextDecoder();
|
9 |
|
10 |
+
export async function action(args: ActionFunctionArgs) {
|
11 |
+
return handleWithAuth(args, enhancerAction);
|
12 |
+
}
|
13 |
+
|
14 |
+
async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
15 |
const { message } = await request.json<{ message: string }>();
|
16 |
|
17 |
try {
|
packages/bolt/app/routes/login.tsx
CHANGED
@@ -3,49 +3,96 @@ import {
|
|
3 |
redirect,
|
4 |
type ActionFunctionArgs,
|
5 |
type LoaderFunctionArgs,
|
6 |
-
|
7 |
} from '@remix-run/cloudflare';
|
8 |
-
import {
|
9 |
-
import {
|
10 |
-
import {
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
}
|
15 |
|
16 |
export async function loader({ request, context }: LoaderFunctionArgs) {
|
17 |
-
const authenticated = await isAuthenticated(request, context.cloudflare.env);
|
18 |
|
19 |
if (authenticated) {
|
20 |
-
return redirect('/');
|
21 |
}
|
22 |
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
|
26 |
-
export async function action({ request, context }: ActionFunctionArgs)
|
27 |
const formData = await request.formData();
|
28 |
-
const password = String(formData.get('password'));
|
29 |
|
30 |
-
const
|
|
|
|
|
|
|
31 |
|
32 |
-
|
33 |
-
errors.password = 'Please provide a password';
|
34 |
-
}
|
35 |
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
}
|
39 |
|
40 |
-
|
41 |
-
|
|
|
|
|
42 |
}
|
43 |
|
44 |
-
|
|
|
|
|
|
|
|
|
45 |
}
|
46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
export default function Login() {
|
48 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
return (
|
51 |
<div className="min-h-screen flex items-center justify-center">
|
@@ -53,38 +100,93 @@ export default function Login() {
|
|
53 |
<div>
|
54 |
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
55 |
</div>
|
56 |
-
|
57 |
-
|
58 |
-
<label htmlFor="password" className="sr-only">
|
59 |
-
Password
|
60 |
-
</label>
|
61 |
-
<input
|
62 |
-
id="password"
|
63 |
-
name="password"
|
64 |
-
type="password"
|
65 |
-
autoComplete="off"
|
66 |
-
data-1p-ignore
|
67 |
-
required
|
68 |
-
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none"
|
69 |
-
placeholder="Password"
|
70 |
-
/>
|
71 |
-
{actionData?.errors?.password ? (
|
72 |
-
<em className="flex items-center space-x-1.5 p-2 mt-2 bg-negative-200 text-negative-600 rounded-lg">
|
73 |
-
<div className="i-ph:x-circle text-xl"></div>
|
74 |
-
<span>{actionData?.errors.password}</span>
|
75 |
-
</em>
|
76 |
-
) : null}
|
77 |
-
</div>
|
78 |
-
<div>
|
79 |
-
<button
|
80 |
-
type="submit"
|
81 |
-
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"
|
82 |
-
>
|
83 |
-
Login
|
84 |
-
</button>
|
85 |
-
</div>
|
86 |
-
</Form>
|
87 |
</div>
|
88 |
</div>
|
89 |
);
|
90 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
redirect,
|
4 |
type ActionFunctionArgs,
|
5 |
type LoaderFunctionArgs,
|
6 |
+
redirectDocument,
|
7 |
} from '@remix-run/cloudflare';
|
8 |
+
import { useFetcher, useLoaderData } from '@remix-run/react';
|
9 |
+
import { auth, type AuthAPI } from '@webcontainer/api';
|
10 |
+
import { useEffect, useState } from 'react';
|
11 |
+
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
12 |
+
import { request as doRequest } from '~/lib/fetch';
|
13 |
+
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
14 |
+
import { logger } from '~/utils/logger';
|
15 |
|
16 |
export async function loader({ request, context }: LoaderFunctionArgs) {
|
17 |
+
const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env);
|
18 |
|
19 |
if (authenticated) {
|
20 |
+
return redirect('/', response);
|
21 |
}
|
22 |
|
23 |
+
const url = new URL(request.url);
|
24 |
+
|
25 |
+
return json(
|
26 |
+
{
|
27 |
+
redirected: url.searchParams.has('code') || url.searchParams.has('error'),
|
28 |
+
},
|
29 |
+
response,
|
30 |
+
);
|
31 |
}
|
32 |
|
33 |
+
export async function action({ request, context }: ActionFunctionArgs) {
|
34 |
const formData = await request.formData();
|
|
|
35 |
|
36 |
+
const payload = {
|
37 |
+
access: String(formData.get('access')),
|
38 |
+
refresh: String(formData.get('refresh')),
|
39 |
+
};
|
40 |
|
41 |
+
let response: Awaited<ReturnType<typeof doRequest>> | undefined;
|
|
|
|
|
42 |
|
43 |
+
try {
|
44 |
+
response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, {
|
45 |
+
headers: { authorization: `Bearer ${payload.access}` },
|
46 |
+
});
|
47 |
+
|
48 |
+
if (!response.ok) {
|
49 |
+
throw await response.json();
|
50 |
+
}
|
51 |
+
} catch (error) {
|
52 |
+
logger.warn('Authentication failure');
|
53 |
+
logger.warn(error);
|
54 |
+
|
55 |
+
return json({ error: 'invalid-token' as const }, { status: 401 });
|
56 |
}
|
57 |
|
58 |
+
const boltEnabled = validateAccessToken(payload.access);
|
59 |
+
|
60 |
+
if (!boltEnabled) {
|
61 |
+
return json({ error: 'bolt-access' as const }, { status: 401 });
|
62 |
}
|
63 |
|
64 |
+
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
65 |
+
|
66 |
+
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo });
|
67 |
+
|
68 |
+
return redirectDocument('/', init);
|
69 |
}
|
70 |
|
71 |
+
type LoginState =
|
72 |
+
| {
|
73 |
+
kind: 'error';
|
74 |
+
error: string;
|
75 |
+
description: string;
|
76 |
+
}
|
77 |
+
| { kind: 'pending' };
|
78 |
+
|
79 |
+
const ERRORS = {
|
80 |
+
'bolt-access': 'You do not have access to Bolt.',
|
81 |
+
'invalid-token': 'Authentication failed.',
|
82 |
+
};
|
83 |
+
|
84 |
export default function Login() {
|
85 |
+
const { redirected } = useLoaderData<typeof loader>();
|
86 |
+
|
87 |
+
useEffect(() => {
|
88 |
+
if (!import.meta.hot?.data.wcAuth) {
|
89 |
+
auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN });
|
90 |
+
}
|
91 |
+
|
92 |
+
if (import.meta.hot) {
|
93 |
+
import.meta.hot.data.wcAuth = true;
|
94 |
+
}
|
95 |
+
}, []);
|
96 |
|
97 |
return (
|
98 |
<div className="min-h-screen flex items-center justify-center">
|
|
|
100 |
<div>
|
101 |
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
102 |
</div>
|
103 |
+
|
104 |
+
{redirected ? 'Processing auth...' : <LoginForm />}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
</div>
|
106 |
</div>
|
107 |
);
|
108 |
}
|
109 |
+
|
110 |
+
function LoginForm() {
|
111 |
+
const [login, setLogin] = useState<LoginState | null>(null);
|
112 |
+
|
113 |
+
const fetcher = useFetcher<typeof action>();
|
114 |
+
|
115 |
+
useEffect(() => {
|
116 |
+
auth.logout({ ignoreRevokeError: true });
|
117 |
+
}, []);
|
118 |
+
|
119 |
+
useEffect(() => {
|
120 |
+
if (fetcher.data?.error) {
|
121 |
+
auth.logout({ ignoreRevokeError: true });
|
122 |
+
|
123 |
+
setLogin({
|
124 |
+
kind: 'error' as const,
|
125 |
+
...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] },
|
126 |
+
});
|
127 |
+
}
|
128 |
+
}, [fetcher.data]);
|
129 |
+
|
130 |
+
async function attemptLogin() {
|
131 |
+
startAuthFlow();
|
132 |
+
|
133 |
+
function startAuthFlow() {
|
134 |
+
auth.startAuthFlow({ popup: true });
|
135 |
+
|
136 |
+
Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => {
|
137 |
+
if (error) {
|
138 |
+
setLogin({ kind: 'error', ...error });
|
139 |
+
} else {
|
140 |
+
onTokens();
|
141 |
+
}
|
142 |
+
});
|
143 |
+
}
|
144 |
+
|
145 |
+
function onTokens() {
|
146 |
+
const tokens = auth.tokens()!;
|
147 |
+
|
148 |
+
fetcher.submit(tokens, {
|
149 |
+
method: 'POST',
|
150 |
+
});
|
151 |
+
|
152 |
+
setLogin({ kind: 'pending' });
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
return (
|
157 |
+
<>
|
158 |
+
<button
|
159 |
+
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"
|
160 |
+
onClick={attemptLogin}
|
161 |
+
disabled={login?.kind === 'pending'}
|
162 |
+
>
|
163 |
+
{login?.kind === 'pending' ? 'Authenticating...' : 'Continue with StackBlitz'}
|
164 |
+
</button>
|
165 |
+
|
166 |
+
{login?.kind === 'error' && (
|
167 |
+
<div>
|
168 |
+
<h2>
|
169 |
+
<code>{login.error}</code>
|
170 |
+
</h2>
|
171 |
+
<p>{login.description}</p>
|
172 |
+
</div>
|
173 |
+
)}
|
174 |
+
</>
|
175 |
+
);
|
176 |
+
}
|
177 |
+
|
178 |
+
interface AuthError {
|
179 |
+
error: string;
|
180 |
+
description: string;
|
181 |
+
}
|
182 |
+
|
183 |
+
function authEvent(auth: AuthAPI, event: 'logged-out'): Promise<void>;
|
184 |
+
function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise<AuthError>;
|
185 |
+
function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') {
|
186 |
+
return new Promise((resolve) => {
|
187 |
+
const unsubscribe = auth.on(event as any, (arg: any) => {
|
188 |
+
unsubscribe();
|
189 |
+
resolve(arg);
|
190 |
+
});
|
191 |
+
});
|
192 |
+
}
|
packages/bolt/app/routes/logout.tsx
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|
packages/bolt/app/utils/logger.ts
CHANGED
@@ -11,7 +11,7 @@ interface Logger {
|
|
11 |
setLevel: (level: DebugLevel) => void;
|
12 |
}
|
13 |
|
14 |
-
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? '
|
15 |
|
16 |
export const logger: Logger = {
|
17 |
trace: (...messages: any[]) => log('trace', undefined, messages),
|
|
|
11 |
setLevel: (level: DebugLevel) => void;
|
12 |
}
|
13 |
|
14 |
+
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
|
15 |
|
16 |
export const logger: Logger = {
|
17 |
trace: (...messages: any[]) => log('trace', undefined, messages),
|
packages/bolt/package.json
CHANGED
@@ -48,6 +48,7 @@
|
|
48 |
"framer-motion": "^11.2.12",
|
49 |
"isbot": "^4.1.0",
|
50 |
"istextorbinary": "^9.5.0",
|
|
|
51 |
"nanostores": "^0.10.3",
|
52 |
"react": "^18.2.0",
|
53 |
"react-dom": "^18.2.0",
|
@@ -64,9 +65,11 @@
|
|
64 |
"@cloudflare/workers-types": "^4.20240620.0",
|
65 |
"@remix-run/dev": "^2.10.0",
|
66 |
"@types/diff": "^5.2.1",
|
|
|
67 |
"@types/react": "^18.2.20",
|
68 |
"@types/react-dom": "^18.2.7",
|
69 |
"fast-glob": "^3.3.2",
|
|
|
70 |
"typescript": "^5.5.2",
|
71 |
"unified": "^11.0.5",
|
72 |
"unocss": "^0.61.3",
|
|
|
48 |
"framer-motion": "^11.2.12",
|
49 |
"isbot": "^4.1.0",
|
50 |
"istextorbinary": "^9.5.0",
|
51 |
+
"jsonwebtoken": "^9.0.2",
|
52 |
"nanostores": "^0.10.3",
|
53 |
"react": "^18.2.0",
|
54 |
"react-dom": "^18.2.0",
|
|
|
65 |
"@cloudflare/workers-types": "^4.20240620.0",
|
66 |
"@remix-run/dev": "^2.10.0",
|
67 |
"@types/diff": "^5.2.1",
|
68 |
+
"@types/jsonwebtoken": "^9.0.6",
|
69 |
"@types/react": "^18.2.20",
|
70 |
"@types/react-dom": "^18.2.7",
|
71 |
"fast-glob": "^3.3.2",
|
72 |
+
"node-fetch": "^3.3.2",
|
73 |
"typescript": "^5.5.2",
|
74 |
"unified": "^11.0.5",
|
75 |
"unocss": "^0.61.3",
|
pnpm-lock.yaml
CHANGED
@@ -131,6 +131,9 @@ importers:
|
|
131 |
istextorbinary:
|
132 |
specifier: ^9.5.0
|
133 |
version: 9.5.0
|
|
|
|
|
|
|
134 |
nanostores:
|
135 |
specifier: ^0.10.3
|
136 |
version: 0.10.3
|
@@ -174,6 +177,9 @@ importers:
|
|
174 |
'@types/diff':
|
175 |
specifier: ^5.2.1
|
176 |
version: 5.2.1
|
|
|
|
|
|
|
177 |
'@types/react':
|
178 |
specifier: ^18.2.20
|
179 |
version: 18.3.3
|
@@ -183,6 +189,9 @@ importers:
|
|
183 |
fast-glob:
|
184 |
specifier: ^3.3.2
|
185 |
version: 3.3.2
|
|
|
|
|
|
|
186 |
typescript:
|
187 |
specifier: ^5.5.2
|
188 |
version: 5.5.2
|
@@ -1484,6 +1493,9 @@ packages:
|
|
1484 |
'@types/[email protected]':
|
1485 |
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
1486 |
|
|
|
|
|
|
|
1487 |
'@types/[email protected]':
|
1488 |
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
|
1489 |
|
@@ -1949,6 +1961,9 @@ packages:
|
|
1949 |
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
1950 |
hasBin: true
|
1951 |
|
|
|
|
|
|
|
1952 | |
1953 |
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
1954 |
|
@@ -2234,6 +2249,10 @@ packages:
|
|
2234 |
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
2235 |
engines: {node: '>= 6'}
|
2236 |
|
|
|
|
|
|
|
|
|
2237 | |
2238 |
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
2239 |
|
@@ -2353,6 +2372,9 @@ packages:
|
|
2353 | |
2354 |
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
2355 |
|
|
|
|
|
|
|
2356 | |
2357 |
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
2358 |
engines: {node: '>=4'}
|
@@ -2615,6 +2637,10 @@ packages:
|
|
2615 | |
2616 |
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
2617 |
|
|
|
|
|
|
|
|
|
2618 | |
2619 |
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
2620 |
engines: {node: '>=16.0.0'}
|
@@ -2653,6 +2679,10 @@ packages:
|
|
2653 |
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
2654 |
engines: {node: '>=0.4.x'}
|
2655 |
|
|
|
|
|
|
|
|
|
2656 | |
2657 |
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
2658 |
engines: {node: '>= 0.6'}
|
@@ -3144,6 +3174,16 @@ packages:
|
|
3144 |
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
3145 |
engines: {'0': node >= 0.2.0}
|
3146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3147 | |
3148 |
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
3149 |
|
@@ -3190,9 +3230,24 @@ packages:
|
|
3190 | |
3191 |
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
3192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3193 | |
3194 |
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
3195 |
|
|
|
|
|
|
|
3196 | |
3197 |
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
|
3198 |
|
@@ -3202,6 +3257,9 @@ packages:
|
|
3202 | |
3203 |
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
|
3204 |
|
|
|
|
|
|
|
3205 | |
3206 |
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
3207 |
|
@@ -3687,9 +3745,17 @@ packages:
|
|
3687 |
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
3688 |
engines: {node: '>= 0.6'}
|
3689 |
|
|
|
|
|
|
|
|
|
3690 | |
3691 |
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
3692 |
|
|
|
|
|
|
|
|
|
3693 | |
3694 |
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
3695 |
engines: {node: '>= 6.13.0'}
|
@@ -4679,8 +4745,8 @@ packages:
|
|
4679 |
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
4680 |
engines: {node: '>=14.0'}
|
4681 |
|
4682 |
-
[email protected].
|
4683 |
-
resolution: {integrity: sha512-
|
4684 |
engines: {node: '>=18.17'}
|
4685 |
|
4686 | |
@@ -6189,7 +6255,7 @@ snapshots:
|
|
6189 |
cookie-signature: 1.2.1
|
6190 |
source-map-support: 0.5.21
|
6191 |
stream-slice: 0.1.2
|
6192 |
-
undici: 6.19.
|
6193 |
optionalDependencies:
|
6194 |
typescript: 5.5.2
|
6195 |
|
@@ -6201,7 +6267,7 @@ snapshots:
|
|
6201 |
cookie-signature: 1.2.1
|
6202 |
source-map-support: 0.5.21
|
6203 |
stream-slice: 0.1.2
|
6204 |
-
undici: 6.19.
|
6205 |
optionalDependencies:
|
6206 |
typescript: 5.5.2
|
6207 |
optional: true
|
@@ -6416,6 +6482,10 @@ snapshots:
|
|
6416 |
|
6417 |
'@types/[email protected]': {}
|
6418 |
|
|
|
|
|
|
|
|
|
6419 |
'@types/[email protected]':
|
6420 |
dependencies:
|
6421 |
'@types/unist': 2.0.10
|
@@ -7091,6 +7161,8 @@ snapshots:
|
|
7091 |
node-releases: 2.0.14
|
7092 |
update-browserslist-db: 1.0.16([email protected])
|
7093 |
|
|
|
|
|
7094 | |
7095 |
|
7096 | |
@@ -7396,6 +7468,8 @@ snapshots:
|
|
7396 |
|
7397 | |
7398 |
|
|
|
|
|
7399 | |
7400 |
|
7401 | |
@@ -7490,6 +7564,10 @@ snapshots:
|
|
7490 |
|
7491 | |
7492 |
|
|
|
|
|
|
|
|
|
7493 | |
7494 |
dependencies:
|
7495 |
version-range: 4.14.0
|
@@ -7876,6 +7954,11 @@ snapshots:
|
|
7876 |
dependencies:
|
7877 |
format: 0.2.2
|
7878 |
|
|
|
|
|
|
|
|
|
|
|
7879 | |
7880 |
dependencies:
|
7881 |
flat-cache: 4.0.1
|
@@ -7925,6 +8008,10 @@ snapshots:
|
|
7925 |
|
7926 | |
7927 |
|
|
|
|
|
|
|
|
|
7928 | |
7929 |
|
7930 | |
@@ -8406,6 +8493,30 @@ snapshots:
|
|
8406 |
|
8407 | |
8408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8409 | |
8410 |
dependencies:
|
8411 |
json-buffer: 3.0.1
|
@@ -8444,14 +8555,26 @@ snapshots:
|
|
8444 |
|
8445 | |
8446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8447 | |
8448 |
|
|
|
|
|
8449 | |
8450 |
|
8451 | |
8452 |
|
8453 | |
8454 |
|
|
|
|
|
8455 | |
8456 |
|
8457 | |
@@ -9317,8 +9440,16 @@ snapshots:
|
|
9317 |
|
9318 | |
9319 |
|
|
|
|
|
9320 | |
9321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
9322 | |
9323 |
|
9324 | |
@@ -10393,7 +10524,7 @@ snapshots:
|
|
10393 |
dependencies:
|
10394 |
'@fastify/busboy': 2.1.1
|
10395 |
|
10396 |
-
[email protected].
|
10397 |
|
10398 | |
10399 |
dependencies:
|
|
|
131 |
istextorbinary:
|
132 |
specifier: ^9.5.0
|
133 |
version: 9.5.0
|
134 |
+
jsonwebtoken:
|
135 |
+
specifier: ^9.0.2
|
136 |
+
version: 9.0.2
|
137 |
nanostores:
|
138 |
specifier: ^0.10.3
|
139 |
version: 0.10.3
|
|
|
177 |
'@types/diff':
|
178 |
specifier: ^5.2.1
|
179 |
version: 5.2.1
|
180 |
+
'@types/jsonwebtoken':
|
181 |
+
specifier: ^9.0.6
|
182 |
+
version: 9.0.6
|
183 |
'@types/react':
|
184 |
specifier: ^18.2.20
|
185 |
version: 18.3.3
|
|
|
189 |
fast-glob:
|
190 |
specifier: ^3.3.2
|
191 |
version: 3.3.2
|
192 |
+
node-fetch:
|
193 |
+
specifier: ^3.3.2
|
194 |
+
version: 3.3.2
|
195 |
typescript:
|
196 |
specifier: ^5.5.2
|
197 |
version: 5.5.2
|
|
|
1493 |
'@types/[email protected]':
|
1494 |
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
1495 |
|
1496 |
+
'@types/[email protected]':
|
1497 |
+
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
1498 |
+
|
1499 |
'@types/[email protected]':
|
1500 |
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
|
1501 |
|
|
|
1961 |
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
1962 |
hasBin: true
|
1963 |
|
1964 | |
1965 |
+
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
1966 |
+
|
1967 | |
1968 |
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
1969 |
|
|
|
2249 |
resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==}
|
2250 |
engines: {node: '>= 6'}
|
2251 |
|
2252 | |
2253 |
+
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
2254 |
+
engines: {node: '>= 12'}
|
2255 |
+
|
2256 | |
2257 |
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
2258 |
|
|
|
2372 | |
2373 |
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
2374 |
|
2375 | |
2376 |
+
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
2377 |
+
|
2378 | |
2379 |
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
2380 |
engines: {node: '>=4'}
|
|
|
2637 | |
2638 |
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
|
2639 |
|
2640 | |
2641 |
+
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
2642 |
+
engines: {node: ^12.20 || >= 14.13}
|
2643 |
+
|
2644 | |
2645 |
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
2646 |
engines: {node: '>=16.0.0'}
|
|
|
2679 |
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
2680 |
engines: {node: '>=0.4.x'}
|
2681 |
|
2682 | |
2683 |
+
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
2684 |
+
engines: {node: '>=12.20.0'}
|
2685 |
+
|
2686 | |
2687 |
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
2688 |
engines: {node: '>= 0.6'}
|
|
|
3174 |
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
3175 |
engines: {'0': node >= 0.2.0}
|
3176 |
|
3177 | |
3178 |
+
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
3179 |
+
engines: {node: '>=12', npm: '>=6'}
|
3180 |
+
|
3181 | |
3182 |
+
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
3183 |
+
|
3184 | |
3185 |
+
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
3186 |
+
|
3187 | |
3188 |
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
3189 |
|
|
|
3230 | |
3231 |
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
3232 |
|
3233 | |
3234 |
+
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
3235 |
+
|
3236 | |
3237 |
+
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
3238 |
+
|
3239 | |
3240 |
+
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
3241 |
+
|
3242 | |
3243 |
+
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
3244 |
+
|
3245 | |
3246 |
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
3247 |
|
3248 | |
3249 |
+
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
3250 |
+
|
3251 | |
3252 |
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
|
3253 |
|
|
|
3257 | |
3258 |
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
|
3259 |
|
3260 | |
3261 |
+
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
3262 |
+
|
3263 | |
3264 |
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
3265 |
|
|
|
3745 |
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
3746 |
engines: {node: '>= 0.6'}
|
3747 |
|
3748 | |
3749 |
+
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
3750 |
+
engines: {node: '>=10.5.0'}
|
3751 |
+
|
3752 | |
3753 |
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
3754 |
|
3755 | |
3756 |
+
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
3757 |
+
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
3758 |
+
|
3759 | |
3760 |
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
3761 |
engines: {node: '>= 6.13.0'}
|
|
|
4745 |
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
4746 |
engines: {node: '>=14.0'}
|
4747 |
|
4748 |
+
[email protected].4:
|
4749 |
+
resolution: {integrity: sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g==}
|
4750 |
engines: {node: '>=18.17'}
|
4751 |
|
4752 | |
|
|
6255 |
cookie-signature: 1.2.1
|
6256 |
source-map-support: 0.5.21
|
6257 |
stream-slice: 0.1.2
|
6258 |
+
undici: 6.19.4
|
6259 |
optionalDependencies:
|
6260 |
typescript: 5.5.2
|
6261 |
|
|
|
6267 |
cookie-signature: 1.2.1
|
6268 |
source-map-support: 0.5.21
|
6269 |
stream-slice: 0.1.2
|
6270 |
+
undici: 6.19.4
|
6271 |
optionalDependencies:
|
6272 |
typescript: 5.5.2
|
6273 |
optional: true
|
|
|
6482 |
|
6483 |
'@types/[email protected]': {}
|
6484 |
|
6485 |
+
'@types/[email protected]':
|
6486 |
+
dependencies:
|
6487 |
+
'@types/node': 20.14.9
|
6488 |
+
|
6489 |
'@types/[email protected]':
|
6490 |
dependencies:
|
6491 |
'@types/unist': 2.0.10
|
|
|
7161 |
node-releases: 2.0.14
|
7162 |
update-browserslist-db: 1.0.16([email protected])
|
7163 |
|
7164 |
+
[email protected]: {}
|
7165 |
+
|
7166 | |
7167 |
|
7168 | |
|
|
7468 |
|
7469 | |
7470 |
|
7471 |
+
[email protected]: {}
|
7472 |
+
|
7473 | |
7474 |
|
7475 | |
|
|
7564 |
|
7565 | |
7566 |
|
7567 | |
7568 |
+
dependencies:
|
7569 |
+
safe-buffer: 5.2.1
|
7570 |
+
|
7571 | |
7572 |
dependencies:
|
7573 |
version-range: 4.14.0
|
|
|
7954 |
dependencies:
|
7955 |
format: 0.2.2
|
7956 |
|
7957 | |
7958 |
+
dependencies:
|
7959 |
+
node-domexception: 1.0.0
|
7960 |
+
web-streams-polyfill: 3.3.3
|
7961 |
+
|
7962 | |
7963 |
dependencies:
|
7964 |
flat-cache: 4.0.1
|
|
|
8008 |
|
8009 | |
8010 |
|
8011 | |
8012 |
+
dependencies:
|
8013 |
+
fetch-blob: 3.2.0
|
8014 |
+
|
8015 | |
8016 |
|
8017 | |
|
|
8493 |
|
8494 | |
8495 |
|
8496 | |
8497 |
+
dependencies:
|
8498 |
+
jws: 3.2.2
|
8499 |
+
lodash.includes: 4.3.0
|
8500 |
+
lodash.isboolean: 3.0.3
|
8501 |
+
lodash.isinteger: 4.0.4
|
8502 |
+
lodash.isnumber: 3.0.3
|
8503 |
+
lodash.isplainobject: 4.0.6
|
8504 |
+
lodash.isstring: 4.0.1
|
8505 |
+
lodash.once: 4.1.1
|
8506 |
+
ms: 2.1.3
|
8507 |
+
semver: 7.6.2
|
8508 |
+
|
8509 | |
8510 |
+
dependencies:
|
8511 |
+
buffer-equal-constant-time: 1.0.1
|
8512 |
+
ecdsa-sig-formatter: 1.0.11
|
8513 |
+
safe-buffer: 5.2.1
|
8514 |
+
|
8515 | |
8516 |
+
dependencies:
|
8517 |
+
jwa: 1.4.1
|
8518 |
+
safe-buffer: 5.2.1
|
8519 |
+
|
8520 | |
8521 |
dependencies:
|
8522 |
json-buffer: 3.0.1
|
|
|
8555 |
|
8556 | |
8557 |
|
8558 |
+
[email protected]: {}
|
8559 |
+
|
8560 |
+
[email protected]: {}
|
8561 |
+
|
8562 |
+
[email protected]: {}
|
8563 |
+
|
8564 |
+
[email protected]: {}
|
8565 |
+
|
8566 | |
8567 |
|
8568 |
+
[email protected]: {}
|
8569 |
+
|
8570 | |
8571 |
|
8572 | |
8573 |
|
8574 | |
8575 |
|
8576 |
+
[email protected]: {}
|
8577 |
+
|
8578 | |
8579 |
|
8580 | |
|
|
9440 |
|
9441 | |
9442 |
|
9443 |
+
[email protected]: {}
|
9444 |
+
|
9445 | |
9446 |
|
9447 | |
9448 |
+
dependencies:
|
9449 |
+
data-uri-to-buffer: 4.0.1
|
9450 |
+
fetch-blob: 3.2.0
|
9451 |
+
formdata-polyfill: 4.0.10
|
9452 |
+
|
9453 | |
9454 |
|
9455 | |
|
|
10524 |
dependencies:
|
10525 |
'@fastify/busboy': 2.1.1
|
10526 |
|
10527 |
+
[email protected].4: {}
|
10528 |
|
10529 | |
10530 |
dependencies:
|