@auth0/auth0-api-js
Advanced tools
+348
-25
@@ -275,7 +275,74 @@ "use strict"; | ||
| // src/lru-cache.ts | ||
| var LruCache = class { | ||
| #map; | ||
| #inflight; | ||
| #ttlMs; | ||
| #maxEntries; | ||
| constructor(ttlMs, maxEntries) { | ||
| this.#ttlMs = ttlMs; | ||
| this.#maxEntries = maxEntries; | ||
| this.#map = /* @__PURE__ */ new Map(); | ||
| this.#inflight = /* @__PURE__ */ new Map(); | ||
| } | ||
| get(key) { | ||
| const entry = this.#map.get(key); | ||
| if (!entry) { | ||
| return void 0; | ||
| } | ||
| if (this.#ttlMs !== void 0 && entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) { | ||
| this.#map.delete(key); | ||
| return void 0; | ||
| } | ||
| this.#map.delete(key); | ||
| this.#map.set(key, entry); | ||
| return entry.value; | ||
| } | ||
| set(key, value) { | ||
| const expiresAt = this.#ttlMs !== void 0 ? Date.now() + this.#ttlMs : void 0; | ||
| this.#map.set(key, { value, expiresAt }); | ||
| this.#enforceLimit(); | ||
| } | ||
| async getOrSet(key, loader) { | ||
| const cached = this.get(key); | ||
| if (cached !== void 0) { | ||
| return cached; | ||
| } | ||
| const inflight = this.#inflight.get(key); | ||
| if (inflight) { | ||
| return inflight; | ||
| } | ||
| const promise = (async () => { | ||
| const value = await loader(); | ||
| this.set(key, value); | ||
| return value; | ||
| })(); | ||
| const inflightPromise = promise.finally(() => { | ||
| this.#inflight.delete(key); | ||
| }); | ||
| this.#inflight.set(key, inflightPromise); | ||
| return inflightPromise; | ||
| } | ||
| #enforceLimit() { | ||
| if (this.#maxEntries === void 0) { | ||
| return; | ||
| } | ||
| while (this.#map.size > this.#maxEntries) { | ||
| const oldestKey = this.#map.keys().next().value; | ||
| if (oldestKey === void 0) { | ||
| break; | ||
| } | ||
| this.#map.delete(oldestKey); | ||
| } | ||
| } | ||
| }; | ||
| // src/api-client.ts | ||
| var ApiClient = class { | ||
| #serverMetadata; | ||
| #serverMetadataByDomain; | ||
| #options; | ||
| #jwks; | ||
| #jwksByUri; | ||
| #domains; | ||
| #algorithms; | ||
| #defaultDomainUrl; | ||
| #authClient; | ||
@@ -310,4 +377,59 @@ constructor(options) { | ||
| } | ||
| const discoveryCacheConfig = options.discoveryCache ?? {}; | ||
| const ttlSeconds = discoveryCacheConfig.ttl ?? 600; | ||
| if (!Number.isFinite(ttlSeconds)) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "ttl" must be a number'); | ||
| } | ||
| if (ttlSeconds < 0) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "ttl" must be a non-negative number'); | ||
| } | ||
| const cacheTtlMs = ttlSeconds * 1e3; | ||
| const maxEntries = discoveryCacheConfig.maxEntries ?? 100; | ||
| if (!Number.isFinite(maxEntries)) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "maxEntries" must be a number'); | ||
| } | ||
| if (maxEntries < 0) { | ||
| throw new InvalidConfigurationError( | ||
| 'Invalid discoveryCache configuration: "maxEntries" must be a non-negative number' | ||
| ); | ||
| } | ||
| this.#serverMetadataByDomain = new LruCache(cacheTtlMs, maxEntries); | ||
| this.#jwksByUri = new LruCache(cacheTtlMs, maxEntries); | ||
| this.#options = options; | ||
| if (options.domain !== void 0) { | ||
| try { | ||
| this.#defaultDomainUrl = normalizeDomain(options.domain); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw new InvalidConfigurationError(`Invalid domain configuration: ${message}`); | ||
| } | ||
| } | ||
| if (options.domains !== void 0) { | ||
| if (Array.isArray(options.domains)) { | ||
| if (options.domains.length === 0) { | ||
| throw new InvalidConfigurationError('Invalid domains configuration: "domains" must not be empty'); | ||
| } | ||
| const normalized = options.domains.map((domain) => { | ||
| try { | ||
| return normalizeDomain(domain); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw new InvalidConfigurationError(`Invalid domains configuration: ${message}`); | ||
| } | ||
| }); | ||
| this.#domains = Array.from(new Set(normalized)); | ||
| } else if (typeof options.domains === "function") { | ||
| this.#domains = options.domains; | ||
| } else { | ||
| throw new InvalidConfigurationError('Invalid domains configuration: "domains" must be an array or a function'); | ||
| } | ||
| } | ||
| this.#algorithms = normalizeAlgorithms(options.algorithms); | ||
| if (!this.#defaultDomainUrl && this.#domains === void 0) { | ||
| throw new MissingRequiredArgumentError("domain or domains"); | ||
| } | ||
| if (options.clientId) { | ||
| if (!options.domain) { | ||
| throw new MissingRequiredArgumentError("domain"); | ||
| } | ||
| this.#authClient = new import_auth0_auth_js.AuthClient({ | ||
@@ -329,16 +451,11 @@ domain: options.domain, | ||
| */ | ||
| async #discover() { | ||
| if (this.#serverMetadata) { | ||
| return { | ||
| serverMetadata: this.#serverMetadata | ||
| }; | ||
| } | ||
| const issuer = new URL(`https://${this.#options.domain}`); | ||
| const response = await oauth.discoveryRequest(issuer, { | ||
| [oauth.customFetch]: this.#options.customFetch | ||
| async #discoverDomain(domain) { | ||
| const serverMetadata = await this.#serverMetadataByDomain.getOrSet(domain, async () => { | ||
| const issuer = new URL(domain); | ||
| const response = await oauth.discoveryRequest(issuer, { | ||
| [oauth.customFetch]: this.#options.customFetch | ||
| }); | ||
| return oauth.processDiscoveryResponse(issuer, response); | ||
| }); | ||
| this.#serverMetadata = await oauth.processDiscoveryResponse(issuer, response); | ||
| return { | ||
| serverMetadata: this.#serverMetadata | ||
| }; | ||
| return { serverMetadata }; | ||
| } | ||
@@ -397,3 +514,3 @@ /** | ||
| const httpUrl = options.httpUrl; | ||
| const hasDpopParams = dpopProof !== void 0 || httpMethod !== void 0 || httpUrl !== void 0; | ||
| const hasDpopParams = dpopProof !== void 0 || httpMethod !== void 0 || this.#domains === void 0 && httpUrl !== void 0; | ||
| if (mode !== "disabled" && scheme && !["bearer", "dpop"].includes(scheme)) { | ||
@@ -428,13 +545,69 @@ const err = new InvalidRequestError(""); | ||
| } | ||
| const { serverMetadata } = await this.#discover(); | ||
| this.#jwks ||= (0, import_jose2.createRemoteJWKSet)(new URL(serverMetadata.jwks_uri), { | ||
| [import_jose2.customFetch]: this.#options.customFetch | ||
| }); | ||
| const accessToken = options.accessToken; | ||
| const domains = this.#domains; | ||
| let jwks; | ||
| let issuerForVerify = ""; | ||
| let unverifiedIss; | ||
| let alg; | ||
| const defaultDomainUrl = this.#defaultDomainUrl; | ||
| try { | ||
| const { payload } = await (0, import_jose2.jwtVerify)(options.accessToken, this.#jwks, { | ||
| issuer: this.#serverMetadata.issuer, | ||
| try { | ||
| const header = (0, import_jose2.decodeProtectedHeader)(accessToken); | ||
| const payload2 = (0, import_jose2.decodeJwt)(accessToken); | ||
| if (typeof header.alg === "string") { | ||
| alg = header.alg; | ||
| } | ||
| if (typeof payload2.iss === "string") { | ||
| unverifiedIss = payload2.iss; | ||
| } | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw this.#addChallenges(new VerifyAccessTokenError(message), mode, scheme); | ||
| } | ||
| if (alg && alg.toUpperCase().startsWith("HS")) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError("unsupported algorithm (symmetric algorithms are not supported)"), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (domains !== void 0) { | ||
| if (!unverifiedIss) { | ||
| throw this.#addChallenges(new VerifyAccessTokenError('missing required "iss" claim'), mode, scheme); | ||
| } | ||
| const context = { | ||
| url: httpUrl, | ||
| headers: options.headers, | ||
| unverifiedIss | ||
| }; | ||
| const allowedDomains = await this.#resolveDomains(domains, context, mode, scheme); | ||
| const matchedDomain = allowedDomains.find((domain) => domain === unverifiedIss); | ||
| if (!matchedDomain) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| 'unexpected "iss" claim value (issuer is not in the configured domain list)' | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| const { serverMetadata } = await this.#discoverDomain(matchedDomain); | ||
| const { issuer, jwksUri } = this.#requireDiscoveryMetadata(serverMetadata, mode, scheme); | ||
| issuerForVerify = issuer; | ||
| jwks = this.#getJwksForDomain(jwksUri); | ||
| } else if (defaultDomainUrl !== void 0) { | ||
| const { serverMetadata } = await this.#discoverDomain(defaultDomainUrl); | ||
| const { issuer, jwksUri } = this.#requireDiscoveryMetadata(serverMetadata, mode, scheme); | ||
| issuerForVerify = issuer; | ||
| jwks = this.#getJwksForDomain(jwksUri); | ||
| } else { | ||
| throw new MissingRequiredArgumentError("domain or domains"); | ||
| } | ||
| const jwtVerifyOptions = { | ||
| audience: this.#options.audience, | ||
| algorithms: ["RS256"], | ||
| requiredClaims: ["iat", "exp", ...options.requiredClaims || []] | ||
| }); | ||
| algorithms: options.algorithms ? normalizeAlgorithms(options.algorithms) : this.#algorithms, | ||
| requiredClaims: ["iat", "exp", ...options.requiredClaims || []], | ||
| issuer: issuerForVerify | ||
| }; | ||
| const { payload } = await (0, import_jose2.jwtVerify)(accessToken, jwks, jwtVerifyOptions); | ||
| let cnfJkt; | ||
@@ -518,2 +691,93 @@ const cnf = payload.cnf; | ||
| } | ||
| async #resolveDomains(domains, context, mode, scheme) { | ||
| if (Array.isArray(domains)) { | ||
| return domains; | ||
| } | ||
| let resolved; | ||
| try { | ||
| resolved = await domains(context); | ||
| } catch { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError("domain validation failed: domains resolver failed"), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (!Array.isArray(resolved)) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver must return an array of domain strings" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (resolved.length === 0) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver returned no allowed domains" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| const normalized = []; | ||
| for (const domain of resolved) { | ||
| if (typeof domain !== "string" || !domain.trim()) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver returned a non-string domain" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| try { | ||
| normalized.push(normalizeDomain(domain)); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw this.#addChallenges(new VerifyAccessTokenError(message), mode, scheme); | ||
| } | ||
| } | ||
| return Array.from(new Set(normalized)); | ||
| } | ||
| #requireDiscoveryMetadata(serverMetadata, mode, scheme) { | ||
| if (!serverMetadata.jwks_uri) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError('missing "jwks_uri" in discovery metadata'), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| return { issuer: serverMetadata.issuer, jwksUri: serverMetadata.jwks_uri }; | ||
| } | ||
| #getJwksForDomain(jwksUri) { | ||
| const existing = this.#jwksByUri.get(jwksUri); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
| const jwksUrl = new URL(jwksUri); | ||
| const jwks = (0, import_jose2.createRemoteJWKSet)(jwksUrl, { | ||
| [import_jose2.customFetch]: this.#createJwksFetch() | ||
| }); | ||
| this.#jwksByUri.set(jwksUri, jwks); | ||
| return jwks; | ||
| } | ||
| #createJwksFetch() { | ||
| const baseFetch = this.#options.customFetch ?? fetch; | ||
| return async (input, init) => { | ||
| try { | ||
| const response = await baseFetch(input, init); | ||
| if (!response.ok) { | ||
| throw new Error("JWKS request failed"); | ||
| } | ||
| return response; | ||
| } catch (error) { | ||
| if (error instanceof Error && error.message.startsWith("JWKS request failed")) { | ||
| throw error; | ||
| } | ||
| throw new Error("JWKS request failed"); | ||
| } | ||
| }; | ||
| } | ||
| #addChallenges(err, mode, scheme, params) { | ||
@@ -621,2 +885,61 @@ const authErr = err; | ||
| }; | ||
| function normalizeDomain(value) { | ||
| if (typeof value !== "string" || !value.trim()) { | ||
| throw new Error("domain must be a non-empty string"); | ||
| } | ||
| const trimmed = value.trim(); | ||
| let withScheme; | ||
| if (/^https?:\/\//i.test(trimmed)) { | ||
| if (!/^https:\/\//i.test(trimmed)) { | ||
| throw new Error("invalid domain URL (https required)"); | ||
| } | ||
| withScheme = trimmed; | ||
| } else { | ||
| withScheme = `https://${trimmed}`; | ||
| } | ||
| let domainUrl; | ||
| try { | ||
| domainUrl = new URL(withScheme); | ||
| } catch { | ||
| throw new Error("invalid domain URL"); | ||
| } | ||
| if (domainUrl.username || domainUrl.password) { | ||
| throw new Error("invalid domain URL (credentials are not allowed)"); | ||
| } | ||
| if (domainUrl.search || domainUrl.hash) { | ||
| throw new Error("invalid domain URL (query/fragment are not allowed)"); | ||
| } | ||
| domainUrl.hash = ""; | ||
| domainUrl.search = ""; | ||
| domainUrl.hostname = domainUrl.hostname.toLowerCase(); | ||
| if (domainUrl.pathname && domainUrl.pathname !== "/" && domainUrl.pathname !== "") { | ||
| throw new Error("invalid domain URL (path segments are not allowed)"); | ||
| } | ||
| domainUrl.pathname = "/"; | ||
| return domainUrl.toString(); | ||
| } | ||
| function normalizeAlgorithms(algorithms) { | ||
| if (algorithms === void 0) { | ||
| return ["RS256"]; | ||
| } | ||
| if (!Array.isArray(algorithms) || algorithms.length === 0) { | ||
| throw new InvalidConfigurationError('Invalid algorithms configuration: "algorithms" must be a non-empty array'); | ||
| } | ||
| const normalized = []; | ||
| for (const algorithm of algorithms) { | ||
| if (typeof algorithm !== "string" || !algorithm.trim()) { | ||
| throw new InvalidConfigurationError( | ||
| 'Invalid algorithms configuration: each "algorithms" entry must be a non-empty string' | ||
| ); | ||
| } | ||
| const trimmed = algorithm.trim(); | ||
| if (trimmed.toUpperCase().startsWith("HS")) { | ||
| throw new InvalidConfigurationError( | ||
| "Invalid algorithms configuration: symmetric algorithms are not allowed" | ||
| ); | ||
| } | ||
| normalized.push(trimmed); | ||
| } | ||
| return Array.from(new Set(normalized)); | ||
| } | ||
@@ -623,0 +946,0 @@ // src/protected-resource-metadata.ts |
+87
-9
| import * as jose from 'jose'; | ||
| export { MissingClientAuthError, TokenExchangeError } from '@auth0/auth0-auth-js'; | ||
| interface ApiClientOptions { | ||
| type DomainsResolverContext = { | ||
| /** | ||
| * The Auth0 domain to use for authentication. | ||
| * @example 'example.auth0.com' (without https://) | ||
| * Full request URL, if available. | ||
| * This is populated from `verifyAccessToken({ httpUrl })` when provided. | ||
| */ | ||
| domain: string; | ||
| url?: string; | ||
| /** | ||
| * HTTP request headers (lowercased keys recommended). | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Unverified issuer extracted from the token. | ||
| */ | ||
| unverifiedIss?: string; | ||
| }; | ||
| /** | ||
| * Resolver that returns a list of allowed domains for the current request. | ||
| */ | ||
| type DomainsResolver = (context: DomainsResolverContext) => Promise<string[]> | string[]; | ||
| /** | ||
| * Optional caching configuration for discovery metadata and JWKS fetchers. | ||
| * TTL is expressed in seconds. maxEntries controls the LRU size. | ||
| */ | ||
| interface DiscoveryCacheOptions { | ||
| ttl?: number; | ||
| maxEntries?: number; | ||
| } | ||
| type ApiClientCommonOptions = { | ||
| /** | ||
| * The expected JWT Access Token audience ("aud") value. | ||
@@ -39,2 +61,7 @@ */ | ||
| /** | ||
| * Optional list of allowed JWT algorithms for access token verification. | ||
| * Defaults to ['RS256'] when not provided. HS* values are rejected. | ||
| */ | ||
| algorithms?: string[]; | ||
| /** | ||
| * Demonstration of Proof-of-Possession (DPoP) configuration. | ||
@@ -45,3 +72,33 @@ * | ||
| dpop?: DPoPOptions; | ||
| } | ||
| /** | ||
| * Optional discovery cache configuration for OIDC metadata. | ||
| * TTL is in seconds. maxEntries controls the LRU size. | ||
| * Defaults when omitted: ttl = 600 seconds, maxEntries = 100. | ||
| */ | ||
| discoveryCache?: DiscoveryCacheOptions; | ||
| }; | ||
| type ApiClientOptions = (ApiClientCommonOptions & { | ||
| /** | ||
| * The Auth0 domain to use for authentication and non-verification flows. | ||
| * @example 'example.auth0.com' | ||
| */ | ||
| domain: string; | ||
| /** | ||
| * Optional domain allowlist or resolver for access token verification. | ||
| * When provided, access token verification uses this instead of `domain`. | ||
| * Provide domains as shown in the Auth0 Dashboard (e.g., "example.auth0.com"). | ||
| */ | ||
| domains?: string[] | DomainsResolver; | ||
| }) | (ApiClientCommonOptions & { | ||
| /** | ||
| * Domain allowlist or resolver for access token verification. | ||
| * Provide domains as shown in the Auth0 Dashboard (e.g., "example.auth0.com"). | ||
| */ | ||
| domains: string[] | DomainsResolver; | ||
| domain?: never; | ||
| clientId?: never; | ||
| clientSecret?: never; | ||
| clientAssertionSigningKey?: never; | ||
| clientAssertionSigningAlg?: never; | ||
| }); | ||
| interface AccessTokenForConnectionOptions { | ||
@@ -195,2 +252,6 @@ /** | ||
| /** | ||
| * HTTP request headers, used for domain resolution. | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Additional claims that are required to be present in the access token. | ||
@@ -208,5 +269,5 @@ */ | ||
| /** | ||
| * HTTP URL is not used for bearer validation. | ||
| * Full request URL, used for MCD domain resolution when available. | ||
| */ | ||
| httpUrl?: undefined; | ||
| httpUrl?: string; | ||
| /** | ||
@@ -216,2 +277,8 @@ * Optional scheme (e.g., 'bearer'); DPoP params must be absent. | ||
| scheme?: string; | ||
| /** | ||
| * The allowed asymmetric algorithms to use for verifying the access token's signature. | ||
| * | ||
| * Defaults to ['RS256'] if not provided. | ||
| */ | ||
| algorithms?: string[]; | ||
| }; | ||
@@ -228,2 +295,6 @@ /** | ||
| /** | ||
| * HTTP request headers, used for domain resolution. | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Additional claims that are required to be present in the access token. | ||
@@ -241,3 +312,4 @@ */ | ||
| /** | ||
| * Full HTTP URL of the authorized request (for `htu` validation). | ||
| * Full HTTP URL of the authorized request. | ||
| * Used for domain resolution when `domains` is configured and for DPoP `htu` validation. | ||
| */ | ||
@@ -249,2 +321,8 @@ httpUrl: string; | ||
| scheme: string; | ||
| /** | ||
| * The allowed asymetric algorithms to use for verifying the access token's signature. | ||
| * | ||
| * Defaults to ['RS256'] if not provided. | ||
| */ | ||
| algorithms?: string[]; | ||
| }; | ||
@@ -619,2 +697,2 @@ type VerifyAccessTokenOptions = BearerVerifyAccessTokenOptions | DPoPVerifyAccessTokenOptions; | ||
| export { type AccessTokenForConnectionOptions, ApiClient, type ApiClientOptions, AuthError, type AuthErrorCause, BearerMethod, type BearerVerifyAccessTokenOptions, type BodyLike, type ConnectionTokenSet, type DPoPOptions, type DPoPVerifyAccessTokenOptions, type ExchangeProfileOptions, GrantType, type HeadersLike, type IProtectedResourceMetadata, InvalidConfigurationError, InvalidDpopProofError, InvalidRequestError, MissingRequiredArgumentError, MissingTransactionError, ProtectedResourceMetadata, ProtectedResourceMetadataBuilder, type QueryLike, SigningAlgorithm, type TokenExchangeProfileResult, VerifyAccessTokenError, type VerifyAccessTokenOptions, getToken }; | ||
| export { type AccessTokenForConnectionOptions, ApiClient, type ApiClientOptions, AuthError, type AuthErrorCause, BearerMethod, type BearerVerifyAccessTokenOptions, type BodyLike, type ConnectionTokenSet, type DPoPOptions, type DPoPVerifyAccessTokenOptions, type DiscoveryCacheOptions, type DomainsResolver, type DomainsResolverContext, type ExchangeProfileOptions, GrantType, type HeadersLike, type IProtectedResourceMetadata, InvalidConfigurationError, InvalidDpopProofError, InvalidRequestError, MissingRequiredArgumentError, MissingTransactionError, ProtectedResourceMetadata, ProtectedResourceMetadataBuilder, type QueryLike, SigningAlgorithm, type TokenExchangeProfileResult, VerifyAccessTokenError, type VerifyAccessTokenOptions, getToken }; |
+87
-9
| import * as jose from 'jose'; | ||
| export { MissingClientAuthError, TokenExchangeError } from '@auth0/auth0-auth-js'; | ||
| interface ApiClientOptions { | ||
| type DomainsResolverContext = { | ||
| /** | ||
| * The Auth0 domain to use for authentication. | ||
| * @example 'example.auth0.com' (without https://) | ||
| * Full request URL, if available. | ||
| * This is populated from `verifyAccessToken({ httpUrl })` when provided. | ||
| */ | ||
| domain: string; | ||
| url?: string; | ||
| /** | ||
| * HTTP request headers (lowercased keys recommended). | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Unverified issuer extracted from the token. | ||
| */ | ||
| unverifiedIss?: string; | ||
| }; | ||
| /** | ||
| * Resolver that returns a list of allowed domains for the current request. | ||
| */ | ||
| type DomainsResolver = (context: DomainsResolverContext) => Promise<string[]> | string[]; | ||
| /** | ||
| * Optional caching configuration for discovery metadata and JWKS fetchers. | ||
| * TTL is expressed in seconds. maxEntries controls the LRU size. | ||
| */ | ||
| interface DiscoveryCacheOptions { | ||
| ttl?: number; | ||
| maxEntries?: number; | ||
| } | ||
| type ApiClientCommonOptions = { | ||
| /** | ||
| * The expected JWT Access Token audience ("aud") value. | ||
@@ -39,2 +61,7 @@ */ | ||
| /** | ||
| * Optional list of allowed JWT algorithms for access token verification. | ||
| * Defaults to ['RS256'] when not provided. HS* values are rejected. | ||
| */ | ||
| algorithms?: string[]; | ||
| /** | ||
| * Demonstration of Proof-of-Possession (DPoP) configuration. | ||
@@ -45,3 +72,33 @@ * | ||
| dpop?: DPoPOptions; | ||
| } | ||
| /** | ||
| * Optional discovery cache configuration for OIDC metadata. | ||
| * TTL is in seconds. maxEntries controls the LRU size. | ||
| * Defaults when omitted: ttl = 600 seconds, maxEntries = 100. | ||
| */ | ||
| discoveryCache?: DiscoveryCacheOptions; | ||
| }; | ||
| type ApiClientOptions = (ApiClientCommonOptions & { | ||
| /** | ||
| * The Auth0 domain to use for authentication and non-verification flows. | ||
| * @example 'example.auth0.com' | ||
| */ | ||
| domain: string; | ||
| /** | ||
| * Optional domain allowlist or resolver for access token verification. | ||
| * When provided, access token verification uses this instead of `domain`. | ||
| * Provide domains as shown in the Auth0 Dashboard (e.g., "example.auth0.com"). | ||
| */ | ||
| domains?: string[] | DomainsResolver; | ||
| }) | (ApiClientCommonOptions & { | ||
| /** | ||
| * Domain allowlist or resolver for access token verification. | ||
| * Provide domains as shown in the Auth0 Dashboard (e.g., "example.auth0.com"). | ||
| */ | ||
| domains: string[] | DomainsResolver; | ||
| domain?: never; | ||
| clientId?: never; | ||
| clientSecret?: never; | ||
| clientAssertionSigningKey?: never; | ||
| clientAssertionSigningAlg?: never; | ||
| }); | ||
| interface AccessTokenForConnectionOptions { | ||
@@ -195,2 +252,6 @@ /** | ||
| /** | ||
| * HTTP request headers, used for domain resolution. | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Additional claims that are required to be present in the access token. | ||
@@ -208,5 +269,5 @@ */ | ||
| /** | ||
| * HTTP URL is not used for bearer validation. | ||
| * Full request URL, used for MCD domain resolution when available. | ||
| */ | ||
| httpUrl?: undefined; | ||
| httpUrl?: string; | ||
| /** | ||
@@ -216,2 +277,8 @@ * Optional scheme (e.g., 'bearer'); DPoP params must be absent. | ||
| scheme?: string; | ||
| /** | ||
| * The allowed asymmetric algorithms to use for verifying the access token's signature. | ||
| * | ||
| * Defaults to ['RS256'] if not provided. | ||
| */ | ||
| algorithms?: string[]; | ||
| }; | ||
@@ -228,2 +295,6 @@ /** | ||
| /** | ||
| * HTTP request headers, used for domain resolution. | ||
| */ | ||
| headers?: Record<string, string | string[] | undefined>; | ||
| /** | ||
| * Additional claims that are required to be present in the access token. | ||
@@ -241,3 +312,4 @@ */ | ||
| /** | ||
| * Full HTTP URL of the authorized request (for `htu` validation). | ||
| * Full HTTP URL of the authorized request. | ||
| * Used for domain resolution when `domains` is configured and for DPoP `htu` validation. | ||
| */ | ||
@@ -249,2 +321,8 @@ httpUrl: string; | ||
| scheme: string; | ||
| /** | ||
| * The allowed asymetric algorithms to use for verifying the access token's signature. | ||
| * | ||
| * Defaults to ['RS256'] if not provided. | ||
| */ | ||
| algorithms?: string[]; | ||
| }; | ||
@@ -619,2 +697,2 @@ type VerifyAccessTokenOptions = BearerVerifyAccessTokenOptions | DPoPVerifyAccessTokenOptions; | ||
| export { type AccessTokenForConnectionOptions, ApiClient, type ApiClientOptions, AuthError, type AuthErrorCause, BearerMethod, type BearerVerifyAccessTokenOptions, type BodyLike, type ConnectionTokenSet, type DPoPOptions, type DPoPVerifyAccessTokenOptions, type ExchangeProfileOptions, GrantType, type HeadersLike, type IProtectedResourceMetadata, InvalidConfigurationError, InvalidDpopProofError, InvalidRequestError, MissingRequiredArgumentError, MissingTransactionError, ProtectedResourceMetadata, ProtectedResourceMetadataBuilder, type QueryLike, SigningAlgorithm, type TokenExchangeProfileResult, VerifyAccessTokenError, type VerifyAccessTokenOptions, getToken }; | ||
| export { type AccessTokenForConnectionOptions, ApiClient, type ApiClientOptions, AuthError, type AuthErrorCause, BearerMethod, type BearerVerifyAccessTokenOptions, type BodyLike, type ConnectionTokenSet, type DPoPOptions, type DPoPVerifyAccessTokenOptions, type DiscoveryCacheOptions, type DomainsResolver, type DomainsResolverContext, type ExchangeProfileOptions, GrantType, type HeadersLike, type IProtectedResourceMetadata, InvalidConfigurationError, InvalidDpopProofError, InvalidRequestError, MissingRequiredArgumentError, MissingTransactionError, ProtectedResourceMetadata, ProtectedResourceMetadataBuilder, type QueryLike, SigningAlgorithm, type TokenExchangeProfileResult, VerifyAccessTokenError, type VerifyAccessTokenOptions, getToken }; |
+349
-26
| // src/api-client.ts | ||
| import * as oauth from "oauth4webapi"; | ||
| import { createRemoteJWKSet, jwtVerify as jwtVerify2, customFetch as customFetch2 } from "jose"; | ||
| import { createRemoteJWKSet, jwtVerify as jwtVerify2, customFetch as customFetch2, decodeJwt, decodeProtectedHeader } from "jose"; | ||
| import { AuthClient, TokenForConnectionError, MissingClientAuthError } from "@auth0/auth0-auth-js"; | ||
@@ -229,7 +229,74 @@ | ||
| // src/lru-cache.ts | ||
| var LruCache = class { | ||
| #map; | ||
| #inflight; | ||
| #ttlMs; | ||
| #maxEntries; | ||
| constructor(ttlMs, maxEntries) { | ||
| this.#ttlMs = ttlMs; | ||
| this.#maxEntries = maxEntries; | ||
| this.#map = /* @__PURE__ */ new Map(); | ||
| this.#inflight = /* @__PURE__ */ new Map(); | ||
| } | ||
| get(key) { | ||
| const entry = this.#map.get(key); | ||
| if (!entry) { | ||
| return void 0; | ||
| } | ||
| if (this.#ttlMs !== void 0 && entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) { | ||
| this.#map.delete(key); | ||
| return void 0; | ||
| } | ||
| this.#map.delete(key); | ||
| this.#map.set(key, entry); | ||
| return entry.value; | ||
| } | ||
| set(key, value) { | ||
| const expiresAt = this.#ttlMs !== void 0 ? Date.now() + this.#ttlMs : void 0; | ||
| this.#map.set(key, { value, expiresAt }); | ||
| this.#enforceLimit(); | ||
| } | ||
| async getOrSet(key, loader) { | ||
| const cached = this.get(key); | ||
| if (cached !== void 0) { | ||
| return cached; | ||
| } | ||
| const inflight = this.#inflight.get(key); | ||
| if (inflight) { | ||
| return inflight; | ||
| } | ||
| const promise = (async () => { | ||
| const value = await loader(); | ||
| this.set(key, value); | ||
| return value; | ||
| })(); | ||
| const inflightPromise = promise.finally(() => { | ||
| this.#inflight.delete(key); | ||
| }); | ||
| this.#inflight.set(key, inflightPromise); | ||
| return inflightPromise; | ||
| } | ||
| #enforceLimit() { | ||
| if (this.#maxEntries === void 0) { | ||
| return; | ||
| } | ||
| while (this.#map.size > this.#maxEntries) { | ||
| const oldestKey = this.#map.keys().next().value; | ||
| if (oldestKey === void 0) { | ||
| break; | ||
| } | ||
| this.#map.delete(oldestKey); | ||
| } | ||
| } | ||
| }; | ||
| // src/api-client.ts | ||
| var ApiClient = class { | ||
| #serverMetadata; | ||
| #serverMetadataByDomain; | ||
| #options; | ||
| #jwks; | ||
| #jwksByUri; | ||
| #domains; | ||
| #algorithms; | ||
| #defaultDomainUrl; | ||
| #authClient; | ||
@@ -264,4 +331,59 @@ constructor(options) { | ||
| } | ||
| const discoveryCacheConfig = options.discoveryCache ?? {}; | ||
| const ttlSeconds = discoveryCacheConfig.ttl ?? 600; | ||
| if (!Number.isFinite(ttlSeconds)) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "ttl" must be a number'); | ||
| } | ||
| if (ttlSeconds < 0) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "ttl" must be a non-negative number'); | ||
| } | ||
| const cacheTtlMs = ttlSeconds * 1e3; | ||
| const maxEntries = discoveryCacheConfig.maxEntries ?? 100; | ||
| if (!Number.isFinite(maxEntries)) { | ||
| throw new InvalidConfigurationError('Invalid discoveryCache configuration: "maxEntries" must be a number'); | ||
| } | ||
| if (maxEntries < 0) { | ||
| throw new InvalidConfigurationError( | ||
| 'Invalid discoveryCache configuration: "maxEntries" must be a non-negative number' | ||
| ); | ||
| } | ||
| this.#serverMetadataByDomain = new LruCache(cacheTtlMs, maxEntries); | ||
| this.#jwksByUri = new LruCache(cacheTtlMs, maxEntries); | ||
| this.#options = options; | ||
| if (options.domain !== void 0) { | ||
| try { | ||
| this.#defaultDomainUrl = normalizeDomain(options.domain); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw new InvalidConfigurationError(`Invalid domain configuration: ${message}`); | ||
| } | ||
| } | ||
| if (options.domains !== void 0) { | ||
| if (Array.isArray(options.domains)) { | ||
| if (options.domains.length === 0) { | ||
| throw new InvalidConfigurationError('Invalid domains configuration: "domains" must not be empty'); | ||
| } | ||
| const normalized = options.domains.map((domain) => { | ||
| try { | ||
| return normalizeDomain(domain); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw new InvalidConfigurationError(`Invalid domains configuration: ${message}`); | ||
| } | ||
| }); | ||
| this.#domains = Array.from(new Set(normalized)); | ||
| } else if (typeof options.domains === "function") { | ||
| this.#domains = options.domains; | ||
| } else { | ||
| throw new InvalidConfigurationError('Invalid domains configuration: "domains" must be an array or a function'); | ||
| } | ||
| } | ||
| this.#algorithms = normalizeAlgorithms(options.algorithms); | ||
| if (!this.#defaultDomainUrl && this.#domains === void 0) { | ||
| throw new MissingRequiredArgumentError("domain or domains"); | ||
| } | ||
| if (options.clientId) { | ||
| if (!options.domain) { | ||
| throw new MissingRequiredArgumentError("domain"); | ||
| } | ||
| this.#authClient = new AuthClient({ | ||
@@ -283,16 +405,11 @@ domain: options.domain, | ||
| */ | ||
| async #discover() { | ||
| if (this.#serverMetadata) { | ||
| return { | ||
| serverMetadata: this.#serverMetadata | ||
| }; | ||
| } | ||
| const issuer = new URL(`https://${this.#options.domain}`); | ||
| const response = await oauth.discoveryRequest(issuer, { | ||
| [oauth.customFetch]: this.#options.customFetch | ||
| async #discoverDomain(domain) { | ||
| const serverMetadata = await this.#serverMetadataByDomain.getOrSet(domain, async () => { | ||
| const issuer = new URL(domain); | ||
| const response = await oauth.discoveryRequest(issuer, { | ||
| [oauth.customFetch]: this.#options.customFetch | ||
| }); | ||
| return oauth.processDiscoveryResponse(issuer, response); | ||
| }); | ||
| this.#serverMetadata = await oauth.processDiscoveryResponse(issuer, response); | ||
| return { | ||
| serverMetadata: this.#serverMetadata | ||
| }; | ||
| return { serverMetadata }; | ||
| } | ||
@@ -351,3 +468,3 @@ /** | ||
| const httpUrl = options.httpUrl; | ||
| const hasDpopParams = dpopProof !== void 0 || httpMethod !== void 0 || httpUrl !== void 0; | ||
| const hasDpopParams = dpopProof !== void 0 || httpMethod !== void 0 || this.#domains === void 0 && httpUrl !== void 0; | ||
| if (mode !== "disabled" && scheme && !["bearer", "dpop"].includes(scheme)) { | ||
@@ -382,13 +499,69 @@ const err = new InvalidRequestError(""); | ||
| } | ||
| const { serverMetadata } = await this.#discover(); | ||
| this.#jwks ||= createRemoteJWKSet(new URL(serverMetadata.jwks_uri), { | ||
| [customFetch2]: this.#options.customFetch | ||
| }); | ||
| const accessToken = options.accessToken; | ||
| const domains = this.#domains; | ||
| let jwks; | ||
| let issuerForVerify = ""; | ||
| let unverifiedIss; | ||
| let alg; | ||
| const defaultDomainUrl = this.#defaultDomainUrl; | ||
| try { | ||
| const { payload } = await jwtVerify2(options.accessToken, this.#jwks, { | ||
| issuer: this.#serverMetadata.issuer, | ||
| try { | ||
| const header = decodeProtectedHeader(accessToken); | ||
| const payload2 = decodeJwt(accessToken); | ||
| if (typeof header.alg === "string") { | ||
| alg = header.alg; | ||
| } | ||
| if (typeof payload2.iss === "string") { | ||
| unverifiedIss = payload2.iss; | ||
| } | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw this.#addChallenges(new VerifyAccessTokenError(message), mode, scheme); | ||
| } | ||
| if (alg && alg.toUpperCase().startsWith("HS")) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError("unsupported algorithm (symmetric algorithms are not supported)"), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (domains !== void 0) { | ||
| if (!unverifiedIss) { | ||
| throw this.#addChallenges(new VerifyAccessTokenError('missing required "iss" claim'), mode, scheme); | ||
| } | ||
| const context = { | ||
| url: httpUrl, | ||
| headers: options.headers, | ||
| unverifiedIss | ||
| }; | ||
| const allowedDomains = await this.#resolveDomains(domains, context, mode, scheme); | ||
| const matchedDomain = allowedDomains.find((domain) => domain === unverifiedIss); | ||
| if (!matchedDomain) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| 'unexpected "iss" claim value (issuer is not in the configured domain list)' | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| const { serverMetadata } = await this.#discoverDomain(matchedDomain); | ||
| const { issuer, jwksUri } = this.#requireDiscoveryMetadata(serverMetadata, mode, scheme); | ||
| issuerForVerify = issuer; | ||
| jwks = this.#getJwksForDomain(jwksUri); | ||
| } else if (defaultDomainUrl !== void 0) { | ||
| const { serverMetadata } = await this.#discoverDomain(defaultDomainUrl); | ||
| const { issuer, jwksUri } = this.#requireDiscoveryMetadata(serverMetadata, mode, scheme); | ||
| issuerForVerify = issuer; | ||
| jwks = this.#getJwksForDomain(jwksUri); | ||
| } else { | ||
| throw new MissingRequiredArgumentError("domain or domains"); | ||
| } | ||
| const jwtVerifyOptions = { | ||
| audience: this.#options.audience, | ||
| algorithms: ["RS256"], | ||
| requiredClaims: ["iat", "exp", ...options.requiredClaims || []] | ||
| }); | ||
| algorithms: options.algorithms ? normalizeAlgorithms(options.algorithms) : this.#algorithms, | ||
| requiredClaims: ["iat", "exp", ...options.requiredClaims || []], | ||
| issuer: issuerForVerify | ||
| }; | ||
| const { payload } = await jwtVerify2(accessToken, jwks, jwtVerifyOptions); | ||
| let cnfJkt; | ||
@@ -472,2 +645,93 @@ const cnf = payload.cnf; | ||
| } | ||
| async #resolveDomains(domains, context, mode, scheme) { | ||
| if (Array.isArray(domains)) { | ||
| return domains; | ||
| } | ||
| let resolved; | ||
| try { | ||
| resolved = await domains(context); | ||
| } catch { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError("domain validation failed: domains resolver failed"), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (!Array.isArray(resolved)) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver must return an array of domain strings" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| if (resolved.length === 0) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver returned no allowed domains" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| const normalized = []; | ||
| for (const domain of resolved) { | ||
| if (typeof domain !== "string" || !domain.trim()) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError( | ||
| "domain validation failed: domains resolver returned a non-string domain" | ||
| ), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| try { | ||
| normalized.push(normalizeDomain(domain)); | ||
| } catch (error) { | ||
| const message = error.message; | ||
| throw this.#addChallenges(new VerifyAccessTokenError(message), mode, scheme); | ||
| } | ||
| } | ||
| return Array.from(new Set(normalized)); | ||
| } | ||
| #requireDiscoveryMetadata(serverMetadata, mode, scheme) { | ||
| if (!serverMetadata.jwks_uri) { | ||
| throw this.#addChallenges( | ||
| new VerifyAccessTokenError('missing "jwks_uri" in discovery metadata'), | ||
| mode, | ||
| scheme | ||
| ); | ||
| } | ||
| return { issuer: serverMetadata.issuer, jwksUri: serverMetadata.jwks_uri }; | ||
| } | ||
| #getJwksForDomain(jwksUri) { | ||
| const existing = this.#jwksByUri.get(jwksUri); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
| const jwksUrl = new URL(jwksUri); | ||
| const jwks = createRemoteJWKSet(jwksUrl, { | ||
| [customFetch2]: this.#createJwksFetch() | ||
| }); | ||
| this.#jwksByUri.set(jwksUri, jwks); | ||
| return jwks; | ||
| } | ||
| #createJwksFetch() { | ||
| const baseFetch = this.#options.customFetch ?? fetch; | ||
| return async (input, init) => { | ||
| try { | ||
| const response = await baseFetch(input, init); | ||
| if (!response.ok) { | ||
| throw new Error("JWKS request failed"); | ||
| } | ||
| return response; | ||
| } catch (error) { | ||
| if (error instanceof Error && error.message.startsWith("JWKS request failed")) { | ||
| throw error; | ||
| } | ||
| throw new Error("JWKS request failed"); | ||
| } | ||
| }; | ||
| } | ||
| #addChallenges(err, mode, scheme, params) { | ||
@@ -575,2 +839,61 @@ const authErr = err; | ||
| }; | ||
| function normalizeDomain(value) { | ||
| if (typeof value !== "string" || !value.trim()) { | ||
| throw new Error("domain must be a non-empty string"); | ||
| } | ||
| const trimmed = value.trim(); | ||
| let withScheme; | ||
| if (/^https?:\/\//i.test(trimmed)) { | ||
| if (!/^https:\/\//i.test(trimmed)) { | ||
| throw new Error("invalid domain URL (https required)"); | ||
| } | ||
| withScheme = trimmed; | ||
| } else { | ||
| withScheme = `https://${trimmed}`; | ||
| } | ||
| let domainUrl; | ||
| try { | ||
| domainUrl = new URL(withScheme); | ||
| } catch { | ||
| throw new Error("invalid domain URL"); | ||
| } | ||
| if (domainUrl.username || domainUrl.password) { | ||
| throw new Error("invalid domain URL (credentials are not allowed)"); | ||
| } | ||
| if (domainUrl.search || domainUrl.hash) { | ||
| throw new Error("invalid domain URL (query/fragment are not allowed)"); | ||
| } | ||
| domainUrl.hash = ""; | ||
| domainUrl.search = ""; | ||
| domainUrl.hostname = domainUrl.hostname.toLowerCase(); | ||
| if (domainUrl.pathname && domainUrl.pathname !== "/" && domainUrl.pathname !== "") { | ||
| throw new Error("invalid domain URL (path segments are not allowed)"); | ||
| } | ||
| domainUrl.pathname = "/"; | ||
| return domainUrl.toString(); | ||
| } | ||
| function normalizeAlgorithms(algorithms) { | ||
| if (algorithms === void 0) { | ||
| return ["RS256"]; | ||
| } | ||
| if (!Array.isArray(algorithms) || algorithms.length === 0) { | ||
| throw new InvalidConfigurationError('Invalid algorithms configuration: "algorithms" must be a non-empty array'); | ||
| } | ||
| const normalized = []; | ||
| for (const algorithm of algorithms) { | ||
| if (typeof algorithm !== "string" || !algorithm.trim()) { | ||
| throw new InvalidConfigurationError( | ||
| 'Invalid algorithms configuration: each "algorithms" entry must be a non-empty string' | ||
| ); | ||
| } | ||
| const trimmed = algorithm.trim(); | ||
| if (trimmed.toUpperCase().startsWith("HS")) { | ||
| throw new InvalidConfigurationError( | ||
| "Invalid algorithms configuration: symmetric algorithms are not allowed" | ||
| ); | ||
| } | ||
| normalized.push(trimmed); | ||
| } | ||
| return Array.from(new Set(normalized)); | ||
| } | ||
@@ -577,0 +900,0 @@ // src/protected-resource-metadata.ts |
+2
-2
| { | ||
| "name": "@auth0/auth0-api-js", | ||
| "version": "1.4.0", | ||
| "version": "1.5.0", | ||
| "description": "Auth0 Authentication SDK for API's on JavaScript runtimes", | ||
@@ -38,3 +38,3 @@ "author": "Auth0", | ||
| "tsup": "^8.4.0", | ||
| "typescript": "^5.7.3", | ||
| "typescript": "~5.8.3", | ||
| "typescript-eslint": "^8.24.0", | ||
@@ -41,0 +41,0 @@ "vitest": "^3.0.5" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
309114
27.11%3073
30.71%9
28.57%4
100%