feat: overhaul oidc work

This commit is contained in:
Aarnav Tale 2025-08-28 22:49:55 -04:00
parent bda9dedbfd
commit eb4669498a
No known key found for this signature in database
10 changed files with 200 additions and 139 deletions

View File

@ -22,6 +22,10 @@
- Switch our build processes to use TypeScript Go and Rolldown Vite for better build and type-check performance. - Switch our build processes to use TypeScript Go and Rolldown Vite for better build and type-check performance.
- Cookies are now encrypted JWTs, preserving API key secrets (*GHSA-wrqq-v7qw-r5w7*) - Cookies are now encrypted JWTs, preserving API key secrets (*GHSA-wrqq-v7qw-r5w7*)
- OIDC profile pictures are now available from Gravatar by setting `oidc.profile_picture_source` to `gravatar` (closes [#232](https://github.com/tale/headplane/issues/232)). - OIDC profile pictures are now available from Gravatar by setting `oidc.profile_picture_source` to `gravatar` (closes [#232](https://github.com/tale/headplane/issues/232)).
- OIDC now allows passing many custom parameters:
- `oidc.authorization_endpoint`, `oidc.token_endpoint`, and `oidc.userinfo_endpoint` can be overridden to support non-standard providers or scenarios without discovery (closes [#117](https://github.com/tale/headplane/issues/117)).
- `oidc.scope` can be set to specify custom scopes (defaults to `openid email profile`).
- `oidc.extra_params` can be set to pass arbitrary query parameters to the authorization endpoint (closes [#197](https://github.com/tale/headplane/issues/197)).
### 0.6.0 (May 25, 2025) ### 0.6.0 (May 25, 2025)
- Headplane 0.6.0 now requires **Headscale 0.26.0** or newer. - Headplane 0.6.0 now requires **Headscale 0.26.0** or newer.

View File

@ -25,7 +25,8 @@ export async function loader({
const data = await beginAuthFlow( const data = await beginAuthFlow(
context.oidc, context.oidc,
redirectUri, redirectUri,
context.config.oidc.token_endpoint_auth_method, context.config.oidc.scope,
context.config.oidc.extra_params,
); );
return redirect(data.url, { return redirect(data.url, {

View File

@ -65,6 +65,11 @@ const oidcConfig = type({
headscale_api_key_path: 'string?', headscale_api_key_path: 'string?',
profile_picture_source: '"oidc" | "gravatar" = "oidc"', profile_picture_source: '"oidc" | "gravatar" = "oidc"',
strict_validation: stringToBool.default(true), strict_validation: stringToBool.default(true),
scope: 'string = "openid email profile"',
extra_params: 'Record<string, string>?',
authorization_endpoint: 'string.url?',
token_endpoint: 'string.url?',
userinfo_endpoint: 'string.url?',
}) })
.narrow((obj: Record<string, unknown>, ctx: any) => { .narrow((obj: Record<string, unknown>, ctx: any) => {
const hasVal = const hasVal =
@ -97,6 +102,11 @@ const partialOidcConfig = type({
headscale_api_key_path: 'string?', headscale_api_key_path: 'string?',
profile_picture_source: '("oidc" | "gravatar")?', profile_picture_source: '("oidc" | "gravatar")?',
strict_validation: stringToBool.default(true), strict_validation: stringToBool.default(true),
scope: 'string?',
extra_params: 'Record<string, string>?',
authorization_endpoint: 'string.url?',
token_endpoint: 'string.url?',
userinfo_endpoint: 'string.url?',
}); });
const headscaleConfig = type({ const headscaleConfig = type({

View File

@ -9,7 +9,7 @@ import { createDbClient } from './db/client.server';
import { createApiClient } from './headscale/api-client'; import { createApiClient } from './headscale/api-client';
import { loadHeadscaleConfig } from './headscale/config-loader'; import { loadHeadscaleConfig } from './headscale/config-loader';
import { createHeadplaneAgent } from './hp-agent'; import { createHeadplaneAgent } from './hp-agent';
import { createOidcClient } from './web/oidc'; import { configureOidcAuth } from './web/oidc';
import { createSessionStorage } from './web/sessions'; import { createSessionStorage } from './web/sessions';
declare global { declare global {
@ -68,7 +68,7 @@ const appLoadContext = {
agents, agents,
integration: await loadIntegration(config.integration), integration: await loadIntegration(config.integration),
oidc: config.oidc ? await createOidcClient(config.oidc) : undefined, oidc: config.oidc ? await configureOidcAuth(config.oidc) : undefined,
db, db,
}; };

View File

@ -1,90 +1,37 @@
import { readFile } from 'node:fs/promises'; import * as oidc from 'openid-client';
import * as client from 'openid-client';
import log from '~/utils/log'; import log from '~/utils/log';
import type { HeadplaneConfig } from '../config/schema'; import { HeadplaneConfig } from '../config/schema';
async function loadClientSecret(path: string) { export type OidcConfig = NonNullable<HeadplaneConfig['oidc']>;
// We need to interpolate environment variables into the path
// Path formatting can be like ${ENV_NAME}/path/to/secret
const matches = path.match(/\${(.*?)}/g);
let resolvedPath = path;
if (matches) { export async function configureOidcAuth(config: OidcConfig) {
for (const match of matches) { log.debug('config', 'Running OIDC discovery for %s', config.issuer);
const env = match.slice(2, -1); let clientAuthMethod: oidc.ClientAuth;
const value = process.env[env]; switch (config.token_endpoint_auth_method) {
if (!value) {
log.error('config', 'Environment variable %s is not set', env);
return;
}
log.debug('config', 'Interpolating %s with %s', match, value);
resolvedPath = resolvedPath.replace(match, value);
}
}
try {
log.debug('config', 'Reading client secret from %s', resolvedPath);
const secret = await readFile(resolvedPath, 'utf-8');
if (secret.trim().length === 0) {
log.error('config', 'Empty OIDC client secret');
return;
}
return secret.trim();
} catch (error) {
log.error('config', 'Failed to read client secret from %s', path);
log.error('config', 'Error: %s', error);
log.debug('config', 'Error details: %o', error);
}
}
function clientAuthMethod(
method: string,
): (secret: string) => client.ClientAuth {
switch (method) {
case 'client_secret_post':
return client.ClientSecretPost;
case 'client_secret_basic': case 'client_secret_basic':
return client.ClientSecretBasic; clientAuthMethod = oidc.ClientSecretBasic(config.client_secret!);
break;
case 'client_secret_post':
clientAuthMethod = oidc.ClientSecretPost(config.client_secret!);
break;
case 'client_secret_jwt': case 'client_secret_jwt':
return client.ClientSecretJwt; clientAuthMethod = oidc.ClientSecretJwt(config.client_secret!);
break;
default: default:
throw new Error('Invalid client authentication method'); throw new Error('Invalid client authentication method');
} }
}
// Loads and configures an OIDC client to support OIDC authentication. let oidcClient: oidc.Configuration;
// This runs under the assumption the OIDC configuration exists and is valid.
// If it is invalid, Headplane automatically disables it.
//
// TODO: Support custom endpoints instead of relying on OIDC discovery.
// This will enable us to support servers like GitHub that do not support
// nor advertise a .well-known endpoint.
export async function createOidcClient(
config: NonNullable<HeadplaneConfig['oidc']>,
) {
// const secret = await loadClientSecret(oidc);
const secret = config.client_secret_path
? await loadClientSecret(config.client_secret_path)
: config.client_secret;
if (!secret) {
log.error('config', 'Missing an OIDC client secret');
return;
}
log.debug('config', 'Running OIDC discovery for %s', config.issuer);
try { try {
const oidc = await client.discovery( const discovery = await oidc.discovery(
new URL(config.issuer), new URL(config.issuer),
config.client_id, config.client_id,
secret, config.client_secret!, // TODO: Fix this config schema
clientAuthMethod(config.token_endpoint_auth_method)(secret), clientAuthMethod,
); );
const metadata = oidc.serverMetadata(); const meta = discovery.serverMetadata();
if (!metadata.authorization_endpoint) { if (!meta.authorization_endpoint) {
log.error( log.error(
'config', 'config',
'Issuer discovery did not return `authorization_endpoint`', 'Issuer discovery did not return `authorization_endpoint`',
@ -93,70 +40,118 @@ export async function createOidcClient(
'config', 'config',
'OIDC server does not support authorization code flow', 'OIDC server does not support authorization code flow',
); );
log.error('config', 'You may need to set this manually in the config');
return; return;
} }
if (!metadata.token_endpoint) { if (!meta.token_endpoint) {
log.error('config', 'Issuer discovery did not return `token_endpoint`'); log.error('config', 'Issuer discovery did not return `token_endpoint`');
log.error('config', 'OIDC server does not support token exchange'); log.error(
'config',
'OIDC server does not support authorization code flow',
);
log.error('config', 'You may need to set this manually in the config');
return; return;
} }
// If this field is missing, assume the server supports all response types if (!meta.userinfo_endpoint) {
// and that we can continue safely. log.error(
if (metadata.response_types_supported) { 'config',
if (!metadata.response_types_supported.includes('code')) { 'Issuer discovery did not return `userinfo_endpoint`',
log.error( );
'config', log.error('config', 'OIDC server does not support user info endpoint');
'Issuer discovery `response_types_supported` does not include `code`', log.error('config', 'You may need to set this manually in the config');
); return;
log.error('config', 'OIDC server does not support code flow');
return;
}
} }
if (metadata.token_endpoint_auth_methods_supported) { if (meta.token_endpoint_auth_methods_supported) {
if ( if (
!metadata.token_endpoint_auth_methods_supported.includes( !meta.token_endpoint_auth_methods_supported.includes(
config.token_endpoint_auth_method, config.token_endpoint_auth_method,
) )
) { ) {
log.error( log.error(
'config', 'config',
'Issuer discovery `token_endpoint_auth_methods_supported` does not include `%s`', 'OIDC server does not support client authentication method %s',
config.token_endpoint_auth_method, config.token_endpoint_auth_method,
); );
log.error( log.error(
'config', 'config',
'OIDC server does not support %s', 'Supported methods: %s',
config.token_endpoint_auth_method, meta.token_endpoint_auth_methods_supported.join(', '),
); );
return; return;
} }
} }
if (!metadata.userinfo_endpoint) { log.debug('config', 'OIDC discovery successful');
log.error(
'config',
'Issuer discovery did not return `userinfo_endpoint`',
);
log.error('config', 'OIDC server does not support userinfo endpoint');
return;
}
log.debug('config', 'OIDC client created successfully');
log.info('config', 'Using %s as the OIDC issuer', config.issuer);
log.debug( log.debug(
'config', 'config',
'Authorization endpoint: %s', 'Authorization endpoint: %s',
metadata.authorization_endpoint, meta.authorization_endpoint,
); );
log.debug('config', 'Token endpoint: %s', metadata.token_endpoint); log.debug('config', 'Token endpoint: %s', meta.token_endpoint);
log.debug('config', 'Userinfo endpoint: %s', metadata.userinfo_endpoint); log.debug('config', 'Userinfo endpoint: %s', meta.userinfo_endpoint);
return oidc;
} catch (error) { // Manually construct the endpoints to coalesce with config if needed
log.error('config', 'Failed to discover OIDC issuer'); oidcClient = new oidc.Configuration(
log.error('config', 'Error: %s', error); {
log.debug('config', 'Error details: %o', error); issuer: config.issuer,
authorization_endpoint:
config.authorization_endpoint || meta.authorization_endpoint,
token_endpoint: config.token_endpoint || meta.token_endpoint,
userinfo_endpoint: config.userinfo_endpoint || meta.userinfo_endpoint,
},
config.client_id,
config.client_secret!,
clientAuthMethod,
);
} catch (err) {
log.error('config', 'OIDC discovery failed: %s', err);
log.debug('config', 'Error details: %o', err);
log.error(
'config',
'This may be an error, or the server may not support discovery',
);
if (
!config.authorization_endpoint ||
!config.token_endpoint ||
!config.userinfo_endpoint
) {
log.error(
'config',
'Endpoints are not fully configured, cannot continue',
);
log.error(
'config',
'You must set authorization_endpoint, token_endpoint and userinfo_endpoint manually in the config or fix the discovery issue',
);
return;
}
oidcClient = new oidc.Configuration(
{
issuer: config.issuer,
authorization_endpoint: config.authorization_endpoint,
token_endpoint: config.token_endpoint,
userinfo_endpoint: config.userinfo_endpoint,
},
config.client_id,
config.client_secret!,
clientAuthMethod,
);
log.debug('config', 'Using manually configured endpoints');
log.debug(
'config',
'Authorization endpoint: %s',
config.authorization_endpoint,
);
log.debug('config', 'Token endpoint: %s', config.token_endpoint);
log.debug('config', 'Userinfo endpoint: %s', config.userinfo_endpoint);
} }
log.info('config', 'Successfully configured OIDC authentication');
return oidcClient;
} }

View File

@ -33,22 +33,21 @@ export function getRedirectUri(req: Request) {
export async function beginAuthFlow( export async function beginAuthFlow(
config: Configuration, config: Configuration,
redirect_uri: string, redirect_uri: string,
token_endpoint_auth_method: string, scope: string,
extra_params: Record<string, string> = {},
) { ) {
const codeVerifier = client.randomPKCECodeVerifier(); const codeVerifier = client.randomPKCECodeVerifier();
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
const params: Record<string, string> = { const params: Record<string, string> = {
...extra_params,
scope,
redirect_uri, redirect_uri,
scope: 'openid profile email',
code_challenge: codeChallenge, code_challenge: codeChallenge,
code_challenge_method: 'S256', code_challenge_method: 'S256',
token_endpoint_auth_method,
state: client.randomState(), state: client.randomState(),
}; };
// PKCE is backwards compatible with non-PKCE servers
// so if we don't support it, just set our nonce
if (!config.serverMetadata().supportsPKCE()) { if (!config.serverMetadata().supportsPKCE()) {
params.nonce = client.randomNonce(); params.nonce = client.randomNonce();
} }

View File

@ -136,6 +136,18 @@ integration:
# (This is optional, but recommended for the best experience) # (This is optional, but recommended for the best experience)
oidc: oidc:
issuer: "https://accounts.google.com" issuer: "https://accounts.google.com"
# If your OIDC provider does not support discovery (does not have the URL at
# `/.well-known/openid-configuration`), you need to manually set endpoints.
# This also works to override endpoints if you so desire or if your OIDC
# discovery is missing certain endpoints (ie GitHub).
# For some typical providers, see the documentation.
# authorization_endpoint: ""
# token_endpoint: ""
# userinfo_endpoint: ""
# The client ID for the OIDC client
client_id: "your-client-id" client_id: "your-client-id"
# The client secret for the OIDC client # The client secret for the OIDC client
@ -146,6 +158,9 @@ oidc:
# with systemd's `LoadCredential` straightforward: # with systemd's `LoadCredential` straightforward:
# client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret" # client_secret_path: "${CREDENTIALS_DIRECTORY}/oidc_client_secret"
# Defaults to 'openid email profile'
# scope: "openid email profile"
disable_api_key_login: false disable_api_key_login: false
token_endpoint_auth_method: "client_secret_post" token_endpoint_auth_method: "client_secret_post"
@ -166,3 +181,9 @@ oidc:
# we go to fetch the userinfo endpoint. Optionally, this can be set to # we go to fetch the userinfo endpoint. Optionally, this can be set to
# "oidc" or "gravatar" as of 0.6.1. # "oidc" or "gravatar" as of 0.6.1.
# profile_picture_source: "gravatar" # profile_picture_source: "gravatar"
# Extra query parameters can be passed to the authorization endpoint
# by setting them here. This is useful for providers that require any kind
# of custom hinting.
# extra_params:
# prompt: "select_account" # Example: force account selection on Google

View File

@ -28,7 +28,7 @@ in
pnpmDeps = pnpm_10.fetchDeps { pnpmDeps = pnpm_10.fetchDeps {
inherit (finalAttrs) pname version src; inherit (finalAttrs) pname version src;
hash = "sha256-Xi4IZEFFHx2Wp7r1qleGj8BHZOKLWsSfFT4UKbF+9Y8="; hash = "sha256-mNVDgVfGNnBRtUVacVwjyww/XWqFTlayHtv6TLgXTT4=";
fetcherVersion = 1; fetcherVersion = 1;
}; };

View File

@ -44,10 +44,10 @@
"dotenv": "17.2.1", "dotenv": "17.2.1",
"drizzle-orm": "0.44.4", "drizzle-orm": "0.44.4",
"isbot": "5.1.30", "isbot": "5.1.30",
"jose": "6.0.13", "jose": "6.1.0",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"mime": "^4.0.7", "mime": "^4.0.7",
"openid-client": "6.6.4", "openid-client": "6.7.0",
"react": "19.1.1", "react": "19.1.1",
"react-aria": "3.42.0", "react-aria": "3.42.0",
"react-codemirror-merge": "4.25.1", "react-codemirror-merge": "4.25.1",

67
pnpm-lock.yaml generated
View File

@ -98,8 +98,8 @@ importers:
specifier: 5.1.30 specifier: 5.1.30
version: 5.1.30 version: 5.1.30
jose: jose:
specifier: 6.0.13 specifier: 6.1.0
version: 6.0.13 version: 6.1.0
lucide-react: lucide-react:
specifier: ^0.540.0 specifier: ^0.540.0
version: 0.540.0(react@19.1.1) version: 0.540.0(react@19.1.1)
@ -107,8 +107,8 @@ importers:
specifier: ^4.0.7 specifier: ^4.0.7
version: 4.0.7 version: 4.0.7
openid-client: openid-client:
specifier: 6.6.4 specifier: 6.7.0
version: 6.6.4 version: 6.7.0
react: react:
specifier: 19.1.1 specifier: 19.1.1
version: 19.1.1 version: 19.1.1
@ -138,7 +138,7 @@ importers:
version: 3.40.0(react@19.1.1) version: 3.40.0(react@19.1.1)
remix-utils: remix-utils:
specifier: ^8.8.0 specifier: ^8.8.0
version: 8.8.0(react-router@7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) version: 8.8.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(react-router@7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)
tailwind-merge: tailwind-merge:
specifier: 3.3.1 specifier: 3.3.1
version: 3.3.1 version: 3.3.1
@ -1038,6 +1038,18 @@ packages:
resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
'@oslojs/asn1@1.0.0':
resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==}
'@oslojs/binary@1.0.0':
resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==}
'@oslojs/crypto@1.0.1':
resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==}
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
'@oxc-project/runtime@0.82.2': '@oxc-project/runtime@0.82.2':
resolution: {integrity: sha512-cYxcj5CPn/vo5QSpCZcYzBiLidU5+GlFSqIeNaMgBDtcVRBsBJHZg3pHw999W6nHamFQ1EHuPPByB26tjaJiJw==} resolution: {integrity: sha512-cYxcj5CPn/vo5QSpCZcYzBiLidU5+GlFSqIeNaMgBDtcVRBsBJHZg3pHw999W6nHamFQ1EHuPPByB26tjaJiJw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -2898,8 +2910,8 @@ packages:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true hasBin: true
jose@6.0.13: jose@6.1.0:
resolution: {integrity: sha512-Yms4GpbmdANamS51kKK6w4hRlKx8KTxbWyAAKT/MhUMtqbIqh5mb2HjhTNUbk7TFL8/MBB5zWSDohL7ed4k/UA==} resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
js-base64@3.7.7: js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
@ -3232,8 +3244,8 @@ packages:
resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
oauth4webapi@3.7.0: oauth4webapi@3.8.0:
resolution: {integrity: sha512-Q52wTPUWPsVLVVmTViXPQFMW2h2xv2jnDGxypjpelCFKaOjLsm7AxYuOk1oQgFm95VNDbuggasu9htXrz6XwKw==} resolution: {integrity: sha512-RSu64fjsBIs6D7s9g5LOCnOohOkI0nnPtlIp/4rrHj2Vb8jGepq+fujkv2Srw4tuw9qa02aETXQzyQUle8nfnQ==}
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@ -3244,8 +3256,8 @@ packages:
oniguruma-to-es@4.3.3: oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
openid-client@6.6.4: openid-client@6.7.0:
resolution: {integrity: sha512-PLWVhRksRnNH05sqeuCX/PR+1J70NyZcAcPske+FeF732KKONd3v0p5Utx1ro1iLfCglH8B3/+dA1vqIHDoIiA==} resolution: {integrity: sha512-mA8Cex9wGK6Jaqq4vd1sPDK/a+cQ6G+lExVJ9IRjwInN4NcEEntYqLAZsBhZOyHwDx7V1nmPKOvnxOcdLtSpoA==}
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@ -4775,7 +4787,7 @@ snapshots:
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonpath-plus: 10.3.0 jsonpath-plus: 10.3.0
node-fetch: 2.7.0 node-fetch: 2.7.0
openid-client: 6.6.4 openid-client: 6.7.0
rfc4648: 1.5.4 rfc4648: 1.5.4
socks-proxy-agent: 8.0.5 socks-proxy-agent: 8.0.5
stream-buffers: 3.0.3 stream-buffers: 3.0.3
@ -4904,6 +4916,23 @@ snapshots:
dependencies: dependencies:
which: 3.0.1 which: 3.0.1
'@oslojs/asn1@1.0.0':
dependencies:
'@oslojs/binary': 1.0.0
optional: true
'@oslojs/binary@1.0.0':
optional: true
'@oslojs/crypto@1.0.1':
dependencies:
'@oslojs/asn1': 1.0.0
'@oslojs/binary': 1.0.0
optional: true
'@oslojs/encoding@1.1.0':
optional: true
'@oxc-project/runtime@0.82.2': {} '@oxc-project/runtime@0.82.2': {}
'@oxc-project/types@0.82.2': {} '@oxc-project/types@0.82.2': {}
@ -7081,7 +7110,7 @@ snapshots:
jiti@2.5.1: {} jiti@2.5.1: {}
jose@6.0.13: {} jose@6.1.0: {}
js-base64@3.7.7: {} js-base64@3.7.7: {}
@ -7362,7 +7391,7 @@ snapshots:
npm-package-arg: 10.1.0 npm-package-arg: 10.1.0
semver: 7.7.2 semver: 7.7.2
oauth4webapi@3.7.0: {} oauth4webapi@3.8.0: {}
once@1.4.0: once@1.4.0:
dependencies: dependencies:
@ -7376,10 +7405,10 @@ snapshots:
regex: 6.0.1 regex: 6.0.1
regex-recursion: 6.0.2 regex-recursion: 6.0.2
openid-client@6.6.4: openid-client@6.7.0:
dependencies: dependencies:
jose: 6.0.13 jose: 6.1.0
oauth4webapi: 3.7.0 oauth4webapi: 3.8.0
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
@ -7666,10 +7695,12 @@ snapshots:
dependencies: dependencies:
regex-utilities: 2.3.0 regex-utilities: 2.3.0
remix-utils@8.8.0(react-router@7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): remix-utils@8.8.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(react-router@7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1):
dependencies: dependencies:
type-fest: 4.41.0 type-fest: 4.41.0
optionalDependencies: optionalDependencies:
'@oslojs/crypto': 1.0.1
'@oslojs/encoding': 1.1.0
react: 19.1.1 react: 19.1.1
react-router: 7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-router: 7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)