UNPKG

@dreamhorizonorg/sentinel

Version:

Open-source, zero-dependency tool that blocks compromised packages BEFORE download. Built to counter supply chain and credential theft attacks like Shai-Hulud.

123 lines (101 loc) 3.82 kB
/** * HTTP utilities * Pure functions for HTTP requests and responses */ import http from 'http'; import https from 'https'; import { URL } from 'url'; import { HTTP_DEFAULT_METHOD, HTTP_PORTS, NPM_REGISTRY_URL } from '../constants/app.constants.mjs'; import { HTTP_STATUS, HTTP_TIMEOUT_MS } from '../constants/http.constants.mjs'; /** * Fetch JSON from HTTP/HTTPS endpoint * @param {string} url - URL to fetch * @param {Object} options - Request options * @param {string} [options.method] - HTTP method (GET, POST, etc.) * @param {Object} [options.headers] - Request headers * @param {string|Object} [options.body] - Request body (stringified if object) * @param {number} [options.timeout] - Request timeout in milliseconds */ export const fetchJsonFromUrl = (url, options = {}) => { return new Promise((resolve, reject) => { const urlObj = new URL(url); const client = urlObj.protocol === 'https:' ? https : http; const method = options.method ?? HTTP_DEFAULT_METHOD; const headers = options.headers ?? {}; const timeout = options.timeout ?? HTTP_TIMEOUT_MS; const body = options.body ? (typeof options.body === 'string' ? options.body : JSON.stringify(options.body)) : null; if (body) { headers['Content-Length'] = Buffer.byteLength(body); } const requestOptions = { hostname: urlObj.hostname, port: urlObj.port ?? (urlObj.protocol === 'https:' ? HTTP_PORTS.HTTPS : HTTP_PORTS.HTTP), path: urlObj.pathname + urlObj.search, method: method, headers: headers }; let req; const timeoutId = setTimeout(() => { if (req) { req.destroy(); } reject(new Error(`Request timeout for ${url}`)); }, timeout); req = client.request(requestOptions, (res) => { // Handle redirects (only for GET requests) const isRedirect = res.statusCode >= HTTP_STATUS.REDIRECT_MIN && res.statusCode < HTTP_STATUS.REDIRECT_MAX; if (isRedirect && res.headers.location && method === 'GET') { clearTimeout(timeoutId); return fetchJsonFromUrl(res.headers.location, options).then(resolve).catch(reject); } if (res.statusCode !== HTTP_STATUS.OK) { clearTimeout(timeoutId); reject(new Error(`HTTP ${res.statusCode} from ${url}`)); return; } let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { clearTimeout(timeoutId); try { const json = JSON.parse(data); resolve(json); } catch (error) { reject(new Error(`Failed to parse JSON from ${url}: ${error.message}`)); } }); }); req.on('error', (error) => { clearTimeout(timeoutId); reject(new Error(`Failed to fetch from ${url}: ${error.message}`)); }); // Send body for POST/PUT requests if (body) { req.write(body); } req.end(); }); }; /** * Resolve the latest version of an npm package * @param {string} packageName - Package name (supports scoped packages like @scope/name) * @returns {Promise<string|null>} - Latest version or null if not found */ export const resolveLatestVersion = async (packageName) => { try { // Encode package name for URL (handles scoped packages like @scope/name) const encodedName = encodeURIComponent(packageName).replace('%40', '@'); const url = `${NPM_REGISTRY_URL}/${encodedName}`; const data = await fetchJsonFromUrl(url, { timeout: 10000 }); if (data && data['dist-tags'] && data['dist-tags'].latest) { return data['dist-tags'].latest; } return null; } catch { // Package not found or registry error return null; } };