diff --git a/app/routes.ts b/app/routes.ts index 00d4b22..499f98f 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -9,6 +9,7 @@ export default [ route('/login', 'routes/auth/login.tsx'), route('/logout', 'routes/auth/logout.ts'), route('/oidc/callback', 'routes/auth/oidc-callback.ts'), + route('/oidc/start', 'routes/auth/oidc-start.ts'), // All the main logged-in dashboard routes layout('layouts/dashboard.tsx', [ diff --git a/app/routes/auth/login.tsx b/app/routes/auth/login.tsx index 0fbfb39..535efa0 100644 --- a/app/routes/auth/login.tsx +++ b/app/routes/auth/login.tsx @@ -13,10 +13,15 @@ import TextField from '~/components/TextField'; import type { Key } from '~/types'; import { loadContext } from '~/utils/config/headplane'; import { pull } from '~/utils/headscale'; -import { startOidc } from '~/utils/oidc'; +import { + startOidc, + beginAuthFlow, + getRedirectUri +} from '~/utils/oidc'; import { commitSession, getSession } from '~/utils/sessions.server'; export async function loader({ request }: LoaderFunctionArgs) { + const session = await getSession(request.headers.get('Cookie')); if (session.has('hsApiKey')) { return redirect('/machines', { @@ -30,7 +35,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // Only set if OIDC is properly enabled anyways if (context.oidc?.disableKeyLogin) { - return startOidc(context.oidc, request); + return redirect('/oidc/start'); } return { @@ -42,6 +47,7 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const oidcStart = formData.get('oidc-start'); + const session = await getSession(request.headers.get('Cookie')); if (oidcStart) { const context = await loadContext(); @@ -50,12 +56,10 @@ export async function action({ request }: ActionFunctionArgs) { throw new Error('An invalid OIDC configuration was provided'); } - // We know it exists here because this action only happens on OIDC - return startOidc(context.oidc, request); + return redirect('/oidc/start'); } const apiKey = String(formData.get('api-key')); - const session = await getSession(request.headers.get('Cookie')); // Test the API key try { @@ -71,6 +75,7 @@ export async function action({ request }: ActionFunctionArgs) { session.set('hsApiKey', apiKey); session.set('user', { + subject: 'unknown-non-oauth', name: key.prefix, email: `${expiresDays.toString()} days`, }); diff --git a/app/routes/auth/oidc-callback.ts b/app/routes/auth/oidc-callback.ts index 4ff3177..e8a543d 100644 --- a/app/routes/auth/oidc-callback.ts +++ b/app/routes/auth/oidc-callback.ts @@ -1,17 +1,77 @@ -import { type LoaderFunctionArgs, data } from 'react-router'; +import { type LoaderFunctionArgs, redirect } from 'react-router'; import { loadContext } from '~/utils/config/headplane'; -import { finishOidc } from '~/utils/oidc'; +import { getSession, commitSession } from '~/utils/sessions.server'; +import { finishAuthFlow, getRedirectUri, formatError } from '~/utils/oidc'; +import { send } from '~/utils/res'; export async function loader({ request }: LoaderFunctionArgs) { - try { - const context = await loadContext(); - if (!context.oidc) { - throw new Error('An invalid OIDC configuration was provided'); - } + // Check if we have 0 query parameters + const url = new URL(request.url); + if (url.searchParams.toString().length === 0) { + return redirect('/machines'); + } - return finishOidc(context.oidc, request); + const session = await getSession(request.headers.get('Cookie')); + if (session.has('hsApiKey')) { + return redirect('/machines') + } + + // This is a hold-over from the old code + // TODO: Rewrite checkOIDC in the context loader + const { oidc } = await loadContext(); + if (!oidc) { + throw new Error('An invalid OIDC configuration was provided'); + } + + const oidcConfig = { + issuer: oidc.issuer, + clientId: oidc.client, + clientSecret: oidc.secret, + redirectUri: oidc.redirectUri, + tokenEndpointAuthMethod: oidc.method, + } + + const codeVerifier = session.get('oidc_code_verif'); + const state = session.get('oidc_state'); + const nonce = session.get('oidc_nonce'); + + if (!codeVerifier || !state || !nonce) { + return send({ error: 'Missing OIDC state' }, { status: 400 }); + } + + const flowOptions = { + redirect_uri: request.url, + codeVerifier, + state, + nonce: nonce === '' ? undefined : nonce, + } + + try { + const user = await finishAuthFlow(oidcConfig, flowOptions); + session.set('user', user); + session.unset('oidc_code_verif'); + session.unset('oidc_state'); + session.unset('oidc_nonce'); + + // TODO: This is breaking, to stop the "over-generation" of API + // keys because they are currently non-deletable in the headscale + // database. Look at this in the future once we have a solution + // or we have permissioned API keys. + session.set('hsApiKey', oidc.rootKey); + return redirect('/machines', { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); } catch (error) { - // Gracefully present OIDC errors - return data({ error }, { status: 500 }); + return new Response( + JSON.stringify(formatError(error)), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ); } } diff --git a/app/routes/auth/oidc-start.ts b/app/routes/auth/oidc-start.ts new file mode 100644 index 0000000..4115629 --- /dev/null +++ b/app/routes/auth/oidc-start.ts @@ -0,0 +1,40 @@ +import { type LoaderFunctionArgs, data, redirect } from 'react-router'; +import { commitSession, getSession } from '~/utils/sessions.server'; +import { send } from '~/utils/res'; +import { beginAuthFlow, getRedirectUri } from '~/utils/oidc'; +import { loadContext } from '~/utils/config/headplane'; + +export async function loader({ request }: LoaderFunctionArgs) { + const session = await getSession(request.headers.get('Cookie')); + if (session.has('hsApiKey')) { + return redirect('/machines') + } + + // This is a hold-over from the old code + // TODO: Rewrite checkOIDC in the context loader + const { oidc } = await loadContext(); + if (!oidc) { + throw new Error('An invalid OIDC configuration was provided'); + } + + const oidcConfig = { + issuer: oidc.issuer, + clientId: oidc.client, + clientSecret: oidc.secret, + redirectUri: oidc.redirectUri, + tokenEndpointAuthMethod: oidc.method, + } + + const redirectUri = oidcConfig.redirectUri ?? getRedirectUri(request); + const data = await beginAuthFlow(oidcConfig, redirectUri); + session.set('oidc_code_verif', data.codeVerifier); + session.set('oidc_state', data.state); + session.set('oidc_nonce', data.nonce); + + return redirect(data.url, { + status: 302, + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); +} diff --git a/app/utils/config/headplane.ts b/app/utils/config/headplane.ts index 2c96585..abeaa64 100644 --- a/app/utils/config/headplane.ts +++ b/app/utils/config/headplane.ts @@ -37,6 +37,7 @@ export interface HeadplaneContext { issuer: string; client: string; secret: string; + redirectUri?: string; rootKey: string; method: string; disableKeyLogin: boolean; @@ -204,10 +205,15 @@ async function checkOidc(config?: HeadscaleConfig) { let secret = process.env.OIDC_CLIENT_SECRET; const method = process.env.OIDC_CLIENT_SECRET_METHOD ?? 'client_secret_basic'; const skip = process.env.OIDC_SKIP_CONFIG_VALIDATION === 'true'; + const redirectUri = process.env.OIDC_REDIRECT_URI; log.debug('CTXT', 'Checking OIDC environment variables'); log.debug('CTXT', 'Issuer: %s', issuer); log.debug('CTXT', 'Client: %s', client); + log.debug('CTXT', 'Token Auth Method: %s', method); + if (redirectUri) { + log.debug('CTXT', 'Redirect URI: %s', redirectUri); + } if ( (issuer ?? client ?? secret) && @@ -223,7 +229,17 @@ async function checkOidc(config?: HeadscaleConfig) { 'CTXT', 'Validating OIDC configuration from environment variables', ); - const result = await testOidc(issuer, client, secret); + + // This is a hold-over from the old code + // TODO: Rewrite checkOIDC in the context loader + const oidcConfig = { + issuer: issuer, + clientId: client, + clientSecret: secret, + tokenEndpointAuthMethod: method, + } + + const result = await testOidc(oidcConfig) if (!result) { return; } @@ -236,6 +252,7 @@ async function checkOidc(config?: HeadscaleConfig) { issuer, client, secret, + redirectUri, method, rootKey, disableKeyLogin, @@ -279,7 +296,14 @@ async function checkOidc(config?: HeadscaleConfig) { if (config?.oidc?.only_start_if_oidc_is_available) { log.debug('CTXT', 'Validating OIDC configuration from headscale config'); - const result = await testOidc(issuer, client, secret); + const oidcConfig = { + issuer: issuer, + clientId: client, + clientSecret: secret, + tokenEndpointAuthMethod: method, + } + + const result = await testOidc(oidcConfig) if (!result) { return; } @@ -292,6 +316,7 @@ async function checkOidc(config?: HeadscaleConfig) { issuer, client, secret, + redirectUri, rootKey, method, disableKeyLogin, diff --git a/app/utils/oidc.ts b/app/utils/oidc.ts index 1b6419d..9e3b262 100644 --- a/app/utils/oidc.ts +++ b/app/utils/oidc.ts @@ -1,24 +1,5 @@ import { redirect } from 'react-router'; import * as client from 'openid-client'; -import { - authorizationCodeGrantRequest, - calculatePKCECodeChallenge, - Client, - ClientAuthenticationMethod, - discoveryRequest, - generateRandomCodeVerifier, - generateRandomNonce, - generateRandomState, - getValidatedIdTokenClaims, - isOAuth2Error, - parseWwwAuthenticateChallenges, - processAuthorizationCodeOpenIDResponse, - processDiscoveryResponse, - validateAuthResponse, -} from 'oauth4webapi'; - -import { post } from '~/utils/headscale'; -import { commitSession, getSession } from '~/utils/sessions.server'; import log from '~/utils/log'; import type { HeadplaneContext } from './config/headplane'; @@ -28,35 +9,10 @@ const oidcConfigSchema = z.object({ issuer: z.string(), clientId: z.string(), clientSecret: z.string(), + redirectUri: z.string().optional(), tokenEndpointAuthMethod: z - .enum(['client_secret_post', 'client_secret_basic']) + .enum(['client_secret_post', 'client_secret_basic', 'client_secret_jwt']) .default('client_secret_basic'), - idTokenSigningAlg: z - .enum([ - 'RS256', - 'RS384', - 'RS512', - 'ES256', - 'ES384', - 'ES512', - 'PS256', - 'PS384', - 'PS512', - ]) - .default('RS256'), - idTokenEncryptionAlg: z - .enum(['RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256']) - .default('RSA-OAEP'), - idTokenEncryptionEnc: z - .enum([ - 'A128CBC-HS256', - 'A192CBC-HS384', - 'A256CBC-HS512', - 'A128GCM', - 'A192GCM', - 'A256GCM', - ]) - .default('A256GCM'), }); declare global { @@ -67,6 +23,7 @@ export type OidcConfig = z.infer; // We try our best to infer the callback URI of our Headplane instance // By default it is always //oidc/callback +// (This can ALWAYS be overridden through the OidcConfig) export function getRedirectUri(req: Request) { const base = __PREFIX__ ?? '/admin'; // Fallback const url = new URL(`${base}/oidc/callback`, req.url); @@ -92,22 +49,38 @@ export function getRedirectUri(req: Request) { return url.href; } +function clientAuthMethod(method: string): (secret: string) => client.ClientAuth { + switch (method) { + case 'client_secret_post': + return client.ClientSecretPost + case 'client_secret_basic': + return client.ClientSecretBasic + case 'client_secret_jwt': + return client.ClientSecretJwt + default: + throw new Error('Invalid client authentication method'); + } +} + export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) { const config = await client.discovery( - oidc.issuer, + new URL(oidc.issuer), oidc.clientId, oidc.clientSecret, + new clientAuthMethod(oidc.tokenEndpointAuthMethod)(oidc.clientSecret), ); let codeVerifier: string, codeChallenge: string; codeVerifier = client.randomPKCECodeVerifier(); codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); - let params: Record = { + const params: Record = { redirect_uri, scope: 'openid profile email', code_challenge: codeChallenge, code_challenge_method: 'S256', + token_endpoint_auth_method: oidc.tokenEndpointAuthMethod, + state: client.randomState(), } // PKCE is backwards compatible with non-PKCE servers @@ -120,213 +93,122 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) { return { url: url.href, codeVerifier, - nonce: params.nonce, + state: params.state, + nonce: params.nonce ?? '', }; } interface FlowOptions { redirect_uri: string; codeVerifier: string; + state: string; nonce?: string; } export async function finishAuthFlow(oidc: OidcConfig, options: FlowOptions) { const config = await client.discovery( - oidc.issuer, + new URL(oidc.issuer), oidc.clientId, oidc.clientSecret, + new clientAuthMethod(oidc.tokenEndpointAuthMethod)(oidc.clientSecret), ); let subject: string, accessToken: string; const tokens = await client.authorizationCodeGrant(config, new URL(options.redirect_uri), { pkceCodeVerifier: options.codeVerifier, expectedNonce: options.nonce, + expectedState: options.state, idTokenExpected: true - }) - - console.log(tokens); -} - -export async function startOidc(oidc: OidcConfig, req: Request) { - const session = await getSession(req.headers.get('Cookie')); - if (session.has('hsApiKey')) { - return redirect('/', { - status: 302, - headers: { - 'Set-Cookie': await commitSession(session), - }, - }); - } - - - // TODO: Properly validate the method is a valid type - const method = oidc.method as ClientAuthenticationMethod; - const issuerUrl = new URL(oidc.issuer); - const oidcClient = { - client_id: oidc.client, - token_endpoint_auth_method: method, - } satisfies Client; - - const response = await discoveryRequest(issuerUrl); - const processed = await processDiscoveryResponse(issuerUrl, response); - if (!processed.authorization_endpoint) { - throw new Error('No authorization endpoint found on the OIDC provider'); - } - - const state = generateRandomState(); - const nonce = generateRandomNonce(); - const verifier = generateRandomCodeVerifier(); - const challenge = await calculatePKCECodeChallenge(verifier); - - const callback = new URL('/admin/oidc/callback', req.url); - callback.protocol = req.headers.get('X-Forwarded-Proto') ?? 'http:'; - callback.host = req.headers.get('Host') ?? ''; - const authUrl = new URL(processed.authorization_endpoint); - - authUrl.searchParams.set('client_id', oidcClient.client_id); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('redirect_uri', callback.href); - authUrl.searchParams.set('scope', 'openid profile email'); - authUrl.searchParams.set('code_challenge', challenge); - authUrl.searchParams.set('code_challenge_method', 'S256'); - authUrl.searchParams.set('state', state); - authUrl.searchParams.set('nonce', nonce); - - session.set('authState', state); - session.set('authNonce', nonce); - session.set('authVerifier', verifier); - - return redirect(authUrl.href, { - status: 302, - headers: { - 'Set-Cookie': await commitSession(session), - }, }); -} -export async function finishOidc(oidc: OidcConfig, req: Request) { - const session = await getSession(req.headers.get('Cookie')); - if (session.has('hsApiKey')) { - return redirect('/', { - status: 302, - headers: { - 'Set-Cookie': await commitSession(session), - }, - }); + const claims = tokens.claims(); + if (!claims?.sub) { + throw new Error('No subject found in OIDC claims'); } - // TODO: Properly validate the method is a valid type - const method = oidc.method as ClientAuthenticationMethod; - const issuerUrl = new URL(oidc.issuer); - const oidcClient = { - client_id: oidc.client, - client_secret: oidc.secret, - token_endpoint_auth_method: method, - } satisfies Client; - - const response = await discoveryRequest(issuerUrl); - const processed = await processDiscoveryResponse(issuerUrl, response); - if (!processed.authorization_endpoint) { - throw new Error('No authorization endpoint found on the OIDC provider'); - } - - const state = session.get('authState'); - const nonce = session.get('authNonce'); - const verifier = session.get('authVerifier'); - if (!state || !nonce || !verifier) { - throw new Error('No OIDC state found in the session'); - } - - const parameters = validateAuthResponse( - processed, - oidcClient, - new URL(req.url), - state, + const user = await client.fetchUserInfo( + config, + tokens.access_token, + claims.sub, ); - if (isOAuth2Error(parameters)) { - throw new Error('Invalid response from the OIDC provider'); - } - - const callback = new URL('/admin/oidc/callback', req.url); - callback.protocol = req.headers.get('X-Forwarded-Proto') ?? 'http:'; - callback.host = req.headers.get('Host') ?? ''; - - const tokenResponse = await authorizationCodeGrantRequest( - processed, - oidcClient, - parameters, - callback.href, - verifier, - ); - - const challenges = parseWwwAuthenticateChallenges(tokenResponse); - if (challenges) { - throw new Error('Recieved a challenge from the OIDC provider'); - } - - const result = await processAuthorizationCodeOpenIDResponse( - processed, - oidcClient, - tokenResponse, - nonce, - ); - - if (isOAuth2Error(result)) { - throw new Error('Invalid response from the OIDC provider'); - } - - const claims = getValidatedIdTokenClaims(result); - const expDate = new Date(claims.exp * 1000).toISOString(); - - const keyResponse = await post<{ apiKey: string }>( - 'v1/apikey', - oidc.rootKey, - { - expiration: expDate, - }, - ); - - session.set('hsApiKey', keyResponse.apiKey); - session.set('user', { + return { + subject: claims.sub, name: claims.name ? String(claims.name) : 'Anonymous', email: claims.email ? String(claims.email) : undefined, - }); - - return redirect('/machines', { - headers: { - 'Set-Cookie': await commitSession(session), - }, - }); -} - -// Runs at application startup to validate the OIDC configuration -export async function testOidc(issuer: string, client: string, secret: string) { - const oidcClient = { - client_id: client, - client_secret: secret, - token_endpoint_auth_method: 'client_secret_post', - } satisfies Client; - - const issuerUrl = new URL(issuer); - - try { - log.debug('OIDC', 'Checking OIDC well-known endpoint'); - const response = await discoveryRequest(issuerUrl); - const processed = await processDiscoveryResponse(issuerUrl, response); - if (!processed.authorization_endpoint) { - log.debug('OIDC', 'No authorization endpoint found on the OIDC provider'); - return false; - } - - log.debug( - 'OIDC', - 'Found auth endpoint: %s', - processed.authorization_endpoint, - ); - return true; - } catch (e) { - log.debug('OIDC', 'Validation failed: %s', e.message); - return false; + username: claims.preferred_username ? String(claims.preferred_username) : undefined, } } + +export function formatError(error: unknown) { + if (error instanceof client.ResponseBodyError) { + return { + code: error.code, + error: { + name: error.error, + description: error.error_description, + }, + }; + } + + if (error instanceof client.AuthorizationResponseError) { + return { + code: error.code, + error: { + name: error.error, + description: error.error_description, + }, + }; + } + + if (error instanceof client.WWWAuthenticateChallengeError) { + return { + code: error.code, + error: { + name: error.name, + description: error.message, + challenges: error.cause, + }, + }; + } + + log.error('OIDC', 'Unknown error: %s', error); + return { + code: 500, + error: { + name: 'Internal Server Error', + description: 'An unknown error occurred', + }, + }; +} + +export async function testOidc(oidc: OidcConfig) { + log.debug('OIDC', 'Discovering OIDC configuration from %s', oidc.issuer); + const config = await client.discovery( + new URL(oidc.issuer), + oidc.clientId, + oidc.clientSecret, + new clientAuthMethod(oidc.tokenEndpointAuthMethod)(oidc.clientSecret), + ); + + const meta = config.serverMetadata(); + if (meta.authorization_endpoint === undefined) { + return false; + } + + log.debug('OIDC', 'Authorization endpoint: %s', meta.authorization_endpoint); + log.debug('OIDC', 'Token endpoint: %s', meta.token_endpoint); + + if (meta.response_types_supported.includes('code') === false) { + log.error('OIDC', 'OIDC server does not support code flow'); + return false; + } + + if (meta.token_endpoint_auth_methods_supported.includes(oidc.tokenEndpointAuthMethod) === false) { + log.error('OIDC', 'OIDC server does not support %s', oidc.tokenEndpointAuthMethod); + return false; + } + + log.debug('OIDC', 'OIDC configuration is valid'); + return true; +} diff --git a/app/utils/sessions.server.ts b/app/utils/sessions.server.ts index 3c5e8b8..9791da1 100644 --- a/app/utils/sessions.server.ts +++ b/app/utils/sessions.server.ts @@ -2,12 +2,14 @@ import { Session, SessionStorage, createCookieSessionStorage } from 'react-route export type SessionData = { hsApiKey: string; - authState: string; - authNonce: string; - authVerifier: string; + oidc_state: string; + oidc_code_verif: string; + oidc_nonce: string; user: { + subject: string; name: string; email?: string; + username?: string; }; }; diff --git a/docs/Configuration.md b/docs/Configuration.md index f403cfa..a4401e8 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -36,6 +36,7 @@ If you use the Headscale configuration integration, these are not required. - **`OIDC_CLIENT_ID`**: The client ID of your OIDC provider. - **`OIDC_CLIENT_SECRET`**: The client secret of your OIDC provider. - **`OIDC_CLIENT_SECRET_METHOD`**: The method used to send the client secret (default: `client_secret_basic`). +- **`OIDC_REDIRECT_URI`**: The redirect URI for the OIDC provider (recommended, otherwise guessed). - **`OIDC_SKIP_CONFIG_VALIDATION`**: Skip the OIDC configuration validation (default: `false`). - **`ROOT_API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long). - **`DISABLE_API_KEY_LOGIN`**: If you want to disable API key login, set this to `true`. diff --git a/package.json b/package.json index 237ce88..42eb1e2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dotenv": "^16.4.7", "isbot": "^5.1.19", "mime": "^4.0.6", - "oauth4webapi": "^2.17.0", + "openid-client": "^6.1.7", "react": "19.0.0", "react-aria-components": "^1.5.0", "react-codemirror-merge": "^4.23.7", @@ -49,7 +49,7 @@ "@biomejs/biome": "^1.9.4", "@react-router/dev": "^7.0.0", "autoprefixer": "^10.4.20", - "babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124", + "babel-plugin-react-compiler": "19.0.0-beta-55955c9-20241229", "postcss": "^8.4.49", "react-router-dom": "^7.1.1", "tailwindcss": "^3.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e26c5e..895a8bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,9 +67,9 @@ importers: mime: specifier: ^4.0.6 version: 4.0.6 - oauth4webapi: - specifier: ^2.17.0 - version: 2.17.0 + openid-client: + specifier: ^6.1.7 + version: 6.1.7 react: specifier: 19.0.0 version: 19.0.0 @@ -126,8 +126,8 @@ importers: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) babel-plugin-react-compiler: - specifier: 19.0.0-beta-df7b47d-20241124 - version: 19.0.0-beta-df7b47d-20241124 + specifier: 19.0.0-beta-55955c9-20241229 + version: 19.0.0-beta-55955c9-20241229 postcss: specifier: ^8.4.49 version: 8.4.49 @@ -1609,8 +1609,8 @@ packages: babel-dead-code-elimination@1.0.8: resolution: {integrity: sha512-og6HQERk0Cmm+nTT4Od2wbPtgABXFMPaHACjbKLulZIFMkYyXZLkUGuAxdgpMJBrxyt/XFpSz++lNzjbcMnPkQ==} - babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124: - resolution: {integrity: sha512-93iSASR20HNsotcOTQ+KPL0zpgfRFVWL86AtXpmHp995HuMVnC9femd8Winr3GxkPEh8lEOyaw3nqY4q2HUm5w==} + babel-plugin-react-compiler@19.0.0-beta-55955c9-20241229: + resolution: {integrity: sha512-APpa9fRiG5UN5kxnB/vznaSBKbXwAWZs6QshN3MLntzWa4cUhOxzUSd7Ohmr5sLQaM0ZHjjOg07pw1ZoR7+Oog==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2138,9 +2138,6 @@ packages: oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - oauth4webapi@2.17.0: - resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} - oauth4webapi@3.1.4: resolution: {integrity: sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==} @@ -4664,7 +4661,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124: + babel-plugin-react-compiler@19.0.0-beta-55955c9-20241229: dependencies: '@babel/types': 7.26.3 @@ -5050,8 +5047,7 @@ snapshots: jiti@1.21.7: {} - jose@5.9.6: - optional: true + jose@5.9.6: {} js-tokens@4.0.0: {} @@ -5183,10 +5179,7 @@ snapshots: oauth-sign@0.9.0: {} - oauth4webapi@2.17.0: {} - - oauth4webapi@3.1.4: - optional: true + oauth4webapi@3.1.4: {} object-assign@4.1.1: {} @@ -5200,7 +5193,6 @@ snapshots: dependencies: jose: 5.9.6 oauth4webapi: 3.1.4 - optional: true package-json-from-dist@1.0.1: {} diff --git a/vite.config.ts b/vite.config.ts index 15b86e7..1f6ca48 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,5 +27,6 @@ export default defineConfig({ }, define: { __VERSION__: JSON.stringify(version), + __PREFIX__: JSON.stringify(prefix), }, });