@@ -51,7 +43,7 @@ export default function Page() {
- {config && oidc ? (
+ {config && isOidcEnabled ? (
<>
diff --git a/app/server/README.md b/app/server/README.md
deleted file mode 100644
index ef23e17..0000000
--- a/app/server/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Headplane Server
-This code is responsible for all code that is necessary *before* any
-web server is started. It is the only part of the code that contains
-many side-effects (in this case, importing a module may run code).
-
-# Hierarchy
-```
-server
-├── index.ts: Loads everything and starts the web server.
-├── agent/
-│ ├── dispatcher.ts: Serializes commands for the agent control fd (stdin).
-│ ├── ssh.ts: Manages & multiplexes the active web SSH connections
-│ ├── env.ts: Checks the environment variables for custom overrides.
-├── config/
-│ ├── integration/
-│ │ ├── abstract.ts: Defines the abstract class for integrations.
-│ │ ├── docker.ts: Contains the Docker integration.
-│ │ ├── index.ts: Determines the correct integration to use (if any).
-│ │ ├── kubernetes.ts: Contains the Kubernetes integration.
-│ │ ├── proc.ts: Contains the Proc integration.
-│ ├── env.ts: Checks the environment variables for custom overrides.
-│ ├── loader.ts: Checks the configuration file and coalesces with ENV.
-│ ├── schema.ts: Defines the schema for the Headplane configuration.
-├── headscale/
-│ ├── api-client.ts: Creates the HTTP client that talks to the Headscale API.
-│ ├── api-error.ts: Contains the ResponseError definition.
-│ ├── config-loader.ts: Loads the Headscale configuration (if available).
-│ ├── config-schema.ts: Defines the schema for the Headscale configuration.
-├── web/
-│ ├── agent.ts: Handles setting up the agent WebSocket if needed.
-│ ├── oidc.ts: Loads and validates an OIDC configuration (if available).
-│ ├── roles.ts: Contains information about authentication permissions.
-│ ├── sessions.ts: Initializes the session store and methods to manage it.
diff --git a/app/server/config/config-schema.ts b/app/server/config/config-schema.ts
index 2dc4c27..38ce95d 100644
--- a/app/server/config/config-schema.ts
+++ b/app/server/config/config-schema.ts
@@ -12,7 +12,7 @@ export const pathSupportedKeys = [
] as const;
const serverConfig = type({
- host: 'string.ip = "127.0.0.1"',
+ host: 'string.ip = "0.0.0.0"',
port: 'number.integer = 3000',
data_path: 'string.lower = "/var/lib/headplane/"',
@@ -65,8 +65,8 @@ const oidcConfig = type({
redirect_uri: 'string.url?',
disable_api_key_login: 'boolean = false',
scope: 'string = "openid email profile"',
- extra_params: 'Record?',
profile_picture_source: '"oidc" | "gravatar" = "oidc"',
+ extra_params: 'Record?',
authorization_endpoint: 'string.url?',
token_endpoint: 'string.url?',
diff --git a/app/server/index.ts b/app/server/index.ts
index 3a8fd79..ce239c1 100644
--- a/app/server/index.ts
+++ b/app/server/index.ts
@@ -8,7 +8,6 @@ import { createDbClient } from './db/client.server';
import { createHeadscaleInterface } from './headscale/api';
import { loadHeadscaleConfig } from './headscale/config-loader';
import { createHeadplaneAgent } from './hp-agent';
-import { configureOidcAuth } from './web/oidc';
import { createSessionStorage } from './web/sessions';
declare global {
@@ -40,6 +39,8 @@ const hsApi = await createHeadscaleInterface(
export type LoadContext = typeof appLoadContext;
import 'react-router';
+import { createOidcConnector } from './web/oidc-connector';
+
declare module 'react-router' {
interface AppLoadContext extends LoadContext {}
}
@@ -68,8 +69,8 @@ const appLoadContext = {
hsApi,
agents,
integration: await loadIntegration(config.integration),
- oidc: config.oidc
- ? await configureOidcAuth(
+ oidcConnector: config.oidc
+ ? await createOidcConnector(
config.oidc,
hsApi.getRuntimeClient(config.oidc.headscale_api_key),
)
diff --git a/app/server/web/oidc-connector.ts b/app/server/web/oidc-connector.ts
new file mode 100644
index 0000000..1f9d2fb
--- /dev/null
+++ b/app/server/web/oidc-connector.ts
@@ -0,0 +1,197 @@
+import * as oidc from 'openid-client';
+import log from '~/utils/log';
+import type { HeadplaneConfig } from '../config/config-schema';
+import type { RuntimeApiClient } from '../headscale/api/endpoints';
+import { isDataUnauthorizedError } from '../headscale/api/error-client';
+
+export type OidcConfig = NonNullable;
+
+/**
+ * Errors that can occur during OIDC connector setup and validation.
+ */
+export type OidcConnectorError =
+ | 'INVALID_API_KEY'
+ | 'MISSING_AUTHORIZATION_ENDPOINT'
+ | 'MISSING_TOKEN_ENDPOINT'
+ | 'MISSING_USERINFO_ENDPOINT'
+ | 'MISSING_REQUIRED_CLAIMS'
+ | 'UNKNOWN_ERROR';
+
+/**
+ * Represents a "configured" OIDC setup for Headplane.
+ * This may include mis-configured versions too and will surface error messages.
+ */
+export type OidcConnector =
+ | {
+ isValid: true;
+ isExclusive: boolean;
+ client: oidc.Configuration;
+ apiKey: string;
+ scope: string;
+ extraParams?: Record;
+ }
+ | {
+ isValid: false;
+ isExclusive: false;
+ errors: OidcConnectorError[];
+ };
+
+/**
+ * Creates an OIDC connector based on the configuration and Headscale API.
+ * This will attempt to validate the configuration and return any errors.
+ *
+ * @param config The OIDC configuration.
+ * @param client The Headscale runtime API client.
+ * @returns An OIDC connector with validation status.
+ */
+export async function createOidcConnector(
+ config: OidcConfig,
+ client: RuntimeApiClient,
+): Promise {
+ // TODO: MEANINGFUL LOGS NOT JUST DEBUG SPAM LOL
+ //
+ const errors: OidcConnectorError[] = [];
+ if (!config.headscale_api_key) {
+ errors.push('INVALID_API_KEY');
+ return {
+ isValid: false,
+ isExclusive: false,
+ errors,
+ };
+ }
+
+ try {
+ await client.getApiKeys();
+ } catch (error) {
+ if (isDataUnauthorizedError(error)) {
+ errors.push('INVALID_API_KEY');
+ return {
+ isValid: false,
+ isExclusive: false,
+ errors,
+ };
+ }
+
+ // MARK: Otherwise assume the API key is valid since the API request
+ // failed for another reason that isn't 401 and we are optimistic
+ }
+
+ const oidcClientOrErrors = await discoveryCoalesce(config);
+ if (Array.isArray(oidcClientOrErrors)) {
+ errors.push(...oidcClientOrErrors);
+ return {
+ isValid: false,
+ isExclusive: false,
+ errors,
+ };
+ }
+
+ return {
+ isValid: true,
+ isExclusive: config.disable_api_key_login,
+ client: oidcClientOrErrors,
+ apiKey: config.headscale_api_key,
+ scope: config.scope,
+ extraParams: config.extra_params,
+ };
+}
+
+/**
+ * Runs OIDC discovery and coalesces the results with the provided config.
+ * We treat the manually supplied values as overrides to discovery.
+ *
+ * @param config The OIDC configuration.
+ * @returns The coalesced OIDC configuration or an array of errors.
+ */
+async function discoveryCoalesce(
+ config: OidcConfig,
+): Promise {
+ let metadata: oidc.ServerMetadata;
+ try {
+ const client = await oidc.discovery(
+ new URL(config.issuer),
+ config.client_id,
+ );
+ metadata = client.serverMetadata();
+ if (metadata.claims_supported != null) {
+ if (!metadata.claims_supported.includes('sub')) {
+ log.error('config', 'OIDC provider does not support `sub` claim');
+ return ['MISSING_REQUIRED_CLAIMS'];
+ }
+
+ if (!metadata.claims_supported.includes('name')) {
+ if (
+ !(
+ metadata.claims_supported.includes('given_name') &&
+ metadata.claims_supported.includes('family_name')
+ )
+ ) {
+ log.warn(
+ 'config',
+ 'OIDC provider does not support `name`, `given_name`, or `family_name` claims',
+ );
+ }
+ }
+
+ if (
+ !metadata.claims_supported.includes('preferred_username') &&
+ !metadata.claims_supported.includes('email')
+ ) {
+ log.warn(
+ 'config',
+ 'OIDC provider does not support `preferred_username` or `email` claims',
+ );
+ }
+ }
+ } catch {
+ log.error(
+ 'config',
+ 'Failed to auto-configure OIDC endpoints via discovery',
+ );
+ log.warn(
+ 'config',
+ 'OIDC server may not support discovery, using manual config',
+ );
+ metadata = {
+ issuer: config.issuer,
+ };
+ }
+
+ const errors: OidcConnectorError[] = [];
+ const authorization_endpoint =
+ config.authorization_endpoint ?? metadata.authorization_endpoint;
+
+ const token_endpoint = config.token_endpoint ?? metadata.token_endpoint;
+
+ const userinfo_endpoint =
+ config.userinfo_endpoint ?? metadata.userinfo_endpoint;
+
+ if (!authorization_endpoint) {
+ errors.push('MISSING_AUTHORIZATION_ENDPOINT');
+ }
+
+ if (!token_endpoint) {
+ errors.push('MISSING_TOKEN_ENDPOINT');
+ }
+
+ if (!userinfo_endpoint) {
+ errors.push('MISSING_USERINFO_ENDPOINT');
+ }
+
+ if (errors.length > 0) {
+ return errors;
+ }
+
+ const oidcClient = new oidc.Configuration(
+ {
+ issuer: config.issuer,
+ authorization_endpoint,
+ token_endpoint,
+ userinfo_endpoint,
+ },
+ config.client_id,
+ config.client_secret,
+ );
+
+ return oidcClient;
+}
diff --git a/app/server/web/oidc.ts b/app/server/web/oidc.ts
deleted file mode 100644
index 0c43b6f..0000000
--- a/app/server/web/oidc.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import * as oidc from 'openid-client';
-import log from '~/utils/log';
-import { HeadplaneConfig } from '../config/schema';
-import type { RuntimeApiClient } from '../headscale/api/endpoints';
-import { isDataUnauthorizedError } from '../headscale/api/error-client';
-
-export type OidcConfig = NonNullable;
-export type OidcConfigError = string;
-
-export async function configureOidcAuth(
- config: OidcConfig,
- client: RuntimeApiClient,
-): Promise {
- // Don't waste any of our time if the OIDC API key is invalid
- try {
- await client.getApiKeys();
- } catch (error) {
- if (isDataUnauthorizedError(error)) {
- return [
- 'The supplied API key for OIDC is invalid.',
- 'OIDC will be disabled until a valid API key is given',
- ].join(' ');
- }
-
- // MARK: Otherwise assume the API key is valid since the API request
- // failed for another reason that isn't 401
- }
-
- log.debug('config', 'Running OIDC discovery for %s', config.issuer);
- let clientAuthMethod: oidc.ClientAuth;
- switch (config.token_endpoint_auth_method) {
- case 'client_secret_basic':
- clientAuthMethod = oidc.ClientSecretBasic(config.client_secret!);
- break;
- case 'client_secret_post':
- clientAuthMethod = oidc.ClientSecretPost(config.client_secret!);
- break;
- case 'client_secret_jwt':
- clientAuthMethod = oidc.ClientSecretJwt(config.client_secret!);
- break;
- default:
- // MARK: Throwing because this is a developer skill issue
- throw new Error('Invalid client authentication method');
- }
-
- let oidcClient: oidc.Configuration;
- try {
- const discovery = await oidc.discovery(
- new URL(config.issuer),
- config.client_id,
- config.client_secret!, // TODO: Fix this config schema
- clientAuthMethod,
- );
-
- const meta = discovery.serverMetadata();
- if (!meta.authorization_endpoint) {
- log.error(
- 'config',
- 'Issuer discovery did not return `authorization_endpoint`',
- );
- 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 'OIDC provider did not return `authorization_endpoint`, please check logs';
- }
-
- if (!meta.token_endpoint) {
- log.error('config', 'Issuer discovery did not return `token_endpoint`');
- 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 'OIDC provider did not return `token_endpoint`, please check logs';
- }
-
- if (!meta.userinfo_endpoint) {
- log.error(
- 'config',
- 'Issuer discovery did not return `userinfo_endpoint`',
- );
- log.error('config', 'OIDC server does not support user info endpoint');
- log.error('config', 'You may need to set this manually in the config');
- return 'OIDC provider did not return `user_info`, please check logs';
- }
-
- if (meta.token_endpoint_auth_methods_supported) {
- if (
- !meta.token_endpoint_auth_methods_supported.includes(
- config.token_endpoint_auth_method,
- )
- ) {
- log.error(
- 'config',
- 'OIDC server does not support client authentication method %s',
- config.token_endpoint_auth_method,
- );
- log.error(
- 'config',
- 'Supported methods: %s',
- meta.token_endpoint_auth_methods_supported.join(', '),
- );
-
- return [
- 'Headplane is expecting the following client authencation method:',
- config.token_endpoint_auth_method,
- 'while the OIDC server only supports',
- `${meta.token_endpoint_auth_methods_supported.join(', ')}.`,
- 'OIDC wil be disabled until configured correctly.',
- ].join(' ');
- }
- }
-
- log.debug('config', 'OIDC discovery successful');
- log.debug(
- 'config',
- 'Authorization endpoint: %s',
- meta.authorization_endpoint,
- );
- log.debug('config', 'Token endpoint: %s', meta.token_endpoint);
- log.debug('config', 'Userinfo endpoint: %s', meta.userinfo_endpoint);
-
- // Manually construct the endpoints to coalesce with config if needed
- oidcClient = new oidc.Configuration(
- {
- 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 'OIDC provider could not be configured, please check logs.';
- }
-
- 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;
-}
diff --git a/app/utils/oidc.ts b/app/utils/oidc.ts
deleted file mode 100644
index 70e772e..0000000
--- a/app/utils/oidc.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import * as client from 'openid-client';
-import { Configuration, IDToken, UserInfoResponse } from 'openid-client';
-import log from '~/utils/log';
-
-// 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);
- let host = req.headers.get('Host');
- if (!host) {
- host = req.headers.get('X-Forwarded-Host');
- }
-
- if (!host) {
- log.error('auth', 'Unable to find a host header');
- log.error('auth', 'Ensure either Host or X-Forwarded-Host is set');
- throw new Error('Could not determine reverse proxy host');
- }
-
- const proto = req.headers.get('X-Forwarded-Proto');
- if (!proto) {
- log.warn('auth', 'No X-Forwarded-Proto header found');
- log.warn('auth', 'Assuming your Headplane instance runs behind HTTP');
- }
-
- url.protocol = proto ?? 'http:';
- url.host = host;
- return url.href;
-}
-
-export async function beginAuthFlow(
- config: Configuration,
- redirect_uri: string,
- scope: string,
- extra_params: Record = {},
-) {
- const codeVerifier = client.randomPKCECodeVerifier();
- const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
-
- const params: Record = {
- ...extra_params,
- scope,
- redirect_uri,
- code_challenge: codeChallenge,
- code_challenge_method: 'S256',
- state: client.randomState(),
- };
-
- if (!config.serverMetadata().supportsPKCE()) {
- params.nonce = client.randomNonce();
- }
-
- const url = client.buildAuthorizationUrl(config, params);
- return {
- url: url.href,
- codeVerifier,
- state: params.state,
- nonce: params.nonce ?? '',
- };
-}
-
-interface FlowOptions {
- redirect_uri: string;
- code_verifier: string;
- state: string;
- nonce?: string;
-}
-
-export interface FlowUser {
- subject: string;
- name: string;
- email: string | undefined;
- username: string | undefined;
- picture: string | undefined;
-}
-
-export async function finishAuthFlow(
- config: Configuration,
- options: FlowOptions,
-): Promise {
- const tokens = await client.authorizationCodeGrant(
- config,
- new URL(options.redirect_uri),
- {
- pkceCodeVerifier: options.code_verifier,
- expectedNonce: options.nonce,
- expectedState: options.state,
- idTokenExpected: true,
- },
- );
-
- const claims = tokens.claims();
- if (!claims?.sub) {
- throw new Error('No subject found in OIDC claims');
- }
-
- const user = await client.fetchUserInfo(
- config,
- tokens.access_token,
- claims.sub,
- );
-
- return {
- subject: user.sub,
- name: getName(user, claims),
- email: user.email ?? claims.email?.toString(),
- username: calculateUsername(claims, user),
- picture: user.picture,
- };
-}
-
-function calculateUsername(claims: IDToken, user: UserInfoResponse) {
- if (user.preferred_username) {
- return user.preferred_username;
- }
-
- if (
- claims.preferred_username &&
- typeof claims.preferred_username === 'string'
- ) {
- return claims.preferred_username;
- }
-
- if (user.email) {
- return user.email.split('@')[0];
- }
-
- if (claims.email && typeof claims.email === 'string') {
- return claims.email.split('@')[0];
- }
-
- return;
-}
-
-function getName(user: client.UserInfoResponse, claims: client.IDToken) {
- if (user.name) {
- return user.name;
- }
-
- if (claims.name && typeof claims.name === 'string') {
- return claims.name;
- }
-
- if (user.given_name && user.family_name) {
- return `${user.given_name} ${user.family_name}`;
- }
-
- if (user.preferred_username) {
- return user.preferred_username;
- }
-
- if (
- claims.preferred_username &&
- typeof claims.preferred_username === 'string'
- ) {
- return claims.preferred_username;
- }
-
- return 'Anonymous';
-}
-
-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('auth', 'Unknown error: %s', error);
- return {
- code: 500,
- error: {
- name: 'Internal Server Error',
- description: 'An unknown error occurred',
- },
- };
-}
diff --git a/package.json b/package.json
index f50c990..6702e71 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "react-router build",
- "dev": "HEADPLANE_LOAD_ENV_OVERRIDES=true HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev",
+ "dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev",
"start": "node build/server/index.js",
"typecheck": "react-router typegen && tsgo",
"test:unit": "vitest run --project unit",
@@ -41,7 +41,7 @@
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
- "@typescript/native-preview": "7.0.0-dev.20251116.1",
+ "@typescript/native-preview": "7.0.0-dev.20251203.1",
"@uiw/codemirror-theme-github": "4.25.1",
"@uiw/codemirror-theme-xcode": "4.25.3",
"@uiw/react-codemirror": "4.25.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4c45158..67420c6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -78,8 +78,8 @@ importers:
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.5)
'@typescript/native-preview':
- specifier: 7.0.0-dev.20251116.1
- version: 7.0.0-dev.20251116.1
+ specifier: 7.0.0-dev.20251203.1
+ version: 7.0.0-dev.20251203.1
'@uiw/codemirror-theme-github':
specifier: 4.25.1
version: 4.25.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.7)
@@ -2344,43 +2344,43 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
- '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-h9IiIyFUzFVOTFCI5DsE0mP70is3lFk6iujnZWpB9Xi2JG2g8H7ltXoBPL43ANPG3OEHQA+N7lEb/t4TliaVFg==}
+ '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-gMPW/y89KANC0fIqdudxwsxUOHTOyujaOGxyj4IOaFLIP+8/gofawsmdf9HVniPq4xCT7tMpiqa/b9btxJ5nGw==}
cpu: [arm64]
os: [darwin]
- '@typescript/native-preview-darwin-x64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-y18xG8zbI8ryCybDRdKc9d7o9eSR38aGNZt02vpEHnPfWgJUP89lTJKhUDm0S2LoEDgVo3ur0Or4jlJh38rPmQ==}
+ '@typescript/native-preview-darwin-x64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-BG/bCAZcTGNM4bQMwY4hI0l70QaxQW9qwJ04GZJL02LAnaQVai8o5X/ghU6awkXFt9CTXdXBWn+hKNb+IiHG+w==}
cpu: [x64]
os: [darwin]
- '@typescript/native-preview-linux-arm64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-eQH1xdStw2yGqLCQtBjNvc5sYI/tYe2qIvpCpmR8LbMWLs5518vYnOrLXMs+PlAZ1zEvnY0qlxm9/4HBacrRpg==}
+ '@typescript/native-preview-linux-arm64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-UGrYYbYbyjlklDubWE93E84WU6jrCGsjpJ2+/GFf4keB3IUrvg9lRqgQ3DUYW4p5kXJR+YC42HnG+OXkN+s6Pw==}
cpu: [arm64]
os: [linux]
- '@typescript/native-preview-linux-arm@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-hyXKS9An1gs+vJI4adTT6COiMbqDy3sHxc7qd2f3FhK4n1Up+Lpo8Psy0615rt0Zu35hKNLtiMZx+ymyrqgQ2A==}
+ '@typescript/native-preview-linux-arm@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-iGrXfVUWtXgiaiVX7FhFVYFz3qFklta2kQEx0VX+km/tBVFMt+egI36tflKYuR7UE5n+0kboKldtyKgmfGrrjQ==}
cpu: [arm]
os: [linux]
- '@typescript/native-preview-linux-x64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-CRKIu9IlvgmK5rDrLu+WviUy2lrqCFP4p5n3dtpzZuobgwI2TYuRduGDDq+qXgfdAVUpQkZOHUUQvF9PmjrNmw==}
+ '@typescript/native-preview-linux-x64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-s3VZtFQGktU1ph0q3v8T2tVsgTrRoiaWFknt2vrErxKnzfQgChWOlM0o7Geaj/y1dnrOGWO/cHwMCSb7vd2fgw==}
cpu: [x64]
os: [linux]
- '@typescript/native-preview-win32-arm64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-FljNjkaA/0KMRUkp/ZTqcQNkqc8PNDgxSKm1lirtdkWgVXXurxyKIkKjZdOgxZchOWDwuqy5ZW4epe2ysTm5lQ==}
+ '@typescript/native-preview-win32-arm64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-rcaW7Kn7Ja8J17wmc9UuOjf0LmlqPQYYnqQTdh/kj72FcK4l+8P7b1LcViQFcsOAiIcRZBKrEVZnZXNQxYdHMQ==}
cpu: [arm64]
os: [win32]
- '@typescript/native-preview-win32-x64@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-a2aDDFlgpPU0vYgmDkeABsWYdKGv1bYtggZ9FpDls8m7FmxdY0i/5SyPhkp/fDgW6qEEiOfcd4e/32zubcl3Gg==}
+ '@typescript/native-preview-win32-x64@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-+DSMCGE7VZWjYzbWDuIenBW2tUclUqAGi7pcOgGq3BpwPEqyVhdQh5ggOk087xgshBv5Wj/yAdXA9gQFAFxpEQ==}
cpu: [x64]
os: [win32]
- '@typescript/native-preview@7.0.0-dev.20251116.1':
- resolution: {integrity: sha512-ZfMvOGyzDSN3PxUYOSbx3jqszetxLSzLQkS+37oY40gX1YnGK8oZNf4ljVdGqYWpyRloYVREB8YbAY+UxLZxhQ==}
+ '@typescript/native-preview@7.0.0-dev.20251203.1':
+ resolution: {integrity: sha512-u6kHGmbkB4WQ2XjQUVq6PixV92biRclTBAq8r09L/MGzsiVREdYzf/Bf1W4aTDcDSu6UQ3hjtBR6hROQRPrMXQ==}
hasBin: true
'@uiw/codemirror-extensions-basic-setup@4.25.3':
@@ -7045,36 +7045,36 @@ snapshots:
dependencies:
'@types/node': 24.10.1
- '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-darwin-x64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-darwin-x64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-linux-arm64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-linux-arm64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-linux-arm@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-linux-arm@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-linux-x64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-linux-x64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-win32-arm64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-win32-arm64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview-win32-x64@7.0.0-dev.20251116.1':
+ '@typescript/native-preview-win32-x64@7.0.0-dev.20251203.1':
optional: true
- '@typescript/native-preview@7.0.0-dev.20251116.1':
+ '@typescript/native-preview@7.0.0-dev.20251203.1':
optionalDependencies:
- '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251116.1
- '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251116.1
- '@typescript/native-preview-linux-arm': 7.0.0-dev.20251116.1
- '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251116.1
- '@typescript/native-preview-linux-x64': 7.0.0-dev.20251116.1
- '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251116.1
- '@typescript/native-preview-win32-x64': 7.0.0-dev.20251116.1
+ '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-linux-arm': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-linux-x64': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251203.1
+ '@typescript/native-preview-win32-x64': 7.0.0-dev.20251203.1
'@uiw/codemirror-extensions-basic-setup@4.25.3(@codemirror/autocomplete@6.18.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.7)(@lezer/common@1.3.0))(@codemirror/commands@6.10.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.7)(@codemirror/state@6.5.2)(@codemirror/view@6.38.7)':
dependencies: