Roberto Vidal commited on
Commit
7ebc805
·
unverified ·
1 Parent(s): b8a197e

feat: oauth-based login (#7)

Browse files
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 test the login locally you need to add the following variables:
40
-
41
  ```
42
- SESSION_SECRET=XXX
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
- export async function handleAuthRequest({ request, context }: LoaderFunctionArgs, body: object = {}) {
12
- const authenticated = await isAuthenticated(request, context.cloudflare.env);
13
 
14
- if (import.meta.env.DEV || authenticated) {
15
- return json(body);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
- return redirect('/login');
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 { env } from 'node:process';
 
 
 
3
 
4
- const USER_SESSION_KEY = 'userId';
5
 
6
- function createSessionStorage(cloudflareEnv: Env) {
7
- return createCookieSessionStorage({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  cookie: {
9
  name: '__session',
10
  httpOnly: true,
11
  path: '/',
12
- sameSite: 'lax',
13
- secrets: [env.SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
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 async function isAuthenticated(request: Request, env: Env) {
37
- const { session } = await getSession(request, env);
38
- const userId = session.get(USER_SESSION_KEY);
 
39
 
40
- return !!userId;
41
  }
42
 
43
- export async function createUserSession(request: Request, env: Env): Promise<ResponseInit> {
44
- const { session, sessionStorage } = await getSession(request, env);
 
45
 
46
- session.set(USER_SESSION_KEY, 'anonymous_user');
 
47
 
48
- return {
49
- headers: {
50
- 'Set-Cookie': await sessionStorage.commitSession(session, {
51
- maxAge: 60 * 60 * 24 * 7, // 7 days,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(() => WebContainer.boot({ workdirName: WORK_DIR_NAME }))
 
 
 
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({ context, request }: ActionFunctionArgs) {
 
 
 
 
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({ context, request }: ActionFunctionArgs) {
 
 
 
 
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
- type TypedResponse,
7
  } from '@remix-run/cloudflare';
8
- import { Form, useActionData } from '@remix-run/react';
9
- import { verifyPassword } from '~/lib/.server/login';
10
- import { createUserSession, isAuthenticated } from '~/lib/.server/sessions';
11
-
12
- interface Errors {
13
- password?: string;
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
- return json({});
 
 
 
 
 
 
 
24
  }
25
 
26
- export async function action({ request, context }: ActionFunctionArgs): Promise<TypedResponse<{ errors?: Errors }>> {
27
  const formData = await request.formData();
28
- const password = String(formData.get('password'));
29
 
30
- const errors: Errors = {};
 
 
 
31
 
32
- if (!password) {
33
- errors.password = 'Please provide a password';
34
- }
35
 
36
- if (!verifyPassword(password, context.cloudflare.env)) {
37
- errors.password = 'Invalid password';
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
- if (Object.keys(errors).length > 0) {
41
- return json({ errors });
 
 
42
  }
43
 
44
- return redirect('/', await createUserSession(request, context.cloudflare.env));
 
 
 
 
45
  }
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  export default function Login() {
48
- const actionData = useActionData<typeof action>();
 
 
 
 
 
 
 
 
 
 
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
- <Form className="mt-8 space-y-6" method="post" noValidate>
57
- <div>
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 ?? 'warn';
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
4683
- resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==}
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.2
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.2
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
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
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
7165
+
7166
7167
 
7168
 
7468
 
7469
7470
 
7471
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
8559
+
8560
8561
+
8562
8563
+
8564
8565
+
8566
8567
 
8568
8569
+
8570
8571
 
8572
8573
 
8574
8575
 
8576
8577
+
8578
8579
 
8580
 
9440
 
9441
9442
 
9443
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
10528
 
10529
10530
  dependencies: