Path: blob/main/src/vs/workbench/api/node/proxyResolver.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IExtHostWorkspaceProvider } from '../common/extHostWorkspace.js';6import { ConfigurationInspect, ExtHostConfigProvider } from '../common/extHostConfiguration.js';7import { MainThreadTelemetryShape } from '../common/extHost.protocol.js';8import { IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js';9import { ExtHostExtensionService } from './extHostExtensionService.js';10import { URI } from '../../../base/common/uri.js';11import { ILogService, LogLevel as LogServiceLevel } from '../../../platform/log/common/log.js';12import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';13import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates, ResolveProxyWithRequest } from '@vscode/proxy-agent';14import { AuthInfo, systemCertificatesNodeDefault } from '../../../platform/request/common/request.js';15import { DisposableStore } from '../../../base/common/lifecycle.js';16import { createRequire } from 'node:module';17import type * as undiciType from 'undici-types';18import type * as tlsType from 'tls';19import { lookupKerberosAuthorization } from '../../../platform/request/node/requestService.js';20import * as proxyAgent from '@vscode/proxy-agent';2122const require = createRequire(import.meta.url);23const http = require('http');24const https = require('https');25const tls: typeof tlsType = require('tls');26const net = require('net');2728const systemCertificatesV2Default = false;29const useElectronFetchDefault = false;3031export function connectProxyResolver(32extHostWorkspace: IExtHostWorkspaceProvider,33configProvider: ExtHostConfigProvider,34extensionService: ExtHostExtensionService,35extHostLogService: ILogService,36mainThreadTelemetry: MainThreadTelemetryShape,37initData: IExtensionHostInitData,38disposables: DisposableStore,39) {4041const isRemote = initData.remote.isRemote;42const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote;43const fallbackToLocalKerberos = useHostProxyDefault;44const loadLocalCertificates = useHostProxyDefault;45const isUseHostProxyEnabled = () => !isRemote || configProvider.getConfiguration('http').get<boolean>('useLocalProxyConfiguration', useHostProxyDefault);46const timedResolveProxy = createTimedResolveProxy(extHostWorkspace, mainThreadTelemetry);47const params: ProxyAgentParams = {48resolveProxy: timedResolveProxy,49lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos),50getProxyURL: () => getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxy'),51getProxySupport: () => getExtHostConfigValue<ProxySupportSetting>(configProvider, isRemote, 'http.proxySupport') || 'off',52getNoProxyConfig: () => getExtHostConfigValue<string[]>(configProvider, isRemote, 'http.noProxy') || [],53isAdditionalFetchSupportEnabled: () => getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.fetchAdditionalSupport', true),54addCertificatesV1: () => certSettingV1(configProvider, isRemote),55addCertificatesV2: () => certSettingV2(configProvider, isRemote),56loadSystemCertificatesFromNode: () => getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificatesNode', systemCertificatesNodeDefault),57log: extHostLogService,58getLogLevel: () => {59const level = extHostLogService.getLevel();60switch (level) {61case LogServiceLevel.Trace: return LogLevel.Trace;62case LogServiceLevel.Debug: return LogLevel.Debug;63case LogServiceLevel.Info: return LogLevel.Info;64case LogServiceLevel.Warning: return LogLevel.Warning;65case LogServiceLevel.Error: return LogLevel.Error;66case LogServiceLevel.Off: return LogLevel.Off;67default: return never(level);68}69function never(level: never) {70extHostLogService.error('Unknown log level', level);71return LogLevel.Debug;72}73},74proxyResolveTelemetry: () => { },75isUseHostProxyEnabled,76getNetworkInterfaceCheckInterval: () => {77const intervalSeconds = getExtHostConfigValue<number>(configProvider, isRemote, 'http.experimental.networkInterfaceCheckInterval', 300);78return intervalSeconds * 1000;79},80loadAdditionalCertificates: async () => {81const useNodeSystemCerts = getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificatesNode', systemCertificatesNodeDefault);82const promises: Promise<string[]>[] = [];83if (isRemote) {84promises.push(loadSystemCertificates({85loadSystemCertificatesFromNode: () => useNodeSystemCerts,86log: extHostLogService,87}));88}89if (loadLocalCertificates) {90if (!isRemote && useNodeSystemCerts) {91promises.push(loadSystemCertificates({92loadSystemCertificatesFromNode: () => useNodeSystemCerts,93log: extHostLogService,94}));95} else {96extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading certificates from main process');97const certs = extHostWorkspace.loadCertificates(); // Loading from main process to share cache.98certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length));99promises.push(certs);100}101}102// Using https.globalAgent because it is shared with proxy.test.ts and mutable.103if (initData.environment.extensionTestsLocationURI && https.globalAgent.testCertificates?.length) {104extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates');105promises.push(Promise.resolve(https.globalAgent.testCertificates as string[]));106}107const result = (await Promise.all(promises)).flat();108mainThreadTelemetry.$publicLog2<AdditionalCertificatesEvent, AdditionalCertificatesClassification>('additionalCertificates', {109count: result.length,110isRemote,111loadLocalCertificates,112useNodeSystemCerts,113});114return result;115},116env: process.env,117};118const { resolveProxyWithRequest, resolveProxyURL } = createProxyResolver(params);119// eslint-disable-next-line local/code-no-any-casts120const target = (proxyAgent as any).default || proxyAgent;121target.resolveProxyURL = resolveProxyURL;122123patchGlobalFetch(params, configProvider, mainThreadTelemetry, initData, resolveProxyURL, disposables);124125const lookup = createPatchedModules(params, resolveProxyWithRequest);126return configureModuleLoading(extensionService, lookup);127}128129const unsafeHeaders = [130'content-length',131'host',132'trailer',133'te',134'upgrade',135'cookie2',136'keep-alive',137'transfer-encoding',138'set-cookie',139];140141function patchGlobalFetch(params: ProxyAgentParams, configProvider: ExtHostConfigProvider, mainThreadTelemetry: MainThreadTelemetryShape, initData: IExtensionHostInitData, resolveProxyURL: (url: string) => Promise<string | undefined>, disposables: DisposableStore) {142// eslint-disable-next-line local/code-no-any-casts143if (!(globalThis as any).__vscodeOriginalFetch) {144const originalFetch = globalThis.fetch;145// eslint-disable-next-line local/code-no-any-casts146(globalThis as any).__vscodeOriginalFetch = originalFetch;147const patchedFetch = proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL);148// eslint-disable-next-line local/code-no-any-casts149(globalThis as any).__vscodePatchedFetch = patchedFetch;150let useElectronFetch = false;151if (!initData.remote.isRemote) {152useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);153disposables.add(configProvider.onDidChangeConfiguration(e => {154if (e.affectsConfiguration('http.electronFetch')) {155useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);156}157}));158}159// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API160globalThis.fetch = async function fetch(input: string | URL | Request, init?: RequestInit) {161function getRequestProperty(name: keyof Request & keyof RequestInit) {162return init && name in init ? init[name] : typeof input === 'object' && 'cache' in input ? input[name] : undefined;163}164// Limitations: https://github.com/electron/electron/pull/36733#issuecomment-1405615494165// net.fetch fails on manual redirect: https://github.com/electron/electron/issues/43715166const urlString = typeof input === 'string' ? input : 'cache' in input ? input.url : input.toString();167const isDataUrl = urlString.startsWith('data:');168if (isDataUrl) {169recordFetchFeatureUse(mainThreadTelemetry, 'data');170}171const isBlobUrl = urlString.startsWith('blob:');172if (isBlobUrl) {173recordFetchFeatureUse(mainThreadTelemetry, 'blob');174}175const isManualRedirect = getRequestProperty('redirect') === 'manual';176if (isManualRedirect) {177recordFetchFeatureUse(mainThreadTelemetry, 'manualRedirect');178}179const integrity = getRequestProperty('integrity');180if (integrity) {181recordFetchFeatureUse(mainThreadTelemetry, 'integrity');182}183if (!useElectronFetch || isDataUrl || isBlobUrl || isManualRedirect || integrity) {184const response = await patchedFetch(input, init);185monitorResponseProperties(mainThreadTelemetry, response, urlString);186return response;187}188// Unsupported headers: https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc;l=32;drc=ee7299f8961a1b05a3554efcc496b6daa0d7f6e1189if (init?.headers) {190const headers = new Headers(init.headers);191for (const header of unsafeHeaders) {192headers.delete(header);193}194init = { ...init, headers };195}196// Support for URL: https://github.com/electron/electron/issues/43712197const electronInput = input instanceof URL ? input.toString() : input;198const electron = require('electron');199const response = await electron.net.fetch(electronInput, init);200monitorResponseProperties(mainThreadTelemetry, response, urlString);201return response;202};203}204}205206function monitorResponseProperties(mainThreadTelemetry: MainThreadTelemetryShape, response: Response, urlString: string) {207const originalUrl = response.url;208Object.defineProperty(response, 'url', {209get() {210recordFetchFeatureUse(mainThreadTelemetry, 'url');211return originalUrl || urlString;212}213});214const originalType = response.type;215Object.defineProperty(response, 'type', {216get() {217recordFetchFeatureUse(mainThreadTelemetry, 'typeProperty');218return originalType !== 'default' ? originalType : 'basic';219}220});221}222223type FetchFeatureUseClassification = {224owner: 'chrmarti';225comment: 'Data about fetch API use';226url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the url property was used.' };227typeProperty: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the type property was used.' };228data: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a data URL was used.' };229blob: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a blob URL was used.' };230integrity: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the integrity property was used.' };231manualRedirect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a manual redirect was used.' };232};233234type FetchFeatureUseEvent = {235url: number;236typeProperty: number;237data: number;238blob: number;239integrity: number;240manualRedirect: number;241};242243const fetchFeatureUse: FetchFeatureUseEvent = {244url: 0,245typeProperty: 0,246data: 0,247blob: 0,248integrity: 0,249manualRedirect: 0,250};251252let timer: Timeout | undefined;253const enableFeatureUseTelemetry = false;254function recordFetchFeatureUse(mainThreadTelemetry: MainThreadTelemetryShape, feature: keyof typeof fetchFeatureUse) {255if (enableFeatureUseTelemetry && !fetchFeatureUse[feature]++) {256if (timer) {257clearTimeout(timer);258}259timer = setTimeout(() => {260mainThreadTelemetry.$publicLog2<FetchFeatureUseEvent, FetchFeatureUseClassification>('fetchFeatureUse', fetchFeatureUse);261}, 10000); // collect additional features for 10 seconds262(timer as unknown as NodeJS.Timeout).unref?.();263}264}265266type AdditionalCertificatesClassification = {267owner: 'chrmarti';268comment: 'Tracks the number of additional certificates loaded for TLS connections';269count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of additional certificates loaded' };270isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether this is a remote extension host' };271loadLocalCertificates: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether local certificates are loaded' };272useNodeSystemCerts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether Node.js system certificates are used' };273};274275type AdditionalCertificatesEvent = {276count: number;277isRemote: boolean;278loadLocalCertificates: boolean;279useNodeSystemCerts: boolean;280};281282type ProxyResolveStatsClassification = {283owner: 'chrmarti';284comment: 'Performance statistics for proxy resolution';285count: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Number of proxy resolution calls' };286totalDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total time spent in proxy resolution (ms)' };287minDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Minimum resolution time (ms)' };288maxDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Maximum resolution time (ms)' };289avgDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Average resolution time (ms)' };290};291292type ProxyResolveStatsEvent = {293count: number;294totalDuration: number;295minDuration: number;296maxDuration: number;297avgDuration: number;298};299300const proxyResolveStats = {301count: 0,302totalDuration: 0,303minDuration: Number.MAX_SAFE_INTEGER,304maxDuration: 0,305lastSentTime: 0,306};307308const telemetryInterval = 60 * 60 * 1000; // 1 hour309310function sendProxyResolveStats(mainThreadTelemetry: MainThreadTelemetryShape) {311if (proxyResolveStats.count > 0) {312const avgDuration = proxyResolveStats.totalDuration / proxyResolveStats.count;313mainThreadTelemetry.$publicLog2<ProxyResolveStatsEvent, ProxyResolveStatsClassification>('proxyResolveStats', {314count: proxyResolveStats.count,315totalDuration: proxyResolveStats.totalDuration,316minDuration: proxyResolveStats.minDuration,317maxDuration: proxyResolveStats.maxDuration,318avgDuration,319});320// Reset stats after sending321proxyResolveStats.count = 0;322proxyResolveStats.totalDuration = 0;323proxyResolveStats.minDuration = Number.MAX_SAFE_INTEGER;324proxyResolveStats.maxDuration = 0;325}326proxyResolveStats.lastSentTime = Date.now();327}328329function createTimedResolveProxy(extHostWorkspace: IExtHostWorkspaceProvider, mainThreadTelemetry: MainThreadTelemetryShape) {330return async (url: string): Promise<string | undefined> => {331const startTime = performance.now();332try {333return await extHostWorkspace.resolveProxy(url);334} finally {335const duration = performance.now() - startTime;336proxyResolveStats.count++;337proxyResolveStats.totalDuration += duration;338proxyResolveStats.minDuration = Math.min(proxyResolveStats.minDuration, duration);339proxyResolveStats.maxDuration = Math.max(proxyResolveStats.maxDuration, duration);340341// Send telemetry if at least an hour has passed since last send342const now = Date.now();343if (now - proxyResolveStats.lastSentTime >= telemetryInterval) {344sendProxyResolveStats(mainThreadTelemetry);345}346}347};348}349350function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolveProxyWithRequest) {351352function mergeModules(module: any, patch: any) {353const target = module.default || module;354target.__vscodeOriginal = Object.assign({}, target);355return Object.assign(target, patch);356}357358return {359http: mergeModules(http, createHttpPatch(params, http, resolveProxy)),360https: mergeModules(https, createHttpPatch(params, https, resolveProxy)),361net: mergeModules(net, createNetPatch(params, net)),362tls: mergeModules(tls, createTlsPatch(params, tls))363};364}365366function certSettingV1(configProvider: ExtHostConfigProvider, isRemote: boolean) {367return !getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');368}369370function certSettingV2(configProvider: ExtHostConfigProvider, isRemote: boolean) {371return !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');372}373374const modulesCache = new Map<IExtensionDescription | undefined, { http?: typeof http; https?: typeof https; undici?: typeof undiciType }>();375function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {376return extensionService.getExtensionPathIndex()377.then(extensionPaths => {378const node_module = require('module');379const original = node_module._load;380node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {381if (request === 'net') {382return lookup.net;383}384385if (request === 'tls') {386return lookup.tls;387}388389if (request !== 'http' && request !== 'https' && request !== 'undici') {390return original.apply(this, arguments);391}392393const ext = extensionPaths.findSubstr(URI.file(parent.filename));394let cache = modulesCache.get(ext);395if (!cache) {396modulesCache.set(ext, cache = {});397}398if (!cache[request]) {399if (request === 'undici') {400const undici = original.apply(this, arguments);401proxyAgent.patchUndici(undici);402cache[request] = undici;403} else {404const mod = lookup[request];405cache[request] = { ...mod }; // Copy to work around #93167.406}407}408return cache[request];409};410});411}412413async function lookupProxyAuthorization(414extHostWorkspace: IExtHostWorkspaceProvider,415extHostLogService: ILogService,416mainThreadTelemetry: MainThreadTelemetryShape,417configProvider: ExtHostConfigProvider,418proxyAuthenticateCache: Record<string, string | string[] | undefined>,419basicAuthCache: Record<string, string | undefined>,420isRemote: boolean,421fallbackToLocalKerberos: boolean,422proxyURL: string,423proxyAuthenticate: string | string[] | undefined,424state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number }425): Promise<string | undefined> {426proxyURL = proxyURL.replace(/\/+$/, '');427const cached = proxyAuthenticateCache[proxyURL];428if (proxyAuthenticate) {429proxyAuthenticateCache[proxyURL] = proxyAuthenticate;430}431extHostLogService.trace('ProxyResolver#lookupProxyAuthorization callback', `proxyURL:${proxyURL}`, `proxyAuthenticate:${proxyAuthenticate}`, `proxyAuthenticateCache:${cached}`);432const header = proxyAuthenticate || cached;433const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : [];434sendTelemetry(mainThreadTelemetry, authenticate, isRemote);435if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) {436state.kerberosRequested = true;437438try {439const spnConfig = getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxyKerberosServicePrincipal');440const response = await lookupKerberosAuthorization(proxyURL, spnConfig, extHostLogService, 'ProxyResolver#lookupProxyAuthorization');441return 'Negotiate ' + response;442} catch (err) {443extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err);444}445446if (isRemote && fallbackToLocalKerberos) {447extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`);448const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL);449if (auth) {450return 'Negotiate ' + auth;451}452}453}454const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a));455if (basicAuthHeader) {456try {457const cachedAuth = basicAuthCache[proxyURL];458if (cachedAuth) {459if (state.basicAuthCacheUsed) {460extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication deleting cached credentials', `proxyURL:${proxyURL}`);461delete basicAuthCache[proxyURL];462} else {463extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication using cached credentials', `proxyURL:${proxyURL}`);464state.basicAuthCacheUsed = true;465return cachedAuth;466}467}468state.basicAuthAttempt = (state.basicAuthAttempt || 0) + 1;469const realm = / realm="([^"]+)"/i.exec(basicAuthHeader)?.[1];470extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication lookup', `proxyURL:${proxyURL}`, `realm:${realm}`);471const url = new URL(proxyURL);472const authInfo: AuthInfo = {473scheme: 'basic',474host: url.hostname,475port: Number(url.port),476realm: realm || '',477isProxy: true,478attempt: state.basicAuthAttempt,479};480const credentials = await extHostWorkspace.lookupAuthorization(authInfo);481if (credentials) {482extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);483const auth = 'Basic ' + Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');484basicAuthCache[proxyURL] = auth;485return auth;486} else {487extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received no credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);488}489} catch (err) {490extHostLogService.error('ProxyResolver#lookupProxyAuthorization Basic authentication failed', err);491}492}493return undefined;494}495496type ProxyAuthenticationClassification = {497owner: 'chrmarti';498comment: 'Data about proxy authentication requests';499authenticationType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Type of the authentication requested' };500extensionHostType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of the extension host' };501};502503type ProxyAuthenticationEvent = {504authenticationType: string;505extensionHostType: string;506};507508let telemetrySent = false;509const enableProxyAuthenticationTelemetry = false;510function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authenticate: string[], isRemote: boolean) {511if (!enableProxyAuthenticationTelemetry || telemetrySent || !authenticate.length) {512return;513}514telemetrySent = true;515516mainThreadTelemetry.$publicLog2<ProxyAuthenticationEvent, ProxyAuthenticationClassification>('proxyAuthenticationRequest', {517authenticationType: authenticate.map(a => a.split(' ')[0]).join(','),518extensionHostType: isRemote ? 'remote' : 'local',519});520}521522function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback: T): T;523function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string): T | undefined;524function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback?: T): T | undefined {525if (isRemote) {526return configProvider.getConfiguration().get<T>(key) ?? fallback;527}528const values: ConfigurationInspect<T> | undefined = configProvider.getConfiguration().inspect<T>(key);529return values?.globalLocalValue ?? values?.defaultValue ?? fallback;530}531532533