feat: refresh token when access token has expired
This commit is contained in:
parent
52f2c969cc
commit
08a41e5e2b
13
.env.example
13
.env.example
@ -1,7 +1,12 @@
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_URL=
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
OAUTH_ISSUER=
|
||||
OAUTH_AUTHORIZATION_URL=
|
||||
OAUTH_TOKEN_URL=
|
||||
OAUTH_USERINFO_URL=
|
||||
OAUTH_JWKS_ENDPOINT=
|
@ -9,11 +9,14 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.0.3",
|
||||
"next-auth": "^4.24.10",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106"
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
|
@ -33,7 +33,7 @@ const Auth = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>Welcome, {session.user.name || "User"}!</p>
|
||||
<p>Welcome, {session.user?.name || "User"}!</p>
|
||||
<button
|
||||
style={{
|
||||
padding: "10px 20px",
|
||||
|
@ -1,59 +1,33 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { axiosInstance } from "@/lib/axios";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FetchWithSession = () => {
|
||||
const { data: session, status } = useSession();
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [data, setData] = useState<{ message: string } | null>(null);
|
||||
const [error, setError] = useState<Error | AxiosError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (status === "authenticated" && session) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/ping`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
await axiosInstance
|
||||
.get<{
|
||||
message: string;
|
||||
}>(`/ping`)
|
||||
.then((response) => setData(response.data))
|
||||
.catch((err: Error | AxiosError) => {
|
||||
if (axios.isAxiosError(err)) setError(err.response?.data);
|
||||
else setError(err);
|
||||
});
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [status, session]);
|
||||
|
||||
if (status === "loading") {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
return <div>You must log in to access this data.</div>;
|
||||
}
|
||||
setInterval(() => {
|
||||
fetchData();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && <p>Error: {error}</p>}
|
||||
{data ? (
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
) : (
|
||||
<p>Fetching data...</p>
|
||||
)}
|
||||
{error && <p>Error: {error.message}</p>}
|
||||
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import { AuthOptions, Session } from "next-auth";
|
||||
|
||||
import { JWT } from "next-auth/jwt";
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
providers: [
|
||||
@ -9,11 +11,10 @@ export const authOptions: AuthOptions = {
|
||||
type: "oauth",
|
||||
clientId: process.env.OAUTH_CLIENT_ID,
|
||||
clientSecret: process.env.OAUTH_CLIENT_SECRET,
|
||||
wellKnown: process.env.OAUTH_WELL_KNOWN,
|
||||
authorization: {
|
||||
url: process.env.OAUTH_AUTHORIZATION_URL,
|
||||
params: {
|
||||
scope: "openid email profile",
|
||||
scope: "openid profile offline_access",
|
||||
response_type: "code",
|
||||
},
|
||||
},
|
||||
@ -27,35 +28,73 @@ export const authOptions: AuthOptions = {
|
||||
return {
|
||||
id: profile.sub || profile.id,
|
||||
name:
|
||||
profile.name || profile.preferred_username ||
|
||||
`${profile.given_name} ${profile.family_name}`
|
||||
profile.name ||
|
||||
profile.preferred_username ||
|
||||
`${profile.given_name} ${profile.family_name}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, account, user }) {
|
||||
if (account) {
|
||||
if (account && user) {
|
||||
token.accessToken = account.access_token;
|
||||
token.accessTokenExpires = moment(account.expires_at).subtract(5, "s");
|
||||
token.refreshToken = account.refresh_token;
|
||||
token.expiresAt = Date.now() + account.expires_in * 1000000;
|
||||
token.user = user;
|
||||
return token;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
token.userId = user.id;
|
||||
if (moment().isBefore(moment(token.accessTokenExpires))) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return token;
|
||||
return refreshAccessToken(token);
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token) {
|
||||
session.user.id = token.userId;
|
||||
session.user = token.user;
|
||||
session.accessToken = token.accessToken;
|
||||
session.refreshToken = token.refreshToken;
|
||||
session.expiresAt = token.expiresAt;
|
||||
session.accessTokenExpires = token.accessTokenExpires;
|
||||
session.error = token.error;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
||||
const response = await axios.post<{
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
error_description?: string;
|
||||
}>(
|
||||
process.env.OAUTH_TOKEN_URL,
|
||||
{
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: token.refreshToken,
|
||||
client_id: process.env.OAUTH_CLIENT_ID,
|
||||
client_secret: process.env.OAUTH_CLIENT_SECRET,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(
|
||||
response.data.error_description || "Failed to refresh access token"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: response.data.access_token,
|
||||
accessTokenExpires: moment().add(response.data.expires_in, "seconds").subtract(5, "s"),
|
||||
refreshToken: response.data.refresh_token,
|
||||
};
|
||||
};
|
||||
|
28
src/lib/axios.ts
Normal file
28
src/lib/axios.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import axios from "axios";
|
||||
import moment, { Moment } from "moment";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
let cachedAccessToken: string | null = null;
|
||||
let tokenExpirationAt: Moment | null = null;
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(async (config) => {
|
||||
if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) {
|
||||
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
||||
return config;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
throw new Error("User is not authenticated");
|
||||
}
|
||||
|
||||
cachedAccessToken = session.accessToken;
|
||||
tokenExpirationAt = moment(session.accessTokenExpires);
|
||||
|
||||
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
|
||||
return config;
|
||||
});
|
32
src/types/next-auth.d.ts
vendored
32
src/types/next-auth.d.ts
vendored
@ -1,24 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Moment } from "moment";
|
||||
import NextAuth, { DefaultSession } from "next-auth";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
sub: string;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
}
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session extends DefaultSession {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
user: {
|
||||
id: string;
|
||||
sub: string;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
};
|
||||
accessTokenExpires: Moment;
|
||||
error?: Error;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface Account {
|
||||
expires_in: number;
|
||||
expires_at: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
@ -27,8 +31,12 @@ declare module "next-auth" {
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
accessToken: string;
|
||||
accessTokenExpires: Moment;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
userId: string;
|
||||
error?: Error;
|
||||
user: User | AdapterUser;
|
||||
iat: number;
|
||||
exp: number;
|
||||
jti: string;
|
||||
}
|
||||
}
|
||||
|
88
yarn.lock
88
yarn.lock
@ -635,6 +635,11 @@ ast-types-flow@^0.0.8:
|
||||
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6"
|
||||
integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
available-typed-arrays@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
||||
@ -647,6 +652,15 @@ axe-core@^4.10.0:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
|
||||
integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
|
||||
|
||||
axios@^1.7.9:
|
||||
version "1.7.9"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
||||
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axobject-query@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee"
|
||||
@ -778,6 +792,13 @@ color@^4.2.3:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
@ -881,6 +902,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
detect-libc@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||
@ -1358,6 +1384,11 @@ flatted@^3.2.9:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27"
|
||||
integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||
@ -1373,6 +1404,15 @@ foreground-child@^3.1.0:
|
||||
cross-spawn "^7.0.0"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
@ -2013,6 +2053,18 @@ micromatch@^4.0.4, micromatch@^4.0.8:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
@ -2037,6 +2089,11 @@ minimist@^1.2.0, minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
moment@^2.30.1:
|
||||
version "2.30.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
|
||||
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
|
||||
|
||||
ms@^2.1.1, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
@ -2387,6 +2444,11 @@ prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
@ -2637,16 +2699,8 @@ streamsearch@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||
name string-width-cjs
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -2727,14 +2781,7 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -3068,3 +3115,8 @@ yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zustand@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca"
|
||||
integrity sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==
|
||||
|
Loading…
Reference in New Issue
Block a user