| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerNamespace(purl) |
| 23 | lowerName(purl) |
| 24 | return purl |
| 25 | } |
| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerNamespace(purl) |
| 23 | lowerName(purl) |
| 24 | return purl |
| 25 | } |
| 8 | import { PurlError } from '../error.js' |
| 9 | import { lowerName } from '../strings.js' |
| 10 | import { validateNoInjectionByType } from '../validate.js' |
| 11 | |
| 12 | interface PurlObject { |
| 13 | name: string |
| 14 | namespace?: string | undefined |
| 15 | qualifiers?: Record<string, string> | undefined |
| 16 | subpath?: string | undefined |
| 17 | type?: string | undefined |
| 18 | version?: string | undefined |
| 19 | } |
| 26 | export function normalize(purl: PurlObject): PurlObject { |
| 27 | lowerName(purl) |
| 28 | return purl |
| 29 | } |
| 36 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 37 | if (!purl.version || purl.version.length === 0) { |
| 38 | if (throws) { |
| 39 | throw new PurlError('bazel requires a "version" component') |
| 40 | } |
| 41 | return false |
| 42 | } |
| 43 | if (!validateNoInjectionByType('bazel', 'name', purl.name, throws)) { |
| 44 | return false |
| 45 | } |
| 46 | return true |
| 47 | } |
| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | import { validateNoInjectionByType } from '../validate.js' |
| 7 | |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | lowerNamespace(purl) |
| 24 | lowerName(purl) |
| 25 | return purl |
| 26 | } |
| 32 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 33 | if ( |
| 34 | !validateNoInjectionByType('bitbucket', 'namespace', purl.namespace, throws) |
| 35 | ) { |
| 36 | return false |
| 37 | } |
| 38 | if (!validateNoInjectionByType('bitbucket', 'name', purl.name, throws)) { |
| 39 | return false |
| 40 | } |
| 41 | return true |
| 42 | } |
| 5 | import { lowerName } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerName(purl) |
| 23 | return purl |
| 24 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeSome, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 14 | |
| 15 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 16 | |
| 17 | interface PurlObject { |
| 18 | name: string |
| 19 | namespace?: string | undefined |
| 20 | qualifiers?: Record<string, string> | undefined |
| 21 | subpath?: string | undefined |
| 22 | type?: string | undefined |
| 23 | version?: string | undefined |
| 24 | } |
| 64 | export async function cargoExists( |
| 65 | name: string, |
| 66 | version?: string, |
| 67 | options?: ExistsOptions, |
| 68 | ): Promise<ExistsResult> { |
| 69 | const cacheKey = version ? `cargo:${name}@${version}` : `cargo:${name}` |
| 70 | |
| 71 | // Try cache first if provided |
| 72 | if (options?.cache) { |
| 73 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 74 | if (cached !== undefined) { |
| 75 | return cached |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const fetchResult = async (): Promise<ExistsResult> => { |
| 80 | try { |
| 81 | const url = `https://crates.io/api/v1/crates/${encodeComponent(name)}` |
| 82 | |
| 83 | const data = await httpJson<{ |
| 84 | crate?: { max_version?: string } |
| 85 | versions?: Array<{ num?: string }> |
| 86 | }>(url, { |
| 87 | headers: { |
| 88 | 'User-Agent': '@socketregistry/packageurl-js', |
| 89 | }, |
| 90 | }) |
| 91 | |
| 92 | const latestVersion = |
| 93 | data.crate?.['max_version'] || data.versions?.[0]?.['num'] |
| 94 | |
| 95 | // If specific version requested, validate it exists |
| 96 | if (version && data.versions) { |
| 97 | const versionExists = ArrayPrototypeSome( |
| 98 | data.versions, |
| 99 | v => v.num === version, |
| 100 | ) |
| 101 | if (!versionExists) { |
| 102 | const result: ExistsResult = { |
| 103 | exists: false, |
| 104 | error: `Version ${version} not found`, |
| 105 | } |
| 106 | if (latestVersion !== undefined) { |
| 107 | result.latestVersion = latestVersion |
| 108 | } |
| 109 | return result |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | const result: ExistsResult = { |
| 114 | exists: true, |
| 115 | } |
| 116 | if (latestVersion !== undefined) { |
| 117 | result.latestVersion = latestVersion |
| 118 | } |
| 119 | return result |
| 120 | } catch (e) { |
| 122 | const error = errorMessage(e) |
| 124 | return { |
| 125 | exists: false, |
| 126 | error: StringPrototypeIncludes(error, '404') |
| 127 | ? 'Crate not found' |
| 128 | : error, |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | const result = await fetchResult() |
| 134 | |
| 135 | // Only cache successful results to avoid negative cache poisoning |
| 136 | // from transient failures (network errors, 5xx responses) |
| 137 | if (options?.cache && result.exists) { |
| 138 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 139 | } |
| 140 | |
| 141 | return result |
| 142 | } |
| 148 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 149 | if ( |
| 150 | !validateEmptyByType('cargo', 'namespace', purl.namespace, { |
| 151 | throws, |
| 152 | }) |
| 153 | ) { |
| 154 | return false |
| 155 | } |
| 156 | if (!validateNoInjectionByType('cargo', 'name', purl.name, throws)) { |
| 157 | return false |
| 158 | } |
| 159 | return true |
| 160 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { errorMessage, PurlError } from '../error.js' |
| 8 | import { |
| 9 | ArrayPrototypeSome, |
| 10 | StringPrototypeCharCodeAt, |
| 11 | StringPrototypeIncludes, |
| 12 | encodeComponent, |
| 13 | } from '@socketsecurity/lib/primordials' |
| 14 | import { validateNoInjectionByType } from '../validate.js' |
| 15 | |
| 16 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 54 | export async function cocoapodsExists( |
| 55 | name: string, |
| 56 | version?: string, |
| 57 | options?: ExistsOptions, |
| 58 | ): Promise<ExistsResult> { |
| 59 | const cacheKey = version |
| 60 | ? `cocoapods:${name}@${version}` |
| 61 | : `cocoapods:${name}` |
| 62 | |
| 63 | // Try cache first if provided |
| 64 | if (options?.cache) { |
| 65 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 66 | if (cached !== undefined) { |
| 67 | return cached |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | const fetchResult = async (): Promise<ExistsResult> => { |
| 72 | try { |
| 73 | const url = `https://trunk.cocoapods.org/api/v1/pods/${encodeComponent(name)}` |
| 74 | |
| 75 | const data = await httpJson<{ |
| 76 | versions?: Array<{ name?: string }> |
| 77 | }>(url) |
| 78 | |
| 79 | const versions = data.versions |
| 80 | if (!versions || versions.length === 0) { |
| 81 | return { exists: false, error: 'No versions found' } |
| 82 | } |
| 83 | |
| 84 | // Latest version is first in the array |
| 85 | const latestVersion = versions[0]?.['name'] |
| 86 | |
| 87 | if (version) { |
| 88 | const versionExists = ArrayPrototypeSome( |
| 89 | versions, |
| 90 | v => v.name === version, |
| 91 | ) |
| 92 | if (!versionExists) { |
| 93 | const result: ExistsResult = { |
| 94 | exists: false, |
| 95 | error: `Version ${version} not found`, |
| 96 | } |
| 97 | if (latestVersion !== undefined) { |
| 98 | result.latestVersion = latestVersion |
| 99 | } |
| 100 | return result |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | const result: ExistsResult = { exists: true } |
| 105 | if (latestVersion !== undefined) { |
| 106 | result.latestVersion = latestVersion |
| 107 | } |
| 108 | return result |
| 109 | } catch (e) { |
| 111 | const error = errorMessage(e) |
| 112 | return { |
| 113 | exists: false, |
| 114 | error: StringPrototypeIncludes(error, '404') ? 'Pod not found' : error, |
| 115 | } |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | const result = await fetchResult() |
| 121 | |
| 122 | // Only cache successful results to avoid negative cache poisoning |
| 123 | // from transient failures (network errors, 5xx responses) |
| 124 | if (options?.cache && result.exists) { |
| 125 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 126 | } |
| 127 | |
| 128 | return result |
| 129 | } |
| 136 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 137 | const { name } = purl |
| 138 | // `name` must not contain injection characters |
| 139 | if (!validateNoInjectionByType('cocoapods', 'name', name, throws)) { |
| 140 | return false |
| 141 | } |
| 142 | // `name` cannot contain a plus (`+`) character |
| 143 | if (StringPrototypeIncludes(name, '+')) { |
| 144 | if (throws) { |
| 145 | throw new PurlError( |
| 146 | 'cocoapods "name" component cannot contain a plus (+) character', |
| 147 | ) |
| 148 | } |
| 149 | return false |
| 150 | } |
| 151 | // `name` cannot begin with a period (`.`) |
| 153 | if (throws) { |
| 154 | throw new PurlError( |
| 155 | 'cocoapods "name" component cannot begin with a period', |
| 156 | ) |
| 157 | } |
| 158 | return false |
| 159 | } |
| 160 | return true |
| 161 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeSome, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { lowerName, lowerNamespace } from '../strings.js' |
| 14 | |
| 15 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 16 | |
| 17 | interface PurlObject { |
| 18 | name: string |
| 19 | namespace?: string | undefined |
| 20 | qualifiers?: Record<string, string> | undefined |
| 21 | subpath?: string | undefined |
| 22 | type?: string | undefined |
| 23 | version?: string | undefined |
| 24 | } |
| 31 | export function normalize(purl: PurlObject): PurlObject { |
| 32 | lowerNamespace(purl) |
| 33 | lowerName(purl) |
| 34 | return purl |
| 35 | } |
| 64 | export async function packagistExists( |
| 65 | name: string, |
| 66 | namespace?: string, |
| 67 | version?: string, |
| 68 | options?: ExistsOptions, |
| 69 | ): Promise<ExistsResult> { |
| 70 | if (!namespace) { |
| 71 | return { exists: false, error: 'Composer requires namespace (vendor)' } |
| 72 | } |
| 73 | |
| 74 | const packageName = `${namespace}/${name}` |
| 75 | const cacheKey = version |
| 76 | ? `composer:${packageName}@${version}` |
| 77 | : `composer:${packageName}` |
| 78 | |
| 79 | if (options?.cache) { |
| 80 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 81 | if (cached !== undefined) { |
| 82 | return cached |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | const fetchResult = async (): Promise<ExistsResult> => { |
| 87 | try { |
| 88 | const url = `https://repo.packagist.org/p2/${encodeComponent(packageName)}.json` |
| 89 | |
| 90 | const data = await httpJson<{ |
| 91 | packages?: { |
| 92 | [key: string]: Array<{ |
| 93 | version?: string |
| 94 | version_normalized?: string |
| 95 | }> |
| 96 | } |
| 97 | }>(url) |
| 98 | |
| 99 | const packageVersions = data.packages?.[packageName] |
| 100 | if (!packageVersions || packageVersions.length === 0) { |
| 101 | return { exists: false, error: 'Package not found' } |
| 102 | } |
| 103 | |
| 104 | // Find the latest stable version (highest `version_normalized` without `dev` suffix) |
| 105 | let latestVersion: string | undefined |
| 106 | for (const pkg of packageVersions) { |
| 107 | const ver = pkg.version |
| 108 | if (ver && !StringPrototypeIncludes(ver, 'dev-')) { |
| 109 | latestVersion = ver |
| 110 | break |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if (version) { |
| 115 | const versionExists = ArrayPrototypeSome( |
| 116 | packageVersions, |
| 117 | pkg => pkg.version === version, |
| 118 | ) |
| 119 | if (!versionExists) { |
| 120 | const result: ExistsResult = { |
| 121 | exists: false, |
| 122 | error: `Version ${version} not found`, |
| 123 | } |
| 124 | if (latestVersion !== undefined) { |
| 125 | result.latestVersion = latestVersion |
| 126 | } |
| 127 | return result |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | const result: ExistsResult = { exists: true } |
| 132 | if (latestVersion !== undefined) { |
| 133 | result.latestVersion = latestVersion |
| 134 | } |
| 135 | return result |
| 136 | } catch (e) { |
| 138 | const error = errorMessage(e) |
| 139 | return { |
| 140 | exists: false, |
| 141 | error: StringPrototypeIncludes(error, '404') |
| 142 | ? 'Package not found' |
| 143 | : error, |
| 144 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | const result = await fetchResult() |
| 150 | // Only cache successful results to avoid negative cache poisoning |
| 151 | // from transient failures (network errors, 5xx responses) |
| 152 | if (options?.cache && result.exists) { |
| 153 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 154 | } |
| 155 | return result |
| 156 | } |
| 5 | import { PurlError } from '../error.js' |
| 6 | import { isNullishOrEmptyString } from '../lang.js' |
| 7 | import { validateNoInjectionByType } from '../validate.js' |
| 8 | |
| 9 | interface PurlObject { |
| 10 | name: string |
| 11 | namespace?: string | undefined |
| 12 | qualifiers?: Record<string, string> | undefined |
| 13 | subpath?: string | undefined |
| 14 | type?: string | undefined |
| 15 | version?: string | undefined |
| 16 | } |
| 24 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 25 | if (isNullishOrEmptyString(purl.namespace)) { |
| 26 | if (purl.qualifiers?.['channel']) { |
| 27 | if (throws) { |
| 28 | throw new PurlError( |
| 29 | 'conan requires a "namespace" component when a "channel" qualifier is present', |
| 30 | ) |
| 31 | } |
| 32 | return false |
| 33 | } |
| 34 | } else if (isNullishOrEmptyString(purl.qualifiers)) { |
| 35 | if (throws) { |
| 36 | throw new PurlError( |
| 37 | 'conan requires a "qualifiers" component when a namespace is present', |
| 38 | ) |
| 39 | } |
| 40 | return false |
| 41 | } |
| 42 | if ( |
| 43 | !validateNoInjectionByType('conan', 'namespace', purl.namespace, throws) |
| 44 | ) { |
| 45 | return false |
| 46 | } |
| 47 | if (!validateNoInjectionByType('conan', 'name', purl.name, throws)) { |
| 48 | return false |
| 49 | } |
| 50 | return true |
| 51 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeIncludes, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { lowerName } from '../strings.js' |
| 14 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 15 | |
| 16 | import type { ExistsOptions, ExistsResult } from './npm.js' |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 32 | export function normalize(purl: PurlObject): PurlObject { |
| 33 | lowerName(purl) |
| 34 | return purl |
| 35 | } |
| 41 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 42 | if ( |
| 43 | !validateEmptyByType('conda', 'namespace', purl.namespace, { |
| 44 | throws, |
| 45 | }) |
| 46 | ) { |
| 47 | return false |
| 48 | } |
| 49 | if (!validateNoInjectionByType('conda', 'name', purl.name, throws)) { |
| 50 | return false |
| 51 | } |
| 52 | return true |
| 53 | } |
| 98 | export async function condaExists( |
| 99 | name: string, |
| 100 | version?: string, |
| 101 | channel?: string, |
| 102 | options?: ExistsOptions, |
| 103 | ): Promise<ExistsResult> { |
| 104 | // Use provided channel or default to `conda-forge` (most popular community channel) |
| 105 | const channelName = channel || 'conda-forge' |
| 106 | const cacheKey = version |
| 107 | ? `conda:${channelName}/${name}@${version}` |
| 108 | : `conda:${channelName}/${name}` |
| 109 | |
| 110 | // Try cache first if provided |
| 111 | if (options?.cache) { |
| 112 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 113 | if (cached !== undefined) { |
| 114 | return cached |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | const fetchResult = async (): Promise<ExistsResult> => { |
| 119 | try { |
| 120 | const encodedChannel = encodeComponent(channelName) |
| 121 | const encodedName = encodeComponent(name) |
| 122 | const url = `https://api.anaconda.org/package/${encodedChannel}/${encodedName}` |
| 123 | |
| 124 | const data = await httpJson<{ |
| 125 | latest_version?: string |
| 126 | versions?: string[] |
| 127 | }>(url) |
| 128 | |
| 129 | const latestVersion = data.latest_version |
| 130 | |
| 131 | // If specific version requested, validate it exists |
| 132 | if (version) { |
| 133 | if (!data.versions || !ArrayPrototypeIncludes(data.versions, version)) { |
| 134 | const result: ExistsResult = { |
| 135 | exists: false, |
| 136 | error: `Version ${version} not found`, |
| 137 | } |
| 138 | if (latestVersion !== undefined) { |
| 139 | result.latestVersion = latestVersion |
| 140 | } |
| 141 | return result |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | const result: ExistsResult = { |
| 146 | exists: true, |
| 147 | } |
| 148 | if (latestVersion !== undefined) { |
| 149 | result.latestVersion = latestVersion |
| 150 | } |
| 151 | return result |
| 152 | } catch (e) { |
| 154 | const error = errorMessage(e) |
| 156 | return { |
| 157 | exists: false, |
| 158 | error: StringPrototypeIncludes(error, '404') |
| 159 | ? 'Package not found' |
| 160 | : error, |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | const result = await fetchResult() |
| 166 | |
| 167 | // Only cache successful results to avoid negative cache poisoning |
| 168 | // from transient failures (network errors, 5xx responses) |
| 169 | if (options?.cache && result.exists) { |
| 170 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 171 | } |
| 172 | |
| 173 | return result |
| 174 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { errorMessage, PurlError } from '../error.js' |
| 8 | import { |
| 9 | StringPrototypeIncludes, |
| 10 | StringPrototypeToUpperCase, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { validateNoInjectionByType } from '../validate.js' |
| 14 | |
| 15 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 16 | |
| 17 | interface PurlObject { |
| 18 | name: string |
| 19 | namespace?: string | undefined |
| 20 | qualifiers?: Record<string, string> | undefined |
| 21 | subpath?: string | undefined |
| 22 | type?: string | undefined |
| 23 | version?: string | undefined |
| 24 | } |
| 53 | export async function cpanExists( |
| 54 | name: string, |
| 55 | version?: string, |
| 56 | options?: ExistsOptions, |
| 57 | ): Promise<ExistsResult> { |
| 58 | const cacheKey = version ? `cpan:${name}@${version}` : `cpan:${name}` |
| 59 | |
| 60 | // Try cache first if provided |
| 61 | if (options?.cache) { |
| 62 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 63 | if (cached !== undefined) { |
| 64 | return cached |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | const fetchResult = async (): Promise<ExistsResult> => { |
| 69 | try { |
| 70 | const url = `https://fastapi.metacpan.org/v1/module/${encodeComponent(name)}` |
| 71 | |
| 72 | const data = await httpJson<{ |
| 73 | version?: string |
| 74 | }>(url) |
| 75 | |
| 76 | const latestVersion = data.version |
| 77 | |
| 78 | if (version) { |
| 79 | // Check specific version |
| 80 | const versionUrl = `https://fastapi.metacpan.org/v1/module/${encodeComponent(name)}/${encodeComponent(version)}` |
| 81 | try { |
| 82 | await httpJson(versionUrl) |
| 83 | } catch { |
| 84 | const result: ExistsResult = { |
| 85 | exists: false, |
| 86 | error: `Version ${version} not found`, |
| 87 | } |
| 88 | if (latestVersion !== undefined) { |
| 89 | result.latestVersion = latestVersion |
| 90 | } |
| 91 | return result |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | const result: ExistsResult = { exists: true } |
| 96 | if (latestVersion !== undefined) { |
| 97 | result.latestVersion = latestVersion |
| 98 | } |
| 99 | return result |
| 100 | } catch (e) { |
| 102 | const error = errorMessage(e) |
| 103 | return { |
| 104 | exists: false, |
| 105 | error: StringPrototypeIncludes(error, '404') |
| 106 | ? 'Module not found' |
| 107 | : error, |
| 108 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | const result = await fetchResult() |
| 114 | |
| 115 | // Only cache successful results to avoid negative cache poisoning |
| 116 | // from transient failures (network errors, 5xx responses) |
| 117 | if (options?.cache && result.exists) { |
| 118 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 119 | } |
| 120 | |
| 121 | return result |
| 122 | } |
| 128 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 129 | const { namespace } = purl |
| 130 | if (namespace && namespace !== StringPrototypeToUpperCase(namespace)) { |
| 131 | if (throws) { |
| 132 | throw new PurlError('cpan "namespace" component must be UPPERCASE') |
| 133 | } |
| 134 | return false |
| 135 | } |
| 136 | if (!validateNoInjectionByType('cpan', 'namespace', namespace, throws)) { |
| 137 | return false |
| 138 | } |
| 139 | if (!validateNoInjectionByType('cpan', 'name', purl.name, throws)) { |
| 140 | return false |
| 141 | } |
| 142 | return true |
| 143 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeIncludes, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { |
| 14 | validateNoInjectionByType, |
| 15 | validateRequiredByType, |
| 16 | } from '../validate.js' |
| 17 | |
| 18 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 19 | |
| 20 | interface PurlObject { |
| 21 | name: string |
| 22 | namespace?: string | undefined |
| 23 | qualifiers?: Record<string, string> | undefined |
| 24 | subpath?: string | undefined |
| 25 | type?: string | undefined |
| 26 | version?: string | undefined |
| 27 | } |
| 57 | export async function cranExists( |
| 58 | name: string, |
| 59 | version?: string, |
| 60 | options?: ExistsOptions, |
| 61 | ): Promise<ExistsResult> { |
| 62 | const cacheKey = version ? `cran:${name}@${version}` : `cran:${name}` |
| 63 | |
| 64 | // Try cache first if provided |
| 65 | if (options?.cache) { |
| 66 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 67 | if (cached !== undefined) { |
| 68 | return cached |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | const fetchResult = async (): Promise<ExistsResult> => { |
| 73 | try { |
| 74 | // CRAN provides a JSON API via r-universe |
| 75 | const url = `https://cran.r-universe.dev/api/packages/${encodeComponent(name)}` |
| 76 | |
| 77 | const data = await httpJson<{ |
| 78 | Version?: string |
| 79 | versions?: string[] |
| 80 | }>(url) |
| 81 | |
| 82 | const latestVersion = data.Version |
| 83 | |
| 84 | if (version) { |
| 85 | const versions = data.versions || [] |
| 86 | if (!ArrayPrototypeIncludes(versions, version)) { |
| 87 | const result: ExistsResult = { |
| 88 | exists: false, |
| 89 | error: `Version ${version} not found`, |
| 90 | } |
| 91 | if (latestVersion !== undefined) { |
| 92 | result.latestVersion = latestVersion |
| 93 | } |
| 94 | return result |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | const result: ExistsResult = { exists: true } |
| 99 | if (latestVersion !== undefined) { |
| 100 | result.latestVersion = latestVersion |
| 101 | } |
| 102 | return result |
| 103 | } catch (e) { |
| 105 | const error = errorMessage(e) |
| 106 | return { |
| 107 | exists: false, |
| 108 | error: StringPrototypeIncludes(error, '404') |
| 109 | ? 'Package not found' |
| 110 | : error, |
| 111 | } |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | const result = await fetchResult() |
| 117 | |
| 118 | // Only cache successful results to avoid negative cache poisoning |
| 119 | // from transient failures (network errors, 5xx responses) |
| 120 | if (options?.cache && result.exists) { |
| 121 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 122 | } |
| 123 | |
| 124 | return result |
| 125 | } |
| 131 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 132 | if ( |
| 133 | !validateRequiredByType('cran', 'version', purl.version, { |
| 134 | throws, |
| 135 | }) |
| 136 | ) { |
| 137 | return false |
| 138 | } |
| 139 | if (!validateNoInjectionByType('cran', 'name', purl.name, throws)) { |
| 140 | return false |
| 141 | } |
| 142 | return true |
| 143 | } |
| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerNamespace(purl) |
| 23 | lowerName(purl) |
| 24 | return purl |
| 25 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | StringPrototypeIncludes, |
| 10 | encodeComponent, |
| 11 | } from '@socketsecurity/lib/primordials' |
| 12 | import { lowerName } from '../strings.js' |
| 13 | import { validateNoInjectionByType } from '../validate.js' |
| 14 | |
| 15 | import type { ExistsOptions, ExistsResult } from './npm.js' |
| 16 | |
| 17 | interface PurlObject { |
| 18 | name: string |
| 19 | namespace?: string | undefined |
| 20 | qualifiers?: Record<string, string> | undefined |
| 21 | subpath?: string | undefined |
| 22 | type?: string | undefined |
| 23 | version?: string | undefined |
| 24 | } |
| 31 | export function normalize(purl: PurlObject): PurlObject { |
| 32 | lowerName(purl) |
| 33 | return purl |
| 34 | } |
| 40 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 41 | if ( |
| 42 | !validateNoInjectionByType('docker', 'namespace', purl.namespace, throws) |
| 43 | ) { |
| 44 | return false |
| 45 | } |
| 46 | if (!validateNoInjectionByType('docker', 'name', purl.name, throws)) { |
| 47 | return false |
| 48 | } |
| 49 | return true |
| 50 | } |
| 94 | export async function dockerExists( |
| 95 | name: string, |
| 96 | namespace?: string, |
| 97 | version?: string, |
| 98 | options?: ExistsOptions, |
| 99 | ): Promise<ExistsResult> { |
| 100 | // Default namespace to `'library'` for official images if not specified |
| 101 | const repo = namespace ? `${namespace}/${name}` : name |
| 102 | const cacheKey = version ? `docker:${repo}:${version}` : `docker:${repo}` |
| 103 | |
| 104 | // Try cache first if provided |
| 105 | if (options?.cache) { |
| 106 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 107 | if (cached !== undefined) { |
| 108 | return cached |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | const fetchResult = async (): Promise<ExistsResult> => { |
| 113 | try { |
| 114 | // Encode each path segment separately to preserve the `/` delimiter |
| 115 | const encodedRepo = namespace |
| 116 | ? `${encodeComponent(namespace)}/${encodeComponent(name)}` |
| 117 | : encodeComponent(name) |
| 118 | const url = `https://hub.docker.com/v2/repositories/${encodedRepo}` |
| 119 | |
| 120 | const data = await httpJson<{ |
| 121 | name?: string |
| 122 | }>(url) |
| 123 | |
| 124 | // Docker Hub doesn't provide a simple `"latest version"` - tags need separate API call |
| 125 | // For now, we just verify the repository exists |
| 126 | if (!data.name) { |
| 127 | return { |
| 128 | exists: false, |
| 129 | error: 'Image not found', |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // If specific tag requested, verify it exists |
| 134 | if (version) { |
| 135 | try { |
| 136 | const tagUrl = `https://hub.docker.com/v2/repositories/${encodedRepo}/tags/${encodeComponent(version)}` |
| 137 | await httpJson(tagUrl) |
| 138 | } catch (e) { |
| 140 | const error = errorMessage(e) |
| 142 | return { |
| 143 | exists: false, |
| 144 | error: StringPrototypeIncludes(error, '404') |
| 145 | ? `Tag ${version} not found` |
| 146 | : error, |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | return { |
| 152 | exists: true, |
| 153 | latestVersion: version || 'latest', |
| 154 | } |
| 155 | } catch (e) { |
| 157 | const error = errorMessage(e) |
| 159 | return { |
| 160 | exists: false, |
| 161 | error: StringPrototypeIncludes(error, '404') |
| 162 | ? 'Image not found' |
| 163 | : error, |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | const result = await fetchResult() |
| 169 | |
| 170 | // Only cache successful results to avoid negative cache poisoning |
| 171 | // from transient failures (network errors, 5xx responses) |
| 172 | if (options?.cache && result.exists) { |
| 173 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 174 | } |
| 175 | |
| 176 | return result |
| 177 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { errorMessage } from '../error.js' |
| 8 | import { |
| 9 | ArrayIsArray, |
| 10 | ArrayPrototypeSome, |
| 11 | StringPrototypeIncludes, |
| 12 | encodeComponent, |
| 13 | } from '@socketsecurity/lib/primordials' |
| 14 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 15 | |
| 16 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 63 | export async function gemExists( |
| 64 | name: string, |
| 65 | version?: string, |
| 66 | options?: ExistsOptions, |
| 67 | ): Promise<ExistsResult> { |
| 68 | const cacheKey = version ? `gem:${name}@${version}` : `gem:${name}` |
| 69 | |
| 70 | // Try cache first if provided |
| 71 | if (options?.cache) { |
| 72 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 73 | if (cached !== undefined) { |
| 74 | return cached |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | const fetchResult = async (): Promise<ExistsResult> => { |
| 79 | try { |
| 80 | const url = `https://rubygems.org/api/v1/versions/${encodeComponent(name)}.json` |
| 81 | |
| 82 | const data = await httpJson<Array<{ number?: string }>>(url) |
| 83 | |
| 84 | if (!ArrayIsArray(data) || data.length === 0) { |
| 85 | return { |
| 86 | exists: false, |
| 87 | error: 'No versions found', |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | const latestVersion = data[0]?.['number'] |
| 92 | |
| 93 | // If specific version requested, validate it exists |
| 94 | if (version) { |
| 95 | const versionExists = ArrayPrototypeSome( |
| 96 | data, |
| 97 | v => v.number === version, |
| 98 | ) |
| 99 | if (!versionExists) { |
| 100 | const result: ExistsResult = { |
| 101 | exists: false, |
| 102 | error: `Version ${version} not found`, |
| 103 | } |
| 104 | if (latestVersion !== undefined) { |
| 105 | result.latestVersion = latestVersion |
| 106 | } |
| 107 | return result |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | const result: ExistsResult = { |
| 112 | exists: true, |
| 113 | } |
| 114 | if (latestVersion !== undefined) { |
| 115 | result.latestVersion = latestVersion |
| 116 | } |
| 117 | return result |
| 118 | } catch (e) { |
| 120 | const error = errorMessage(e) |
| 121 | return { |
| 122 | exists: false, |
| 123 | error: StringPrototypeIncludes(error, '404') ? 'Gem not found' : error, |
| 124 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | const result = await fetchResult() |
| 130 | |
| 131 | // Only cache successful results to avoid negative cache poisoning |
| 132 | // from transient failures (network errors, 5xx responses) |
| 133 | if (options?.cache && result.exists) { |
| 134 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 135 | } |
| 136 | |
| 137 | return result |
| 138 | } |
| 144 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 145 | if ( |
| 146 | !validateEmptyByType('gem', 'namespace', purl.namespace, { |
| 147 | throws, |
| 148 | }) |
| 149 | ) { |
| 150 | return false |
| 151 | } |
| 152 | if (!validateNoInjectionByType('gem', 'name', purl.name, throws)) { |
| 153 | return false |
| 154 | } |
| 155 | return true |
| 156 | } |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | return purl |
| 24 | } |
| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | import { validateNoInjectionByType } from '../validate.js' |
| 7 | |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | lowerNamespace(purl) |
| 24 | lowerName(purl) |
| 25 | return purl |
| 26 | } |
| 32 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 33 | if ( |
| 34 | !validateNoInjectionByType('github', 'namespace', purl.namespace, throws) |
| 35 | ) { |
| 36 | return false |
| 37 | } |
| 38 | if (!validateNoInjectionByType('github', 'name', purl.name, throws)) { |
| 39 | return false |
| 40 | } |
| 41 | return true |
| 42 | } |
| 5 | import { lowerName, lowerNamespace } from '../strings.js' |
| 6 | import { validateNoInjectionByType } from '../validate.js' |
| 7 | |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | lowerNamespace(purl) |
| 24 | lowerName(purl) |
| 25 | return purl |
| 26 | } |
| 32 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 33 | if ( |
| 34 | !validateNoInjectionByType('gitlab', 'namespace', purl.namespace, throws) |
| 35 | ) { |
| 36 | return false |
| 37 | } |
| 38 | if (!validateNoInjectionByType('gitlab', 'name', purl.name, throws)) { |
| 39 | return false |
| 40 | } |
| 41 | return true |
| 42 | } |
| 30 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 31 | |
| 32 | import { errorMessage, PurlError } from '../error.js' |
| 33 | import { |
| 34 | ArrayPrototypeJoin, |
| 35 | encodeComponent, |
| 36 | StringPrototypeCharCodeAt, |
| 37 | StringPrototypeIncludes, |
| 38 | StringPrototypeReplace, |
| 39 | StringPrototypeSlice, |
| 40 | StringPrototypeSplit, |
| 41 | StringPrototypeToLowerCase, |
| 42 | } from '@socketsecurity/lib/primordials' |
| 43 | import { isSemverString } from '../strings.js' |
| 44 | import { validateNoInjectionByType } from '../validate.js' |
| 45 | |
| 46 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 47 | |
| 48 | interface PurlObject { |
| 49 | name: string |
| 50 | namespace?: string | undefined |
| 51 | qualifiers?: Record<string, string> | undefined |
| 52 | subpath?: string | undefined |
| 53 | type?: string | undefined |
| 54 | version?: string | undefined |
| 55 | } |
| 90 | export async function golangExists( |
| 91 | name: string, |
| 92 | namespace?: string, |
| 93 | version?: string, |
| 94 | options?: ExistsOptions, |
| 95 | ): Promise<ExistsResult> { |
| 96 | const modulePath = namespace ? `${namespace}/${name}` : name |
| 97 | const cacheKey = version |
| 98 | ? `golang:${modulePath}@${version}` |
| 99 | : `golang:${modulePath}` |
| 100 | |
| 101 | if (options?.cache) { |
| 102 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 103 | if (cached !== undefined) { |
| 104 | return cached |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | const fetchResult = async (): Promise<ExistsResult> => { |
| 109 | try { |
| 110 | // Encode the module path for the URL |
| 111 | // Go proxy uses case-encoded paths where uppercase letters are `!lowercase` |
| 112 | const parts = StringPrototypeSplit(modulePath, '/' as any) |
| 113 | for (let i = 0; i < parts.length; i++) { |
| 114 | parts[i] = encodeComponent( |
| 115 | StringPrototypeReplace( |
| 116 | parts[i]!, |
| 117 | /[A-Z]/g, |
| 118 | letter => `!${StringPrototypeToLowerCase(letter)}`, |
| 119 | ), |
| 120 | ) |
| 121 | } |
| 122 | const encodedPath = ArrayPrototypeJoin(parts, '/') |
| 123 | |
| 124 | const url = `https://proxy.golang.org/${encodedPath}/@latest` |
| 125 | |
| 126 | const data = await httpJson<{ |
| 127 | Version?: string |
| 128 | Time?: string |
| 129 | }>(url) |
| 130 | |
| 131 | const latestVersion = data.Version |
| 132 | |
| 133 | if (version) { |
| 134 | const versionUrl = `https://proxy.golang.org/${encodedPath}/@v/${encodeComponent(version)}.info` |
| 135 | try { |
| 136 | await httpJson(versionUrl) |
| 137 | } catch { |
| 138 | const result: ExistsResult = { |
| 139 | exists: false, |
| 140 | error: `Version ${version} not found`, |
| 141 | } |
| 142 | if (latestVersion !== undefined) { |
| 143 | result.latestVersion = latestVersion |
| 144 | } |
| 145 | return result |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | const result: ExistsResult = { exists: true } |
| 150 | if (latestVersion !== undefined) { |
| 151 | result.latestVersion = latestVersion |
| 152 | } |
| 153 | return result |
| 154 | } catch (e) { |
| 156 | const error = errorMessage(e) |
| 157 | return { |
| 158 | exists: false, |
| 159 | error: |
| 160 | StringPrototypeIncludes(error, '404') || |
| 161 | StringPrototypeIncludes(error, '410') |
| 162 | ? 'Module not found' |
| 163 | : error, |
| 164 | } |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | const result = await fetchResult() |
| 170 | // Only cache successful results to avoid negative cache poisoning |
| 171 | // from transient failures (network errors, 5xx responses) |
| 172 | if (options?.cache && result.exists) { |
| 173 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 174 | } |
| 175 | return result |
| 176 | } |
| 183 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 184 | if ( |
| 185 | !validateNoInjectionByType('golang', 'namespace', purl.namespace, throws) |
| 186 | ) { |
| 187 | return false |
| 188 | } |
| 189 | if (!validateNoInjectionByType('golang', 'name', purl.name, throws)) { |
| 190 | return false |
| 191 | } |
| 192 | // Still being lenient here since the standard changes aren't official |
| 193 | // Pending spec change: https://github.com/package-url/purl-spec/pull/196 |
| 194 | const { version } = purl |
| 195 | const length = typeof version === 'string' ? version.length : 0 |
| 196 | // If the version starts with a `"v"` then ensure its a valid semver version |
| 197 | // This, by semver semantics, also supports pseudo-version number |
| 198 | // https://go.dev/doc/modules/version-numbers#pseudo-version-number |
| 199 | if ( |
| 200 | length && |
| 202 | !isSemverString(StringPrototypeSlice(version!, 1)) |
| 203 | ) { |
| 204 | if (throws) { |
| 205 | throw new PurlError( |
| 206 | 'golang "version" component starting with a "v" must be followed by a valid semver version', |
| 207 | ) |
| 208 | } |
| 209 | return false |
| 210 | } |
| 211 | return true |
| 212 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeIncludes, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | |
| 14 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 43 | export async function hackageExists( |
| 44 | name: string, |
| 45 | version?: string, |
| 46 | options?: ExistsOptions, |
| 47 | ): Promise<ExistsResult> { |
| 48 | const cacheKey = version ? `hackage:${name}@${version}` : `hackage:${name}` |
| 49 | |
| 50 | // Try cache first if provided |
| 51 | if (options?.cache) { |
| 52 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 53 | if (cached !== undefined) { |
| 54 | return cached |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | const fetchResult = async (): Promise<ExistsResult> => { |
| 59 | try { |
| 60 | const url = `https://hackage.haskell.org/package/${encodeComponent(name)}/preferred` |
| 61 | |
| 62 | const data = await httpJson<{ |
| 63 | 'normal-version'?: string[] |
| 64 | }>(url) |
| 65 | |
| 66 | const versions = data['normal-version'] || [] |
| 67 | if (versions.length === 0) { |
| 68 | return { exists: false, error: 'No versions found' } |
| 69 | } |
| 70 | |
| 71 | // Latest version is typically the last in the array |
| 72 | const latestVersion = versions[versions.length - 1] |
| 73 | |
| 74 | if (version) { |
| 75 | if (!ArrayPrototypeIncludes(versions, version)) { |
| 76 | const result: ExistsResult = { |
| 77 | exists: false, |
| 78 | error: `Version ${version} not found`, |
| 79 | } |
| 80 | if (latestVersion !== undefined) { |
| 81 | result.latestVersion = latestVersion |
| 82 | } |
| 83 | return result |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | const result: ExistsResult = { exists: true } |
| 88 | if (latestVersion !== undefined) { |
| 89 | result.latestVersion = latestVersion |
| 90 | } |
| 91 | return result |
| 92 | } catch (e) { |
| 94 | const error = errorMessage(e) |
| 95 | return { |
| 96 | exists: false, |
| 97 | error: StringPrototypeIncludes(error, '404') |
| 98 | ? 'Package not found' |
| 99 | : error, |
| 100 | } |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | const result = await fetchResult() |
| 106 | |
| 107 | // Only cache successful results to avoid negative cache poisoning |
| 108 | // from transient failures (network errors, 5xx responses) |
| 109 | if (options?.cache && result.exists) { |
| 110 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 111 | } |
| 112 | |
| 113 | return result |
| 114 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeSome, |
| 10 | StringPrototypeIncludes, |
| 11 | encodeComponent, |
| 12 | } from '@socketsecurity/lib/primordials' |
| 13 | import { lowerName, lowerNamespace } from '../strings.js' |
| 14 | import { validateNoInjectionByType } from '../validate.js' |
| 15 | |
| 16 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 54 | export async function hexExists( |
| 55 | name: string, |
| 56 | version?: string, |
| 57 | options?: ExistsOptions, |
| 58 | ): Promise<ExistsResult> { |
| 59 | const cacheKey = version ? `hex:${name}@${version}` : `hex:${name}` |
| 60 | |
| 61 | if (options?.cache) { |
| 62 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 63 | if (cached !== undefined) { |
| 64 | return cached |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | const fetchResult = async (): Promise<ExistsResult> => { |
| 69 | try { |
| 70 | const url = `https://hex.pm/api/packages/${encodeComponent(name)}` |
| 71 | |
| 72 | const data = await httpJson<{ |
| 73 | latest_version?: string |
| 74 | releases?: Array<{ |
| 75 | version?: string |
| 76 | }> |
| 77 | }>(url) |
| 78 | |
| 79 | const latestVersion = data.latest_version |
| 80 | |
| 81 | if (version) { |
| 82 | const releases = data.releases || [] |
| 83 | const versionExists = ArrayPrototypeSome( |
| 84 | releases, |
| 85 | r => r.version === version, |
| 86 | ) |
| 87 | if (!versionExists) { |
| 88 | const result: ExistsResult = { |
| 89 | exists: false, |
| 90 | error: `Version ${version} not found`, |
| 91 | } |
| 92 | if (latestVersion !== undefined) { |
| 93 | result.latestVersion = latestVersion |
| 94 | } |
| 95 | return result |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | const result: ExistsResult = { exists: true } |
| 100 | if (latestVersion !== undefined) { |
| 101 | result.latestVersion = latestVersion |
| 102 | } |
| 103 | return result |
| 104 | } catch (e) { |
| 106 | const error = errorMessage(e) |
| 107 | return { |
| 108 | exists: false, |
| 109 | error: StringPrototypeIncludes(error, '404') |
| 110 | ? 'Package not found' |
| 111 | : error, |
| 112 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | const result = await fetchResult() |
| 118 | // Only cache successful results to avoid negative cache poisoning |
| 119 | // from transient failures (network errors, 5xx responses) |
| 120 | if (options?.cache && result.exists) { |
| 121 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 122 | } |
| 123 | return result |
| 124 | } |
| 130 | export function normalize(purl: PurlObject): PurlObject { |
| 131 | lowerNamespace(purl) |
| 132 | lowerName(purl) |
| 133 | return purl |
| 134 | } |
| 140 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 141 | if (!validateNoInjectionByType('hex', 'namespace', purl.namespace, throws)) { |
| 142 | return false |
| 143 | } |
| 144 | if (!validateNoInjectionByType('hex', 'name', purl.name, throws)) { |
| 145 | return false |
| 146 | } |
| 147 | return true |
| 148 | } |
| 5 | import { lowerVersion } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerVersion(purl) |
| 23 | return purl |
| 24 | } |
| 8 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 9 | |
| 10 | interface PurlObject { |
| 11 | name: string |
| 12 | namespace?: string | undefined |
| 13 | qualifiers?: Record<string, string> | undefined |
| 14 | subpath?: string | undefined |
| 15 | type?: string | undefined |
| 16 | version?: string | undefined |
| 17 | } |
| 24 | export function normalize(purl: PurlObject): PurlObject { |
| 25 | return purl |
| 26 | } |
| 32 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 33 | if ( |
| 34 | !validateEmptyByType('julia', 'namespace', purl.namespace, { |
| 35 | throws, |
| 36 | }) |
| 37 | ) { |
| 38 | return false |
| 39 | } |
| 40 | if (!validateNoInjectionByType('julia', 'name', purl.name, throws)) { |
| 41 | return false |
| 42 | } |
| 43 | return true |
| 44 | } |
| 5 | import { lowerVersion } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerVersion(purl) |
| 23 | return purl |
| 24 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { errorMessage, PurlError } from '../error.js' |
| 8 | import { |
| 9 | StringPrototypeIncludes, |
| 10 | encodeComponent, |
| 11 | } from '@socketsecurity/lib/primordials' |
| 12 | import { |
| 13 | validateNoInjectionByType, |
| 14 | validateRequiredByType, |
| 15 | } from '../validate.js' |
| 16 | |
| 17 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 18 | |
| 19 | interface PurlObject { |
| 20 | name: string |
| 21 | namespace?: string | undefined |
| 22 | qualifiers?: Record<string, string> | undefined |
| 23 | subpath?: string | undefined |
| 24 | type?: string | undefined |
| 25 | version?: string | undefined |
| 26 | } |
| 57 | export async function mavenExists( |
| 58 | name: string, |
| 59 | namespace?: string, |
| 60 | version?: string, |
| 61 | options?: ExistsOptions, |
| 62 | ): Promise<ExistsResult> { |
| 63 | if (!namespace) { |
| 64 | return { exists: false, error: 'Maven requires namespace (group ID)' } |
| 65 | } |
| 66 | |
| 67 | const packageId = `${namespace}:${name}` |
| 68 | const cacheKey = version |
| 69 | ? `maven:${packageId}@${version}` |
| 70 | : `maven:${packageId}` |
| 71 | |
| 72 | if (options?.cache) { |
| 73 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 74 | if (cached !== undefined) { |
| 75 | return cached |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const fetchResult = async (): Promise<ExistsResult> => { |
| 80 | try { |
| 81 | const g = encodeComponent(namespace) |
| 82 | const a = encodeComponent(name) |
| 83 | const url = `https://search.maven.org/solrsearch/select?q=g:${g}+AND+a:${a}&rows=1&wt=json` |
| 84 | |
| 85 | const data = await httpJson<{ |
| 86 | response?: { |
| 87 | numFound?: number |
| 88 | docs?: Array<{ latestVersion?: string; v?: string }> |
| 89 | } |
| 90 | }>(url) |
| 91 | |
| 92 | const numFound = data.response?.['numFound'] || 0 |
| 93 | if (numFound === 0) { |
| 94 | return { exists: false, error: 'Package not found' } |
| 95 | } |
| 96 | |
| 97 | const doc = data.response?.['docs']?.[0] |
| 98 | const latestVersion = doc?.['latestVersion'] || doc?.['v'] |
| 99 | |
| 100 | if (version) { |
| 101 | const versionUrl = `https://search.maven.org/solrsearch/select?q=g:${g}+AND+a:${a}+AND+v:${encodeComponent(version)}&rows=1&wt=json` |
| 102 | const versionData = await httpJson<{ |
| 103 | response?: { numFound?: number } |
| 104 | }>(versionUrl) |
| 105 | |
| 106 | const versionFound = versionData.response?.['numFound'] || 0 |
| 107 | if (versionFound === 0) { |
| 108 | const result: ExistsResult = { |
| 109 | exists: false, |
| 110 | error: `Version ${version} not found`, |
| 111 | } |
| 112 | if (latestVersion !== undefined) { |
| 113 | result.latestVersion = latestVersion |
| 114 | } |
| 115 | return result |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | const result: ExistsResult = { exists: true } |
| 120 | if (latestVersion !== undefined) { |
| 121 | result.latestVersion = latestVersion |
| 122 | } |
| 123 | return result |
| 124 | } catch (e) { |
| 126 | const error = errorMessage(e) |
| 127 | return { |
| 128 | exists: false, |
| 129 | error: StringPrototypeIncludes(error, '404') |
| 130 | ? 'Package not found' |
| 131 | : error, |
| 132 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | const result = await fetchResult() |
| 138 | // Only cache successful results to avoid negative cache poisoning |
| 139 | // from transient failures (network errors, 5xx responses) |
| 140 | if (options?.cache && result.exists) { |
| 141 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 142 | } |
| 143 | return result |
| 144 | } |
| 151 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 152 | if ( |
| 153 | !validateRequiredByType('maven', 'namespace', purl.namespace, { |
| 154 | throws, |
| 155 | }) |
| 156 | ) { |
| 157 | return false |
| 158 | } |
| 159 | if ( |
| 160 | !validateNoInjectionByType('maven', 'namespace', purl.namespace, throws) |
| 161 | ) { |
| 162 | return false |
| 163 | } |
| 164 | // Maven `groupId` uses dots as separators; a literal `'/'` in the namespace is |
| 165 | // not a valid `groupId` and corrupts downstream URL construction. |
| 166 | if ( |
| 167 | typeof purl.namespace === 'string' && |
| 168 | StringPrototypeIncludes(purl.namespace, '/') |
| 169 | ) { |
| 170 | if (throws) { |
| 171 | throw new PurlError( |
| 172 | 'maven "namespace" component must not contain a slash', |
| 173 | ) |
| 174 | } |
| 175 | return false |
| 176 | } |
| 177 | if (!validateNoInjectionByType('maven', 'name', purl.name, throws)) { |
| 178 | return false |
| 179 | } |
| 180 | return true |
| 181 | } |
| 5 | import { StringPrototypeIncludes } from '@socketsecurity/lib/primordials' |
| 6 | import { lowerName } from '../strings.js' |
| 7 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 8 | |
| 9 | interface PurlObject { |
| 10 | name: string |
| 11 | namespace?: string | undefined |
| 12 | qualifiers?: Record<string, string> | undefined |
| 13 | subpath?: string | undefined |
| 14 | type?: string | undefined |
| 15 | version?: string | undefined |
| 16 | } |
| 23 | export function normalize(purl: PurlObject): PurlObject { |
| 24 | const repoUrl = purl.qualifiers?.['repository_url'] |
| 25 | if (repoUrl !== undefined && StringPrototypeIncludes(repoUrl, 'databricks')) { |
| 26 | lowerName(purl) |
| 27 | } |
| 28 | return purl |
| 29 | } |
| 35 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 36 | if ( |
| 37 | !validateEmptyByType('mlflow', 'namespace', purl.namespace, { |
| 38 | throws, |
| 39 | }) |
| 40 | ) { |
| 41 | return false |
| 42 | } |
| 43 | if (!validateNoInjectionByType('mlflow', 'name', purl.name, throws)) { |
| 44 | return false |
| 45 | } |
| 46 | return true |
| 47 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { encodeComponent } from '../encode.js' |
| 8 | import { errorMessage, PurlError } from '../error.js' |
| 9 | import { |
| 10 | ErrorCtor, |
| 11 | RegExpPrototypeTest, |
| 12 | SetCtor, |
| 13 | StringPrototypeCharCodeAt, |
| 14 | StringPrototypeIncludes, |
| 15 | StringPrototypeIndexOf, |
| 16 | StringPrototypeReplace, |
| 17 | StringPrototypeSlice, |
| 18 | StringPrototypeStartsWith, |
| 19 | StringPrototypeToLowerCase, |
| 20 | StringPrototypeTrim, |
| 21 | } from '@socketsecurity/lib/primordials' |
| 22 | import { isBlank, lowerName, lowerNamespace } from '../strings.js' |
| 23 | import { validateNoInjectionByType } from '../validate.js' |
| 24 | |
| 25 | import type { TtlCache } from '@socketsecurity/lib/cache-with-ttl' |
| 26 | |
| 27 | interface PurlObject { |
| 28 | name: string |
| 29 | namespace?: string | undefined |
| 30 | qualifiers?: Record<string, string> | undefined |
| 31 | subpath?: string | undefined |
| 32 | type?: string | undefined |
| 33 | version?: string | undefined |
| 34 | } |
| 40 | export type ExistsResult = { |
| 41 | exists: boolean |
| 42 | latestVersion?: string |
| 43 | error?: string |
| 44 | } |
| 49 | export type ExistsOptions = { |
| 63 | cache?: TtlCache |
| 64 | } |
| 70 | export type NpmPackageComponents = { |
| 71 | namespace: string | undefined |
| 72 | name: string |
| 73 | version: string | undefined |
| 74 | } |
| 79 | const getNpmBuiltinSet = (() => { |
| 80 | let builtinSet: Set<string> | undefined |
| 81 | return () => { |
| 82 | if (builtinSet === undefined) { |
| 83 | let builtinNames: string[] | undefined |
| 85 | try { |
| 86 | // Try to use Node.js `builtinModules` first |
| 87 | builtinNames = (module.constructor as { builtinModules?: string[] }) |
| 88 | ?.builtinModules |
| 89 | } catch {} |
| 91 | if (!builtinNames) { |
| 92 | // Fallback to hardcoded list |
| 93 | builtinNames = [ |
| 94 | 'assert', |
| 95 | 'async_hooks', |
| 96 | 'buffer', |
| 97 | 'child_process', |
| 98 | 'cluster', |
| 99 | 'console', |
| 100 | 'constants', |
| 101 | 'crypto', |
| 102 | 'dgram', |
| 103 | 'diagnostics_channel', |
| 104 | 'dns', |
| 105 | 'domain', |
| 106 | 'events', |
| 107 | 'fs', |
| 108 | 'http', |
| 109 | 'http2', |
| 110 | 'https', |
| 111 | 'inspector', |
| 112 | 'module', |
| 113 | 'net', |
| 114 | 'os', |
| 115 | 'path', |
| 116 | 'perf_hooks', |
| 117 | 'process', |
| 118 | 'punycode', |
| 119 | 'querystring', |
| 120 | 'readline', |
| 121 | 'repl', |
| 122 | 'stream', |
| 123 | 'string_decoder', |
| 124 | 'sys', |
| 125 | 'timers', |
| 126 | 'tls', |
| 127 | 'trace_events', |
| 128 | 'tty', |
| 129 | 'url', |
| 130 | 'util', |
| 131 | 'v8', |
| 132 | 'vm', |
| 133 | 'wasi', |
| 134 | 'worker_threads', |
| 135 | 'zlib', |
| 136 | ] |
| 137 | } |
| 138 | builtinSet = new SetCtor(builtinNames) |
| 139 | } |
| 140 | return builtinSet |
| 141 | } |
| 142 | })() |
| 147 | function getNpmId(purl: PurlObject): string { |
| 148 | const { name, namespace } = purl |
| 149 | return `${namespace && namespace.length > 0 ? `${namespace}/` : ''}${name}` |
| 150 | } |
| 155 | const getNpmLegacySet = (() => { |
| 156 | let legacySet: Set<string> | undefined |
| 157 | |
| 158 | return (): Set<string> => { |
| 159 | if (legacySet === undefined) { |
| 160 | let fullLegacyNames: string[] |
| 162 | try { |
| 163 | // Try to load the full list from JSON file |
| 164 | fullLegacyNames = require('../../data/npm/legacy-names.json') |
| 165 | } catch { |
| 166 | // Fallback to hardcoded builtin names for simplicity |
| 167 | fullLegacyNames = [ |
| 168 | 'assert', |
| 169 | 'buffer', |
| 170 | 'crypto', |
| 171 | 'events', |
| 172 | 'fs', |
| 173 | 'http', |
| 174 | 'os', |
| 175 | 'path', |
| 176 | 'url', |
| 177 | 'util', |
| 178 | ] |
| 179 | } |
| 181 | legacySet = new SetCtor(fullLegacyNames) |
| 182 | } |
| 183 | return legacySet |
| 184 | } |
| 185 | })() |
| 190 | const isNpmBuiltinName = (id: string): boolean => |
| 191 | getNpmBuiltinSet().has(StringPrototypeToLowerCase(id)) |
| 196 | const isNpmLegacyName = (id: string): boolean => getNpmLegacySet().has(id) |
| 202 | export function normalize(purl: PurlObject): PurlObject { |
| 203 | lowerNamespace(purl) |
| 204 | // Ignore lowercasing legacy names because they could be mixed case |
| 205 | // https://github.com/npm/validate-npm-package-name/tree/v6.0.0?tab=readme-ov-file#legacy-names |
| 206 | if (!isNpmLegacyName(getNpmId(purl))) { |
| 207 | lowerName(purl) |
| 208 | } |
| 209 | return purl |
| 210 | } |
| 252 | export async function npmExists( |
| 253 | name: string, |
| 254 | namespace?: string, |
| 255 | version?: string, |
| 256 | options?: ExistsOptions, |
| 257 | ): Promise<ExistsResult> { |
| 258 | // Build cache key |
| 259 | const packageName = namespace ? `${namespace}/${name}` : name |
| 260 | const cacheKey = version |
| 261 | ? `npm:${packageName}@${version}` |
| 262 | : `npm:${packageName}` |
| 263 | |
| 264 | // Try cache first if provided |
| 265 | if (options?.cache) { |
| 266 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 267 | if (cached !== undefined) { |
| 268 | return cached |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | const fetchResult = async (): Promise<ExistsResult> => { |
| 273 | try { |
| 274 | const encodedName = encodeComponent(packageName) |
| 275 | const url = `https://registry.npmjs.org/${encodedName}` |
| 276 | |
| 277 | const data = await httpJson<{ |
| 278 | 'dist-tags'?: { latest?: string } |
| 279 | versions?: Record<string, unknown> |
| 280 | }>(url) |
| 281 | |
| 282 | const latestVersion = data['dist-tags']?.['latest'] |
| 283 | |
| 284 | // If specific version requested, validate it exists |
| 285 | if (version && data.versions) { |
| 286 | if (!(version in data.versions)) { |
| 287 | const result: ExistsResult = { |
| 288 | exists: false, |
| 289 | error: `Version ${version} not found`, |
| 290 | } |
| 291 | if (latestVersion !== undefined) { |
| 292 | result.latestVersion = latestVersion |
| 293 | } |
| 294 | return result |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | const result: ExistsResult = { |
| 299 | exists: true, |
| 300 | } |
| 301 | if (latestVersion !== undefined) { |
| 302 | result.latestVersion = latestVersion |
| 303 | } |
| 304 | return result |
| 305 | } catch (e) { |
| 307 | // `httpJson` throws on non-2xx status codes |
| 308 | const error = errorMessage(e) |
| 309 | return { |
| 310 | exists: false, |
| 311 | error: StringPrototypeIncludes(error, '404') |
| 312 | ? 'Package not found' |
| 313 | : error, |
| 314 | } |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | const result = await fetchResult() |
| 320 | |
| 321 | // Only cache successful results to avoid negative cache poisoning |
| 322 | // from transient failures (network errors, 5xx responses) |
| 323 | if (options?.cache && result.exists) { |
| 324 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 325 | } |
| 326 | |
| 327 | return result |
| 328 | } |
| 375 | export function parseNpmSpecifier(specifier: unknown): NpmPackageComponents { |
| 376 | if (typeof specifier !== 'string') { |
| 377 | throw new ErrorCtor('npm package specifier string is required.') |
| 378 | } |
| 379 | |
| 380 | if (isBlank(specifier)) { |
| 381 | throw new ErrorCtor('npm package specifier cannot be empty.') |
| 382 | } |
| 383 | |
| 384 | // Handle scoped packages: `@scope/name@version` |
| 385 | let namespace: string | undefined |
| 386 | let name: string |
| 387 | let version: string | undefined |
| 388 | |
| 389 | // Check if it's a scoped package |
| 390 | if (StringPrototypeStartsWith(specifier, '@')) { |
| 391 | // Find the second slash (after `@scope/`) |
| 392 | const slashIndex = StringPrototypeIndexOf(specifier, '/') |
| 393 | if (slashIndex === -1) { |
| 394 | throw new ErrorCtor( |
| 395 | 'npm scoped specifier must contain "/" after scope (e.g. "@scope/name").', |
| 396 | ) |
| 397 | } |
| 398 | |
| 399 | // Find the `@` after the scope |
| 400 | const atIndex = StringPrototypeIndexOf(specifier, '@', slashIndex) |
| 401 | if (atIndex === -1) { |
| 402 | // No version specified |
| 403 | namespace = StringPrototypeSlice(specifier, 0, slashIndex) |
| 404 | name = StringPrototypeSlice(specifier, slashIndex + 1) |
| 405 | } else { |
| 406 | namespace = StringPrototypeSlice(specifier, 0, slashIndex) |
| 407 | name = StringPrototypeSlice(specifier, slashIndex + 1, atIndex) |
| 408 | version = StringPrototypeSlice(specifier, atIndex + 1) |
| 409 | } |
| 410 | } else { |
| 411 | // Non-scoped package: `name@version` |
| 412 | const atIndex = StringPrototypeIndexOf(specifier, '@') |
| 413 | if (atIndex === -1) { |
| 414 | // No version specified |
| 415 | name = specifier |
| 416 | } else { |
| 417 | name = StringPrototypeSlice(specifier, 0, atIndex) |
| 418 | version = StringPrototypeSlice(specifier, atIndex + 1) |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | // Clean up version - remove common npm range prefixes |
| 423 | if (version) { |
| 424 | // Remove leading `^`, `~`, `>=`, `<=`, `>`, `<`, `=` |
| 425 | version = StringPrototypeReplace(version, /^[\^~>=<]+/, '' as any) |
| 426 | // Handle version ranges like `"1.0.0 - 2.0.0"` by taking first version |
| 427 | const spaceIndex = StringPrototypeIndexOf(version, ' ') |
| 428 | if (spaceIndex !== -1) { |
| 429 | version = StringPrototypeSlice(version, 0, spaceIndex) |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | return { namespace, name, version } |
| 434 | } |
| 442 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 443 | const { name, namespace } = purl |
| 444 | // Validate `name` and `namespace` for injection characters |
| 445 | if (!validateNoInjectionByType('npm', 'name', name, throws)) { |
| 446 | return false |
| 447 | } |
| 448 | if (!validateNoInjectionByType('npm', 'namespace', namespace, throws)) { |
| 449 | return false |
| 450 | } |
| 451 | const hasNs = namespace && namespace.length > 0 |
| 452 | const id = getNpmId(purl) |
| 453 | const code0 = StringPrototypeCharCodeAt(id, 0) |
| 454 | const compName = hasNs ? 'namespace' : 'name' |
| 456 | if (throws) { |
| 457 | throw new PurlError( |
| 458 | `npm "${compName}" component cannot start with a period`, |
| 459 | ) |
| 460 | } |
| 461 | return false |
| 462 | } |
| 464 | if (throws) { |
| 465 | throw new PurlError( |
| 466 | `npm "${compName}" component cannot start with an underscore`, |
| 467 | ) |
| 468 | } |
| 469 | return false |
| 470 | } |
| 472 | if (StringPrototypeTrim(name) !== name) { |
| 473 | if (throws) { |
| 474 | throw new PurlError( |
| 475 | 'npm "name" component cannot contain leading or trailing spaces', |
| 476 | ) |
| 477 | } |
| 478 | return false |
| 479 | } |
| 481 | if (encodeComponent(name) !== name) { |
| 482 | if (throws) { |
| 483 | throw new PurlError( |
| 484 | `npm "name" component can only contain URL-friendly characters`, |
| 485 | ) |
| 486 | } |
| 487 | return false |
| 488 | } |
| 489 | if (hasNs) { |
| 491 | if ( |
| 492 | (namespace !== undefined ? StringPrototypeTrim(namespace) : namespace) !== |
| 493 | namespace |
| 494 | ) { |
| 495 | if (throws) { |
| 496 | throw new PurlError( |
| 497 | 'npm "namespace" component cannot contain leading or trailing spaces', |
| 498 | ) |
| 499 | } |
| 500 | return false |
| 501 | } |
| 503 | if (code0 !== 64 ) { |
| 504 | if (throws) { |
| 505 | throw new PurlError( |
| 506 | `npm "namespace" component must start with an "@" character`, |
| 507 | ) |
| 508 | } |
| 509 | return false |
| 510 | } |
| 511 | const namespaceWithoutAtSign = |
| 512 | namespace !== undefined ? StringPrototypeSlice(namespace, 1) : namespace |
| 513 | if (encodeComponent(namespaceWithoutAtSign) !== namespaceWithoutAtSign) { |
| 514 | if (throws) { |
| 515 | throw new PurlError( |
| 516 | `npm "namespace" component can only contain URL-friendly characters`, |
| 517 | ) |
| 518 | } |
| 519 | return false |
| 520 | } |
| 521 | } |
| 522 | const loweredId = StringPrototypeToLowerCase(id) |
| 523 | if (loweredId === 'node_modules' || loweredId === 'favicon.ico') { |
| 524 | if (throws) { |
| 525 | throw new PurlError( |
| 526 | `npm "${compName}" component of "${loweredId}" is not allowed`, |
| 527 | ) |
| 528 | } |
| 529 | return false |
| 530 | } |
| 531 | // The remaining checks are only for modern names |
| 532 | // https://github.com/npm/validate-npm-package-name/tree/v6.0.0?tab=readme-ov-file#naming-rules |
| 533 | if (!isNpmLegacyName(id)) { |
| 534 | if (id.length > 214) { |
| 535 | if (throws) { |
| 536 | // Tested: validation returns false in non-throw mode |
| 537 | // V8 coverage can't see both throw and return false paths in same test |
| 539 | throw new PurlError( |
| 540 | `npm "namespace" and "name" components can not collectively be more than 214 characters`, |
| 541 | ) |
| 543 | } |
| 544 | return false |
| 545 | } |
| 546 | if (loweredId !== id) { |
| 547 | if (throws) { |
| 548 | throw new PurlError( |
| 549 | `npm "name" component can not contain capital letters`, |
| 550 | ) |
| 551 | } |
| 552 | return false |
| 553 | } |
| 555 | if (RegExpPrototypeTest(/[~'!()*]/, name)) { |
| 556 | if (throws) { |
| 557 | throw new PurlError( |
| 558 | `npm "name" component can not contain special characters ("~'!()*")`, |
| 559 | ) |
| 560 | } |
| 561 | return false |
| 562 | } |
| 564 | if (isNpmBuiltinName(id)) { |
| 565 | if (throws) { |
| 566 | // Tested: validation returns false in non-throw mode |
| 567 | // V8 coverage can't see both throw and return false paths in same test |
| 569 | throw new PurlError( |
| 570 | 'npm "name" component can not be a core module name', |
| 571 | ) |
| 573 | } |
| 574 | return false |
| 575 | } |
| 576 | } |
| 577 | return true |
| 578 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | ArrayPrototypeIncludes, |
| 10 | ArrayPrototypePush, |
| 11 | StringPrototypeIncludes, |
| 12 | StringPrototypeToLowerCase, |
| 13 | encodeComponent, |
| 14 | } from '@socketsecurity/lib/primordials' |
| 15 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 16 | |
| 17 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 18 | |
| 19 | interface PurlObject { |
| 20 | name: string |
| 21 | namespace?: string | undefined |
| 22 | qualifiers?: Record<string, string> | undefined |
| 23 | subpath?: string | undefined |
| 24 | type?: string | undefined |
| 25 | version?: string | undefined |
| 26 | } |
| 55 | export async function nugetExists( |
| 56 | name: string, |
| 57 | version?: string, |
| 58 | options?: ExistsOptions, |
| 59 | ): Promise<ExistsResult> { |
| 60 | const cacheKey = version ? `nuget:${name}@${version}` : `nuget:${name}` |
| 61 | |
| 62 | if (options?.cache) { |
| 63 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 64 | if (cached !== undefined) { |
| 65 | return cached |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | const fetchResult = async (): Promise<ExistsResult> => { |
| 70 | try { |
| 71 | const lowerName = StringPrototypeToLowerCase(name) |
| 72 | const url = `https://api.nuget.org/v3/registration5-semver1/${encodeComponent(lowerName)}/index.json` |
| 73 | |
| 74 | const data = await httpJson<{ |
| 75 | items?: Array<{ |
| 76 | items?: Array<{ |
| 77 | catalogEntry?: { |
| 78 | version?: string |
| 79 | } |
| 80 | }> |
| 81 | upper?: string |
| 82 | }> |
| 83 | }>(url) |
| 84 | |
| 85 | if (!data.items || data.items.length === 0) { |
| 86 | return { exists: false, error: 'Package not found' } |
| 87 | } |
| 88 | |
| 89 | // Get all versions from all pages |
| 90 | const versions: string[] = [] |
| 91 | for (const page of data.items) { |
| 92 | if (page.items) { |
| 93 | for (const item of page.items) { |
| 94 | const ver = item.catalogEntry?.['version'] |
| 95 | if (ver) { |
| 96 | ArrayPrototypePush(versions, ver) |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | if (versions.length === 0) { |
| 103 | return { exists: false, error: 'No versions found' } |
| 104 | } |
| 105 | |
| 106 | // Latest version is typically the last one |
| 107 | const latestVersion = versions[versions.length - 1] |
| 108 | |
| 109 | if (version) { |
| 110 | if (!ArrayPrototypeIncludes(versions, version)) { |
| 111 | const result: ExistsResult = { |
| 112 | exists: false, |
| 113 | error: `Version ${version} not found`, |
| 114 | } |
| 115 | if (latestVersion !== undefined) { |
| 116 | result.latestVersion = latestVersion |
| 117 | } |
| 118 | return result |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | const result: ExistsResult = { exists: true } |
| 123 | if (latestVersion !== undefined) { |
| 124 | result.latestVersion = latestVersion |
| 125 | } |
| 126 | return result |
| 127 | } catch (e) { |
| 129 | const error = errorMessage(e) |
| 130 | return { |
| 131 | exists: false, |
| 132 | error: StringPrototypeIncludes(error, '404') |
| 133 | ? 'Package not found' |
| 134 | : error, |
| 135 | } |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | const result = await fetchResult() |
| 141 | // Only cache successful results to avoid negative cache poisoning |
| 142 | // from transient failures (network errors, 5xx responses) |
| 143 | if (options?.cache && result.exists) { |
| 144 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 145 | } |
| 146 | return result |
| 147 | } |
| 153 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 154 | if ( |
| 155 | !validateEmptyByType('nuget', 'namespace', purl.namespace, { |
| 156 | throws, |
| 157 | }) |
| 158 | ) { |
| 159 | return false |
| 160 | } |
| 161 | if (!validateNoInjectionByType('nuget', 'name', purl.name, throws)) { |
| 162 | return false |
| 163 | } |
| 164 | return true |
| 165 | } |
| 5 | import { lowerName, lowerVersion } from '../strings.js' |
| 6 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 7 | |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | lowerName(purl) |
| 24 | lowerVersion(purl) |
| 25 | return purl |
| 26 | } |
| 32 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 33 | if ( |
| 34 | !validateEmptyByType('oci', 'namespace', purl.namespace, { |
| 35 | throws, |
| 36 | }) |
| 37 | ) { |
| 38 | return false |
| 39 | } |
| 40 | if (!validateNoInjectionByType('oci', 'name', purl.name, throws)) { |
| 41 | return false |
| 42 | } |
| 43 | return true |
| 44 | } |
| 7 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 8 | |
| 9 | interface PurlObject { |
| 10 | name: string |
| 11 | namespace?: string | undefined |
| 12 | qualifiers?: Record<string, string> | undefined |
| 13 | subpath?: string | undefined |
| 14 | type?: string | undefined |
| 15 | version?: string | undefined |
| 16 | } |
| 23 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 24 | if ( |
| 25 | !validateEmptyByType('opam', 'namespace', purl.namespace, { |
| 26 | throws, |
| 27 | }) |
| 28 | ) { |
| 29 | return false |
| 30 | } |
| 31 | if (!validateNoInjectionByType('opam', 'name', purl.name, throws)) { |
| 32 | return false |
| 33 | } |
| 34 | return true |
| 35 | } |
| 8 | import { lowerName } from '../strings.js' |
| 9 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 10 | |
| 11 | interface PurlObject { |
| 12 | name: string |
| 13 | namespace?: string | undefined |
| 14 | qualifiers?: Record<string, string> | undefined |
| 15 | subpath?: string | undefined |
| 16 | type?: string | undefined |
| 17 | version?: string | undefined |
| 18 | } |
| 25 | export function normalize(purl: PurlObject): PurlObject { |
| 26 | lowerName(purl) |
| 27 | return purl |
| 28 | } |
| 34 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 35 | if ( |
| 36 | !validateEmptyByType('otp', 'namespace', purl.namespace, { |
| 37 | throws, |
| 38 | }) |
| 39 | ) { |
| 40 | return false |
| 41 | } |
| 42 | if (!validateNoInjectionByType('otp', 'name', purl.name, throws)) { |
| 43 | return false |
| 44 | } |
| 45 | return true |
| 46 | } |
| 5 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 6 | |
| 7 | import { errorMessage, PurlError } from '../error.js' |
| 8 | import { |
| 9 | ArrayPrototypeSome, |
| 10 | StringPrototypeCharCodeAt, |
| 11 | StringPrototypeIncludes, |
| 12 | encodeComponent, |
| 13 | } from '@socketsecurity/lib/primordials' |
| 14 | import { lowerName, replaceDashesWithUnderscores } from '../strings.js' |
| 15 | |
| 16 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 32 | export function normalize(purl: PurlObject): PurlObject { |
| 33 | lowerName(purl) |
| 34 | purl.name = replaceDashesWithUnderscores(purl.name) |
| 35 | return purl |
| 36 | } |
| 64 | export async function pubExists( |
| 65 | name: string, |
| 66 | version?: string, |
| 67 | options?: ExistsOptions, |
| 68 | ): Promise<ExistsResult> { |
| 69 | const cacheKey = version ? `pub:${name}@${version}` : `pub:${name}` |
| 70 | |
| 71 | if (options?.cache) { |
| 72 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 73 | if (cached !== undefined) { |
| 74 | return cached |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | const fetchResult = async (): Promise<ExistsResult> => { |
| 79 | try { |
| 80 | const url = `https://pub.dev/api/packages/${encodeComponent(name)}` |
| 81 | |
| 82 | const data = await httpJson<{ |
| 83 | latest?: { |
| 84 | version?: string |
| 85 | } |
| 86 | versions?: Array<{ |
| 87 | version?: string |
| 88 | }> |
| 89 | }>(url) |
| 90 | |
| 91 | const latestVersion = data.latest?.['version'] |
| 92 | |
| 93 | if (version) { |
| 94 | const versions = data.versions || [] |
| 95 | const versionExists = ArrayPrototypeSome( |
| 96 | versions, |
| 97 | v => v.version === version, |
| 98 | ) |
| 99 | if (!versionExists) { |
| 100 | const result: ExistsResult = { |
| 101 | exists: false, |
| 102 | error: `Version ${version} not found`, |
| 103 | } |
| 104 | if (latestVersion !== undefined) { |
| 105 | result.latestVersion = latestVersion |
| 106 | } |
| 107 | return result |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | const result: ExistsResult = { exists: true } |
| 112 | if (latestVersion !== undefined) { |
| 113 | result.latestVersion = latestVersion |
| 114 | } |
| 115 | return result |
| 116 | } catch (e) { |
| 118 | const error = errorMessage(e) |
| 119 | return { |
| 120 | exists: false, |
| 121 | error: StringPrototypeIncludes(error, '404') |
| 122 | ? 'Package not found' |
| 123 | : error, |
| 124 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | const result = await fetchResult() |
| 130 | // Only cache successful results to avoid negative cache poisoning |
| 131 | // from transient failures (network errors, 5xx responses) |
| 132 | if (options?.cache && result.exists) { |
| 133 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 134 | } |
| 135 | return result |
| 136 | } |
| 142 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 143 | const { name } = purl |
| 144 | for (let i = 0, { length } = name; i < length; i += 1) { |
| 145 | const code = StringPrototypeCharCodeAt(name, i) |
| 146 | // biome-ignore format: newlines |
| 147 | if ( |
| 148 | !( |
| 149 | // 0-9 |
| 150 | ( |
| 151 | (code >= 48 && code <= 57) || |
| 152 | // a-z |
| 153 | (code >= 97 && code <= 122) || |
| 154 | code === 95 |
| 155 | ) |
| 156 | // _ |
| 157 | ) |
| 158 | ) { |
| 159 | if (throws) { |
| 160 | // Tested: validation returns false in non-throw mode |
| 161 | // V8 coverage can't see both throw and return false paths in same test |
| 163 | throw new PurlError( |
| 164 | 'pub "name" component may only contain [a-z0-9_] characters', |
| 165 | ) |
| 166 | } |
| 167 | return false |
| 168 | } |
| 169 | } |
| 170 | return true |
| 171 | } |
| 5 | import { errorMessage } from '../error.js' |
| 6 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 7 | |
| 8 | import { |
| 9 | StringPrototypeIncludes, |
| 10 | encodeComponent, |
| 11 | } from '@socketsecurity/lib/primordials' |
| 12 | import { |
| 13 | lowerName, |
| 14 | lowerNamespace, |
| 15 | lowerVersion, |
| 16 | replaceUnderscoresWithDashes, |
| 17 | } from '../strings.js' |
| 18 | import { validateNoInjectionByType } from '../validate.js' |
| 19 | |
| 20 | import type { ExistsResult, ExistsOptions } from './npm.js' |
| 21 | |
| 22 | interface PurlObject { |
| 23 | name: string |
| 24 | namespace?: string | undefined |
| 25 | qualifiers?: Record<string, string> | undefined |
| 26 | subpath?: string | undefined |
| 27 | type?: string | undefined |
| 28 | version?: string | undefined |
| 29 | } |
| 37 | export function normalize(purl: PurlObject): PurlObject { |
| 38 | lowerNamespace(purl) |
| 39 | lowerName(purl) |
| 40 | lowerVersion(purl) |
| 41 | purl.name = replaceUnderscoresWithDashes(purl.name) |
| 42 | return purl |
| 43 | } |
| 49 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 50 | if (!validateNoInjectionByType('pypi', 'name', purl.name, throws)) { |
| 51 | return false |
| 52 | } |
| 53 | return true |
| 54 | } |
| 91 | export async function pypiExists( |
| 92 | name: string, |
| 93 | version?: string, |
| 94 | options?: ExistsOptions, |
| 95 | ): Promise<ExistsResult> { |
| 96 | const cacheKey = version ? `pypi:${name}@${version}` : `pypi:${name}` |
| 97 | |
| 98 | // Try cache first if provided |
| 99 | if (options?.cache) { |
| 100 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 101 | if (cached !== undefined) { |
| 102 | return cached |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | const fetchResult = async (): Promise<ExistsResult> => { |
| 107 | try { |
| 108 | const url = `https://pypi.org/pypi/${encodeComponent(name)}/json` |
| 109 | |
| 110 | const data = await httpJson<{ |
| 111 | info?: { version?: string } |
| 112 | releases?: Record<string, unknown[]> |
| 113 | }>(url) |
| 114 | |
| 115 | const latestVersion = data.info?.['version'] |
| 116 | |
| 117 | // If specific version requested, validate it exists |
| 118 | if (version && data.releases) { |
| 119 | if (!(version in data.releases)) { |
| 120 | const result: ExistsResult = { |
| 121 | exists: false, |
| 122 | error: `Version ${version} not found`, |
| 123 | } |
| 124 | if (latestVersion !== undefined) { |
| 125 | result.latestVersion = latestVersion |
| 126 | } |
| 127 | return result |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | const result: ExistsResult = { |
| 132 | exists: true, |
| 133 | } |
| 134 | if (latestVersion !== undefined) { |
| 135 | result.latestVersion = latestVersion |
| 136 | } |
| 137 | return result |
| 138 | } catch (e) { |
| 140 | const error = errorMessage(e) |
| 141 | return { |
| 142 | exists: false, |
| 143 | error: StringPrototypeIncludes(error, '404') |
| 144 | ? 'Package not found' |
| 145 | : error, |
| 146 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | const result = await fetchResult() |
| 152 | |
| 153 | // Only cache successful results to avoid negative cache poisoning |
| 154 | // from transient failures (network errors, 5xx responses) |
| 155 | if (options?.cache && result.exists) { |
| 156 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 157 | } |
| 158 | |
| 159 | return result |
| 160 | } |
| 5 | import { lowerNamespace } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerNamespace(purl) |
| 23 | return purl |
| 24 | } |
| 5 | import { lowerNamespace } from '../strings.js' |
| 6 | |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | lowerNamespace(purl) |
| 23 | return purl |
| 24 | } |
| 8 | interface PurlObject { |
| 9 | name: string |
| 10 | namespace?: string | undefined |
| 11 | qualifiers?: Record<string, string> | undefined |
| 12 | subpath?: string | undefined |
| 13 | type?: string | undefined |
| 14 | version?: string | undefined |
| 15 | } |
| 22 | export function normalize(purl: PurlObject): PurlObject { |
| 23 | return purl |
| 24 | } |
| 5 | import { PurlError } from '../error.js' |
| 6 | import { |
| 7 | ObjectFreeze, |
| 8 | RegExpPrototypeTest, |
| 9 | StringPrototypeToLowerCase, |
| 10 | StringPrototypeTrim, |
| 11 | } from '@socketsecurity/lib/primordials' |
| 12 | import { validateNoInjectionByType } from '../validate.js' |
| 13 | |
| 14 | const GUID_PATTERN = ObjectFreeze( |
| 15 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, |
| 16 | ) |
| 17 | |
| 18 | interface PurlObject { |
| 19 | name: string |
| 20 | namespace?: string | undefined |
| 21 | qualifiers?: Record<string, string> | undefined |
| 22 | subpath?: string | undefined |
| 23 | type?: string | undefined |
| 24 | version?: string | undefined |
| 25 | } |
| 33 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 34 | const { qualifiers } = purl |
| 35 | // SWID requires a `tag_id` qualifier |
| 36 | const tagId = qualifiers?.['tag_id'] |
| 37 | if (!tagId) { |
| 38 | if (throws) { |
| 39 | throw new PurlError('swid requires a "tag_id" qualifier') |
| 40 | } |
| 41 | return false |
| 42 | } |
| 43 | // `tag_id` must not be empty after trimming |
| 44 | const tagIdStr = StringPrototypeTrim(String(tagId)) |
| 45 | if (tagIdStr.length === 0) { |
| 47 | if (throws) { |
| 48 | throw new PurlError('swid "tag_id" qualifier must not be empty') |
| 49 | } |
| 50 | return false |
| 51 | } |
| 52 | // If `tag_id` is a GUID, it must be lowercase |
| 53 | if (RegExpPrototypeTest(GUID_PATTERN, tagIdStr)) { |
| 54 | if (tagIdStr !== StringPrototypeToLowerCase(tagIdStr)) { |
| 55 | if (throws) { |
| 56 | throw new PurlError( |
| 57 | 'swid "tag_id" qualifier must be lowercase when it is a GUID', |
| 58 | ) |
| 59 | } |
| 60 | return false |
| 61 | } |
| 62 | } |
| 63 | if (!validateNoInjectionByType('swid', 'name', purl.name, throws)) { |
| 64 | return false |
| 65 | } |
| 66 | return true |
| 67 | } |
| 5 | import { |
| 6 | validateNoInjectionByType, |
| 7 | validateRequiredByType, |
| 8 | } from '../validate.js' |
| 9 | |
| 10 | interface PurlObject { |
| 11 | name: string |
| 12 | namespace?: string | undefined |
| 13 | qualifiers?: Record<string, string> | undefined |
| 14 | subpath?: string | undefined |
| 15 | type?: string | undefined |
| 16 | version?: string | undefined |
| 17 | } |
| 25 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 26 | if ( |
| 27 | !validateRequiredByType('swift', 'namespace', purl.namespace, { |
| 28 | throws, |
| 29 | }) |
| 30 | ) { |
| 31 | return false |
| 32 | } |
| 33 | if (!validateRequiredByType('swift', 'version', purl.version, { throws })) { |
| 34 | return false |
| 35 | } |
| 36 | if ( |
| 37 | !validateNoInjectionByType('swift', 'namespace', purl.namespace, throws) |
| 38 | ) { |
| 39 | return false |
| 40 | } |
| 41 | if (!validateNoInjectionByType('swift', 'name', purl.name, throws)) { |
| 42 | return false |
| 43 | } |
| 44 | return true |
| 45 | } |
| 7 | interface PurlObject { |
| 8 | name: string |
| 9 | namespace?: string | undefined |
| 10 | qualifiers?: Record<string, string> | undefined |
| 11 | subpath?: string | undefined |
| 12 | type?: string | undefined |
| 13 | version?: string | undefined |
| 14 | } |
| 21 | export function normalize(purl: PurlObject): PurlObject { |
| 22 | return purl |
| 23 | } |
| 8 | import { httpJson } from '@socketsecurity/lib/http-request' |
| 9 | |
| 10 | import { errorMessage, PurlError } from '../error.js' |
| 11 | import { |
| 12 | ArrayPrototypeSome, |
| 13 | JSONStringify, |
| 14 | StringPrototypeIncludes, |
| 15 | } from '@socketsecurity/lib/primordials' |
| 16 | import { |
| 17 | isSemverString, |
| 18 | lowerName, |
| 19 | lowerNamespace, |
| 20 | lowerVersion, |
| 21 | } from '../strings.js' |
| 22 | import { |
| 23 | validateNoInjectionByType, |
| 24 | validateRequiredByType, |
| 25 | } from '../validate.js' |
| 26 | |
| 27 | import type { ExistsOptions, ExistsResult } from './npm.js' |
| 28 | |
| 29 | interface PurlObject { |
| 30 | name: string |
| 31 | namespace?: string | undefined |
| 32 | qualifiers?: Record<string, string> | undefined |
| 33 | subpath?: string | undefined |
| 34 | type?: string | undefined |
| 35 | version?: string | undefined |
| 36 | } |
| 44 | export function normalize(purl: PurlObject): PurlObject { |
| 45 | lowerNamespace(purl) |
| 46 | lowerName(purl) |
| 47 | lowerVersion(purl) |
| 48 | return purl |
| 49 | } |
| 56 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 57 | const { name, namespace, version, qualifiers } = purl |
| 58 | // VSCode extensions require a `namespace` (publisher) |
| 59 | if ( |
| 60 | !validateRequiredByType('vscode-extension', 'namespace', namespace, { |
| 61 | throws, |
| 62 | }) |
| 63 | ) { |
| 64 | return false |
| 65 | } |
| 66 | // `namespace` must not contain injection characters |
| 67 | if ( |
| 68 | !validateNoInjectionByType( |
| 69 | 'vscode-extension', |
| 70 | 'namespace', |
| 71 | namespace, |
| 72 | throws, |
| 73 | ) |
| 74 | ) { |
| 75 | return false |
| 76 | } |
| 77 | // `name` must not contain injection characters |
| 78 | if (!validateNoInjectionByType('vscode-extension', 'name', name, throws)) { |
| 79 | return false |
| 80 | } |
| 81 | // `version` must be valid semver when present |
| 82 | if ( |
| 83 | typeof version === 'string' && |
| 84 | version.length > 0 && |
| 85 | !isSemverString(version) |
| 86 | ) { |
| 87 | if (throws) { |
| 88 | throw new PurlError( |
| 89 | 'vscode-extension "version" component must be a valid semver version', |
| 90 | ) |
| 91 | } |
| 92 | return false |
| 93 | } |
| 94 | // `platform` qualifier must not contain injection characters |
| 95 | if ( |
| 96 | !validateNoInjectionByType( |
| 97 | 'vscode-extension', |
| 98 | 'platform', |
| 99 | qualifiers?.['platform'], |
| 100 | throws, |
| 101 | ) |
| 102 | ) { |
| 103 | return false |
| 104 | } |
| 105 | return true |
| 106 | } |
| 145 | export async function vscodeExtensionExists( |
| 146 | name: string, |
| 147 | namespace?: string, |
| 148 | version?: string, |
| 149 | options?: ExistsOptions, |
| 150 | ): Promise<ExistsResult> { |
| 151 | if (!namespace) { |
| 152 | return { |
| 153 | exists: false, |
| 154 | error: 'Namespace (publisher) is required for VSCode extensions', |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | const extensionId = `${namespace}.${name}` |
| 159 | const cacheKey = version |
| 160 | ? `vscode-extension:${extensionId}@${version}` |
| 161 | : `vscode-extension:${extensionId}` |
| 162 | |
| 163 | // Try cache first if provided |
| 164 | if (options?.cache) { |
| 165 | const cached = await options.cache.get<ExistsResult>(cacheKey) |
| 166 | if (cached !== undefined) { |
| 167 | return cached |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | const fetchResult = async (): Promise<ExistsResult> => { |
| 172 | try { |
| 173 | // VS Marketplace API endpoint |
| 174 | const url = |
| 175 | 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' |
| 176 | |
| 177 | const requestBody = { |
| 178 | filters: [ |
| 179 | { |
| 180 | criteria: [ |
| 181 | { |
| 182 | filterType: 7, |
| 183 | value: extensionId, |
| 184 | }, |
| 185 | ], |
| 186 | }, |
| 187 | ], |
| 188 | flags: 914, |
| 189 | } |
| 190 | |
| 191 | const data = await httpJson<{ |
| 192 | results?: Array<{ |
| 193 | extensions?: Array<{ |
| 194 | versions?: Array<{ |
| 195 | version?: string |
| 196 | }> |
| 197 | }> |
| 198 | }> |
| 199 | }>(url, { |
| 200 | method: 'POST', |
| 201 | headers: { |
| 202 | 'Content-Type': 'application/json', |
| 203 | Accept: 'application/json;api-version=7.1-preview.1', |
| 204 | }, |
| 205 | body: JSONStringify(requestBody), |
| 206 | }) |
| 207 | |
| 208 | const extensions = data.results?.[0]?.['extensions'] |
| 209 | if (!extensions || extensions.length === 0) { |
| 210 | return { |
| 211 | exists: false, |
| 212 | error: 'Extension not found', |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | const versions = extensions[0]?.['versions'] |
| 217 | const latestVersion = versions?.[0]?.['version'] |
| 218 | |
| 219 | // If specific version requested, validate it exists |
| 220 | if (version && versions) { |
| 221 | const versionExists = ArrayPrototypeSome( |
| 222 | versions, |
| 223 | v => v.version === version, |
| 224 | ) |
| 225 | if (!versionExists) { |
| 226 | const result: ExistsResult = { |
| 227 | exists: false, |
| 228 | error: `Version ${version} not found`, |
| 229 | } |
| 230 | if (latestVersion !== undefined) { |
| 231 | result.latestVersion = latestVersion |
| 232 | } |
| 233 | return result |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | const result: ExistsResult = { |
| 238 | exists: true, |
| 239 | } |
| 240 | if (latestVersion !== undefined) { |
| 241 | result.latestVersion = latestVersion |
| 242 | } |
| 243 | return result |
| 244 | } catch (e) { |
| 246 | const error = errorMessage(e) |
| 248 | // IMPORTANT: `httpJson()` throws on non-2xx responses, so we cannot inspect |
| 249 | // `response.status` directly. We rely on the error message containing `"404"` |
| 250 | // to distinguish "not found" from other HTTP errors. |
| 251 | // This depends on `@socketsecurity/lib`'s error message format: |
| 252 | // `"HTTP 404: Not Found"` or similar containing the status code. |
| 253 | // If upstream changes error format, this string matching may break. |
| 254 | return { |
| 255 | exists: false, |
| 256 | error: StringPrototypeIncludes(error, '404') |
| 257 | ? 'Extension not found' |
| 258 | : error, |
| 259 | } |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | const result = await fetchResult() |
| 264 | |
| 265 | // Only cache successful results to avoid negative cache poisoning |
| 266 | // from transient failures (network errors, 5xx responses) |
| 267 | if (options?.cache && result.exists) { |
| 268 | await options.cache.set(cacheKey, Object.freeze(result)) |
| 269 | } |
| 270 | |
| 271 | return result |
| 272 | } |
| 8 | import { lowerName } from '../strings.js' |
| 9 | import { validateEmptyByType, validateNoInjectionByType } from '../validate.js' |
| 10 | |
| 11 | interface PurlObject { |
| 12 | name: string |
| 13 | namespace?: string | undefined |
| 14 | qualifiers?: Record<string, string> | undefined |
| 15 | subpath?: string | undefined |
| 16 | type?: string | undefined |
| 17 | version?: string | undefined |
| 18 | } |
| 25 | export function normalize(purl: PurlObject): PurlObject { |
| 26 | lowerName(purl) |
| 27 | return purl |
| 28 | } |
| 34 | export function validate(purl: PurlObject, throws: boolean): boolean { |
| 35 | if ( |
| 36 | !validateEmptyByType('yocto', 'namespace', purl.namespace, { |
| 37 | throws, |
| 38 | }) |
| 39 | ) { |
| 40 | return false |
| 41 | } |
| 42 | if (!validateNoInjectionByType('yocto', 'name', purl.name, throws)) { |
| 43 | return false |
| 44 | } |
| 45 | return true |
| 46 | } |