@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
JavaScript
/**
* 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;
}
};