From 003a2d88abbb4c17dfdde5cadda31137f5ad17af Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 24 Jun 2025 22:25:12 -0500 Subject: [PATCH] feat: remove openauth oauth utils from api-client --- packages/api-client/package.json | 1 - packages/api-client/src/auth-client.ts | 738 ------------------------- packages/api-client/src/auth-subjects | 5 - packages/api-client/src/errors.ts | 120 ---- packages/api-client/src/pkce.ts | 41 -- packages/api-client/src/subject.ts | 130 ----- pnpm-lock.yaml | 11 - 7 files changed, 1046 deletions(-) delete mode 100644 packages/api-client/src/auth-client.ts delete mode 100644 packages/api-client/src/auth-subjects delete mode 100644 packages/api-client/src/errors.ts delete mode 100644 packages/api-client/src/pkce.ts delete mode 100644 packages/api-client/src/subject.ts diff --git a/packages/api-client/package.json b/packages/api-client/package.json index e36b2eff..59e35e07 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -26,7 +26,6 @@ "@agentic/platform-core": "workspace:*", "@agentic/platform-types": "workspace:*", "@standard-schema/spec": "catalog:", - "jose": "catalog:", "ky": "catalog:", "type-fest": "catalog:" }, diff --git a/packages/api-client/src/auth-client.ts b/packages/api-client/src/auth-client.ts deleted file mode 100644 index 7daad114..00000000 --- a/packages/api-client/src/auth-client.ts +++ /dev/null @@ -1,738 +0,0 @@ -import type { StandardSchemaV1 } from '@standard-schema/spec' -import { - createLocalJWKSet, - decodeJwt, - errors, - type JSONWebKeySet, - jwtVerify -} from 'jose' - -import type { SubjectSchema } from './subject' -import { - InvalidAccessTokenError, - InvalidAuthorizationCodeError, - InvalidRefreshTokenError, - InvalidSubjectError -} from './errors' -import { generatePKCE } from './pkce' - -/** - * The well-known information for an OAuth 2.0 authorization server. - * @internal - */ -export interface WellKnown { - /** - * The URI to the JWKS endpoint. - */ - jwks_uri: string - - /** - * The URI to the token endpoint. - */ - token_endpoint: string - - /** - * The URI to the authorization endpoint. - */ - authorization_endpoint: string -} - -/** - * The tokens returned by the auth server. - */ -export interface Tokens { - /** - * The access token. - */ - access: string - - /** - * The refresh token. - */ - refresh: string - - /** - * The number of seconds until the access token expires. - */ - expiresIn: number -} - -interface ResponseLike { - json(): Promise - ok: Response['ok'] -} -type FetchLike = (...args: any[]) => Promise - -/** - * The challenge that you can use to verify the code. - */ -export type Challenge = { - /** - * The state that was sent to the redirect URI. - */ - state: string - - /** - * The verifier that was sent to the redirect URI. - */ - verifier?: string -} - -/** - * Configure the client. - */ -export interface AuthClientInput { - /** - * The client ID. This is just a string to identify your app. - * - * If you have a web app and a mobile app, you want to use different client IDs both. - * - * @example - * ```ts - * { - * clientId: "my-client" - * } - * ``` - */ - clientId: string - - /** - * The URL of your OpenAuth server. - * - * @example - * ```ts - * { - * issuer: "https://auth.myserver.com" - * } - * ``` - */ - issuer: string - - /** - * Optionally, override the internally used fetch function. - * - * This is useful if you are using a polyfilled fetch function in your application and you - * want the client to use it too. - */ - fetch?: FetchLike -} - -export interface AuthorizeOptions { - /** - * Enable the PKCE flow. This is for SPA apps. - * - * ```ts - * { - * pkce: true - * } - * ``` - * - * @default false - */ - pkce?: boolean - - /** - * The provider you want to use for the OAuth flow. - * - * ```ts - * { - * provider: "google" - * } - * ``` - * - * If no provider is specified, the user is directed to a page where they can select from the - * list of configured providers. - * - * If there's only one provider configured, the user will be redirected to that. - */ - provider?: string -} - -export interface AuthorizeResult { - /** - * The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps. - * - * This is an object that you _stringify_ and store it in session storage. - * - * ```ts - * sessionStorage.setItem("challenge", JSON.stringify(challenge)) - * ``` - */ - challenge: Challenge - - /** - * The URL to redirect the user to. This starts the OAuth flow. - * - * For example, for SPA apps. - * - * ```ts - * location.href = url - * ``` - */ - url: string -} - -/** - * Returned when the exchange is successful. - */ -export interface ExchangeSuccess { - /** - * This is always `false` when the exchange is successful. - */ - err: false - - /** - * The access and refresh tokens. - */ - tokens: Tokens -} - -/** - * Returned when the exchange fails. - */ -export interface ExchangeError { - /** - * The type of error that occurred. You can handle this by checking the type. - * - * @example - * ```ts - * import { InvalidAuthorizationCodeError } from "@agentic/api-client/error" - * - * console.log(err instanceof InvalidAuthorizationCodeError) - *``` - */ - err: InvalidAuthorizationCodeError -} - -export interface RefreshOptions { - /** - * Optionally, pass in the access token. - */ - access?: string -} - -/** - * Returned when the refresh is successful. - */ -export interface RefreshSuccess { - /** - * This is always `false` when the refresh is successful. - */ - err: false - - /** - * Returns the refreshed tokens only if they've been refreshed. - * - * If they are still valid, this will be `undefined`. - */ - tokens?: Tokens -} - -/** - * Returned when the refresh fails. - */ -export interface RefreshError { - /** - * The type of error that occurred. You can handle this by checking the type. - * - * @example - * ```ts - * import { InvalidRefreshTokenError } from "@agentic/api-client/error" - * - * console.log(err instanceof InvalidRefreshTokenError) - *``` - */ - err: InvalidRefreshTokenError | InvalidAccessTokenError -} - -export interface VerifyOptions { - /** - * Optionally, pass in the refresh token. - * - * If passed in, this will automatically refresh the access token if it has expired. - */ - refresh?: string - - /** - * @internal - */ - issuer?: string - - /** - * @internal - */ - audience?: string - - /** - * Optionally, override the internally used fetch function. - * - * This is useful if you are using a polyfilled fetch function in your application and you - * want the client to use it too. - */ - fetch?: FetchLike -} - -export interface VerifyResult { - /** - * This is always `undefined` when the verify is successful. - */ - err?: undefined - - /** - * Returns the refreshed tokens only if they’ve been refreshed. - * - * If they are still valid, this will be undefined. - */ - tokens?: Tokens - - /** - * @internal - */ - aud: string - - /** - * The decoded subjects from the access token. - * - * Has the same shape as the subjects you defined when creating the issuer. - */ - subject: { - [type in keyof T]: { - type: type - properties: StandardSchemaV1.InferOutput - } - }[keyof T] -} - -/** - * Returned when the verify call fails. - */ -export interface VerifyError { - /** - * The type of error that occurred. You can handle this by checking the type. - * - * @example - * ```ts - * import { InvalidRefreshTokenError } from "@agentic/api-client/error" - * - * console.log(err instanceof InvalidRefreshTokenError) - *``` - */ - err: InvalidRefreshTokenError | InvalidAccessTokenError -} - -/** - * An instance of the OpenAuth client contains the following methods. - */ -export interface AuthClient { - /** - * Start the autorization flow. For example, in SSR sites. - * - * ```ts - * const { url } = await client.authorize(, "code") - * ``` - * - * This takes a redirect URI and the type of flow you want to use. The redirect URI is the - * location where the user will be redirected to after the flow is complete. - * - * Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more - * secure. - * - * :::tip - * This returns a URL to redirect the user to. This starts the OAuth flow. - * ::: - * - * This returns a URL to the auth server. You can redirect the user to the URL to start the - * OAuth flow. - * - * For SPA apps, we recommend using the PKCE flow. - * - * ```ts {4} - * const { challenge, url } = await client.authorize( - * , - * "code", - * { pkce: true } - * ) - * ``` - * - * This returns a redirect URL and a challenge that you need to use later to verify the code. - */ - authorize( - redirectUri: string, - response: 'code' | 'token', - opts?: AuthorizeOptions - ): Promise - - /** - * Exchange the code for access and refresh tokens. - * - * ```ts - * const exchanged = await client.exchange(, ) - * ``` - * - * You call this after the user has been redirected back to your app after the OAuth flow. - * - * :::tip - * For SSR sites, the code is returned in the query parameter. - * ::: - * - * So the code comes from the query parameter in the redirect URI. The redirect URI here is - * the one that you passed in to the `authorize` call when starting the flow. - * - * :::tip - * For SPA sites, the code is returned through the URL hash. - * ::: - * - * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL - * hash. - * - * ```ts {4} - * const exchanged = await client.exchange( - * , - * , - * - * ) - * ``` - * - * You also need to pass in the previously stored challenge verifier. - * - * This method returns the access and refresh tokens. Or if it fails, it returns an error that - * you can handle depending on the error. - * - * ```ts - * import { InvalidAuthorizationCodeError } from "@agentic/api-client/error" - * - * if (exchanged.err) { - * if (exchanged.err instanceof InvalidAuthorizationCodeError) { - * // handle invalid code error - * } - * else { - * // handle other errors - * } - * } - * - * const { access, refresh } = exchanged.tokens - * ``` - */ - exchange( - code: string, - redirectUri: string, - verifier?: string - ): Promise - - /** - * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the - * session, without logging the user out. - * - * ```ts - * const next = await client.refresh() - * ``` - * - * Can optionally take the access token as well. If passed in, this will skip the refresh - * if the access token is still valid. - * - * ```ts - * const next = await client.refresh(, { access: }) - * ``` - * - * This returns the refreshed tokens only if they've been refreshed. - * - * ```ts - * if (!next.err) { - * // tokens are still valid - * } - * if (next.tokens) { - * const { access, refresh } = next.tokens - * } - * ``` - * - * Or if it fails, it returns an error that you can handle depending on the error. - * - * ```ts - * import { InvalidRefreshTokenError } from "@agentic/api-client/error" - * - * if (next.err) { - * if (next.err instanceof InvalidRefreshTokenError) { - * // handle invalid refresh token error - * } - * else { - * // handle other errors - * } - * } - * ``` - */ - refresh( - refresh: string, - opts?: RefreshOptions - ): Promise - - /** - * Verify the token in the incoming request. - * - * This is typically used for SSR sites where the token is stored in an HTTP only cookie. And - * is passed to the server on every request. - * - * ```ts - * const verified = await client.verify(, ) - * ``` - * - * This takes the subjects that you had previously defined when creating the issuer. - * - * :::tip - * If the refresh token is passed in, it'll automatically refresh the access token. - * ::: - * - * This can optionally take the refresh token as well. If passed in, it'll automatically - * refresh the access token if it has expired. - * - * ```ts - * const verified = await client.verify(, , { refresh: }) - * ``` - * - * This returns the decoded subjects from the access token. And the tokens if they've been - * refreshed. - * - * ```ts - * // based on the subjects you defined earlier - * console.log(verified.subject.properties.userID) - * - * if (verified.tokens) { - * const { access, refresh } = verified.tokens - * } - * ``` - * - * Or if it fails, it returns an error that you can handle depending on the error. - * - * ```ts - * import { InvalidRefreshTokenError } from "@agentic/api-client/error" - * - * if (verified.err) { - * if (verified.err instanceof InvalidRefreshTokenError) { - * // handle invalid refresh token error - * } - * else { - * // handle other errors - * } - * } - * ``` - */ - verify( - subjects: T, - token: string, - options?: VerifyOptions - ): Promise | VerifyError> -} - -/** - * Create an OpenAuth client. - * - * @param input - Configure the client. - */ -export function createAuthClient(input: AuthClientInput): AuthClient { - const issuer = input.issuer - if (!issuer) { - throw new Error('No issuer') - } - - const jwksCache = new Map>() - const issuerCache = new Map() - const f = input.fetch ?? fetch - - async function getIssuer() { - const cached = issuerCache.get(issuer!) - if (cached) return cached - - const wellKnown = (await f( - `${issuer}/.well-known/oauth-authorization-server` - ).then((r) => r.json())) as WellKnown - issuerCache.set(issuer!, wellKnown) - - return wellKnown - } - - async function getJWKS() { - const wk = await getIssuer() - - const cached = jwksCache.get(issuer!) - if (cached) return cached - - const keyset = (await f(wk.jwks_uri).then((r) => r.json())) as JSONWebKeySet - const result = createLocalJWKSet(keyset) - jwksCache.set(issuer!, result) - - return result - } - - const authClient = { - async authorize( - redirectUri: string, - response: 'code' | 'token', - opts?: AuthorizeOptions - ) { - const result = new URL(issuer + '/authorize') - const challenge: Challenge = { state: crypto.randomUUID() } - - result.searchParams.set('client_id', input.clientId) - result.searchParams.set('redirect_uri', redirectUri) - result.searchParams.set('response_type', response) - result.searchParams.set('state', challenge.state) - - if (opts?.provider) result.searchParams.set('provider', opts.provider) - if (opts?.pkce && response === 'code') { - const pkce = await generatePKCE() - result.searchParams.set('code_challenge_method', 'S256') - result.searchParams.set('code_challenge', pkce.challenge) - challenge.verifier = pkce.verifier - } - - return { - challenge, - url: result.toString() - } - }, - - async exchange( - code: string, - redirectUri: string, - verifier?: string - ): Promise { - const tokens = await f(issuer + '/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - code, - redirect_uri: redirectUri, - grant_type: 'authorization_code', - client_id: input.clientId, - code_verifier: verifier || '' - }).toString() - }) - - if (!tokens.ok) { - return { - err: new InvalidAuthorizationCodeError() - } - } - - const json = (await tokens.json()) as any - return { - err: false, - tokens: { - access: json.access_token as string, - refresh: json.refresh_token as string, - expiresIn: json.expires_in as number - } - } - }, - - async refresh( - refresh: string, - opts?: RefreshOptions - ): Promise { - if (opts && opts.access) { - const decoded = decodeJwt(opts.access) - if (!decoded) { - return { - err: new InvalidAccessTokenError() - } - } - - // allow 30s window for expiration - if ((decoded.exp || 0) > Date.now() / 1000 + 30) { - return { - err: false - } - } - } - - const tokens = await f(issuer + '/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refresh - }).toString() - }) - - if (!tokens.ok) { - return { - err: new InvalidRefreshTokenError() - } - } - - const json = (await tokens.json()) as any - return { - err: false, - tokens: { - access: json.access_token as string, - refresh: json.refresh_token as string, - expiresIn: json.expires_in as number - } - } - }, - - async verify( - subjects: T, - token: string, - options?: VerifyOptions - ): Promise | VerifyError> { - const jwks = await getJWKS() - - try { - const result = await jwtVerify<{ - mode: 'access' - type: keyof T - properties: StandardSchemaV1.InferInput - }>(token, jwks, { - issuer - }) - - const validated = await subjects[result.payload.type]![ - '~standard' - ].validate(result.payload.properties) - - if (!validated.issues && result.payload.mode === 'access') { - return { - aud: result.payload.aud as string, - subject: { - type: result.payload.type, - properties: validated.value - } as any - } - } - - return { - err: new InvalidSubjectError() - } - } catch (err) { - if (err instanceof errors.JWTExpired && options?.refresh) { - const refreshed = await this.refresh(options.refresh) - if (refreshed.err) return refreshed - - const verified = await authClient.verify( - subjects, - refreshed.tokens!.access, - { - refresh: refreshed.tokens!.refresh, - issuer, - fetch: options?.fetch - } - ) - if (verified.err) return verified - - verified.tokens = refreshed.tokens - return verified - } - - return { - err: new InvalidAccessTokenError() - } - } - } - } - - return authClient -} diff --git a/packages/api-client/src/auth-subjects b/packages/api-client/src/auth-subjects deleted file mode 100644 index 2aaae8f5..00000000 --- a/packages/api-client/src/auth-subjects +++ /dev/null @@ -1,5 +0,0 @@ -// import { authSubjectSchemas } from '@agentic/platform-types' - -// import { createSubjects } from './subject' - -// export const authSubjects = createSubjects(authSubjectSchemas) diff --git a/packages/api-client/src/errors.ts b/packages/api-client/src/errors.ts deleted file mode 100644 index 9c7e82c5..00000000 --- a/packages/api-client/src/errors.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * A list of errors that can be thrown by OpenAuth. - * - * You can use these errors to check the type of error and handle it. For example. - * - * ```ts - * import { InvalidAuthorizationCodeError } from "@agentic/api-client/error" - * - * if (err instanceof InvalidAuthorizationCodeError) { - * // handle invalid code error - * } - * ``` - * - * @packageDocumentation - */ - -/** - * The OAuth server returned an error. - */ -export class OauthError extends Error { - constructor( - public error: - | 'invalid_request' - | 'invalid_grant' - | 'unauthorized_client' - | 'access_denied' - | 'unsupported_grant_type' - | 'server_error' - | 'temporarily_unavailable', - public description: string - ) { - super(error + ' - ' + description) - } -} - -/** - * The `provider` needs to be passed in. - */ -export class MissingProviderError extends OauthError { - constructor() { - super( - 'invalid_request', - 'Must specify `provider` query parameter if `select` callback on issuer is not specified' - ) - } -} - -/** - * The given parameter is missing. - */ -export class MissingParameterError extends OauthError { - constructor(public parameter: string) { - super('invalid_request', 'Missing parameter: ' + parameter) - } -} - -/** - * The given client is not authorized to use the redirect URI that was passed in. - */ -export class UnauthorizedClientError extends OauthError { - constructor( - public clientID: string, - redirectURI: string - ) { - super( - 'unauthorized_client', - `Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}` - ) - } -} - -/** - * The browser was in an unknown state. - * - * This can happen when certain cookies have expired. Or the browser was switched in the middle - * of the authentication flow. - */ -export class UnknownStateError extends Error { - constructor() { - super( - 'The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.' - ) - } -} - -/** - * The given subject is invalid. - */ -export class InvalidSubjectError extends Error { - constructor() { - super('Invalid subject') - } -} - -/** - * The given refresh token is invalid. - */ -export class InvalidRefreshTokenError extends Error { - constructor() { - super('Invalid refresh token') - } -} - -/** - * The given access token is invalid. - */ -export class InvalidAccessTokenError extends Error { - constructor() { - super('Invalid access token') - } -} - -/** - * The given authorization code is invalid. - */ -export class InvalidAuthorizationCodeError extends Error { - constructor() { - super('Invalid authorization code') - } -} diff --git a/packages/api-client/src/pkce.ts b/packages/api-client/src/pkce.ts deleted file mode 100644 index 8b896c04..00000000 --- a/packages/api-client/src/pkce.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { base64url } from 'jose' - -function generateVerifier(length: number): string { - const buffer = new Uint8Array(length) - crypto.getRandomValues(buffer) - return base64url.encode(buffer) -} - -async function generateChallenge(verifier: string, method: 'S256' | 'plain') { - if (method === 'plain') return verifier - const encoder = new TextEncoder() - const data = encoder.encode(verifier) - const hash = await crypto.subtle.digest('SHA-256', data) - return base64url.encode(new Uint8Array(hash)) -} - -export async function generatePKCE(length = 64) { - if (length < 43 || length > 128) { - throw new Error( - 'Code verifier length must be between 43 and 128 characters' - ) - } - - const verifier = generateVerifier(length) - const challenge = await generateChallenge(verifier, 'S256') - return { - verifier, - challenge, - method: 'S256' - } -} - -export async function validatePKCE( - verifier: string, - challenge: string, - method: 'S256' | 'plain' = 'S256' -) { - const generatedChallenge = await generateChallenge(verifier, method) - // timing safe equals? - return generatedChallenge === challenge -} diff --git a/packages/api-client/src/subject.ts b/packages/api-client/src/subject.ts deleted file mode 100644 index 9d418266..00000000 --- a/packages/api-client/src/subject.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Subjects are what the access token generated at the end of the auth flow will map to. Under - * the hood, the access token is a JWT that contains this data. - * - * #### Define subjects - * - * ```ts title="subjects.ts" - * import { object, string } from "valibot" - * - * const subjects = createSubjects({ - * user: object({ - * userID: string() - * }) - * }) - * ``` - * - * We are using [valibot](https://github.com/fabian-hiller/valibot) here. You can use any - * validation library that's following the - * [standard-schema specification](https://github.com/standard-schema/standard-schema). - * - * :::tip - * You typically want to place subjects in its own file so it can be imported by all of your apps. - * ::: - * - * You can start with one subject. Later you can add more for different types of users. - * - * #### Set the subjects - * - * Then you can pass it to the `issuer`. - * - * ```ts title="issuer.ts" - * import { subjects } from "./subjects" - * - * const app = issuer({ - * providers: { ... }, - * subjects, - * // ... - * }) - * ``` - * - * #### Add the subject payload - * - * When your user completes the flow, you can add the subject payload in the `success` callback. - * - * ```ts title="issuer.ts" - * const app = issuer({ - * providers: { ... }, - * subjects, - * async success(ctx, value) { - * let userID - * if (value.provider === "password") { - * console.log(value.email) - * userID = ... // lookup user or create them - * } - * return ctx.subject("user", { - * userID - * }) - * }, - * // ... - * }) - * ``` - * - * Here we are looking up the userID from our database and adding it to the subject payload. - * - * :::caution - * You should only store properties that won't change for the lifetime of the user. - * ::: - * - * Since these will be stored in the access token, you should avoid storing information - * that'll change often. For example, if you store the user's username, you'll need to - * revoke the access token when the user changes their username. - * - * #### Decode the subject - * - * Now when your user logs in, you can use the OpenAuth client to decode the subject. For - * example, in our SSR app we can do the following. - * - * ```ts title="app/page.tsx" - * import { subjects } from "../subjects" - * - * const verified = await client.verify(subjects, cookies.get("access_token")!) - * console.log(verified.subject.properties.userID) - * ``` - * - * All this is typesafe based on the shape of the subjects you defined. - * - * @packageDocumentation - */ -import type { StandardSchemaV1 } from '@standard-schema/spec' -import type { Simplify } from 'type-fest' - -/** - * Subject schema is a map of types that are used to define the subjects. - */ -export type SubjectSchema = Record - -/** @internal */ -export type SubjectPayload = Simplify< - { - [type in keyof T & string]: { - type: type - properties: StandardSchemaV1.InferOutput - } - }[keyof T & string] -> - -/** - * Create a subject schema. - * - * @example - * ```ts - * const subjects = createSubjects({ - * user: object({ - * userID: string() - * }), - * admin: object({ - * workspaceID: string() - * }) - * }) - * ``` - * - * This is using [valibot](https://github.com/fabian-hiller/valibot) to define the shape of the - * subjects. You can use any validation library that's following the - * [standard-schema specification](https://github.com/standard-schema/standard-schema). - */ -export function createSubjects( - types: Schema -): Schema { - return { ...types } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48858279..efe2f5f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,9 +240,6 @@ catalogs: is-obj: specifier: ^3.0.0 version: 3.0.0 - jose: - specifier: ^6.0.11 - version: 6.0.11 knip: specifier: ^5.61.2 version: 5.61.2 @@ -868,9 +865,6 @@ importers: '@standard-schema/spec': specifier: 'catalog:' version: 1.0.0 - jose: - specifier: 'catalog:' - version: 6.0.11 ky: specifier: 'catalog:' version: 1.8.1 @@ -5647,9 +5641,6 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true - jose@6.0.11: - resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==} - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -12310,8 +12301,6 @@ snapshots: jiti@2.4.2: {} - jose@6.0.11: {} - joycon@3.1.1: {} js-cookie@2.2.1: {}