@socketsecurity/lib 2.8.4 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/http-request.ts"],
4
- "sourcesContent": ["/**\n * @fileoverview HTTP/HTTPS request utilities using Node.js built-in modules with retry logic, redirects, and download support.\n *\n * This module provides a fetch-like API built on top of Node.js native `http` and `https` modules.\n * It supports automatic retries with exponential backoff, redirect following, streaming downloads,\n * and provides a familiar fetch-style response interface.\n *\n * Key Features:\n * - Automatic retries with exponential backoff for failed requests.\n * - Redirect following with configurable max redirects.\n * - Streaming downloads with progress callbacks.\n * - Fetch-like response interface (`.json()`, `.text()`, `.arrayBuffer()`).\n * - Timeout support for all operations.\n * - Zero dependencies on external HTTP libraries.\n */\n\nimport { createWriteStream } from 'node:fs'\n\nimport type { IncomingMessage } from 'node:http'\n\nlet _http: typeof import('http') | undefined\nlet _https: typeof import('https') | undefined\n/**\n * Lazily load http and https modules to avoid Webpack errors.\n * @private\n */\n/*@__NO_SIDE_EFFECTS__*/\nfunction getHttp() {\n if (_http === undefined) {\n // Use non-'node:' prefixed require to avoid Webpack errors.\n\n _http = /*@__PURE__*/ require('node:http')\n }\n return _http as typeof import('http')\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction getHttps() {\n if (_https === undefined) {\n // Use non-'node:' prefixed require to avoid Webpack errors.\n\n _https = /*@__PURE__*/ require('node:https')\n }\n return _https as typeof import('https')\n}\n\n/**\n * Configuration options for HTTP/HTTPS requests.\n */\nexport interface HttpRequestOptions {\n /**\n * Request body to send.\n * Can be a string (e.g., JSON) or Buffer (e.g., binary data).\n *\n * @example\n * ```ts\n * // Send JSON data\n * await httpRequest('https://api.example.com/data', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Alice' }),\n * headers: { 'Content-Type': 'application/json' }\n * })\n *\n * // Send binary data\n * const buffer = Buffer.from([0x00, 0x01, 0x02])\n * await httpRequest('https://api.example.com/upload', {\n * method: 'POST',\n * body: buffer\n * })\n * ```\n */\n body?: Buffer | string | undefined\n /**\n * Whether to automatically follow HTTP redirects (3xx status codes).\n *\n * @default true\n *\n * @example\n * ```ts\n * // Follow redirects (default)\n * await httpRequest('https://example.com/redirect')\n *\n * // Don't follow redirects\n * const response = await httpRequest('https://example.com/redirect', {\n * followRedirects: false\n * })\n * console.log(response.status) // 301 or 302\n * ```\n */\n followRedirects?: boolean | undefined\n /**\n * HTTP headers to send with the request.\n * A `User-Agent` header is automatically added if not provided.\n *\n * @example\n * ```ts\n * await httpRequest('https://api.example.com/data', {\n * headers: {\n * 'Authorization': 'Bearer token123',\n * 'Content-Type': 'application/json',\n * 'Accept': 'application/json'\n * }\n * })\n * ```\n */\n headers?: Record<string, string> | undefined\n /**\n * Maximum number of redirects to follow before throwing an error.\n * Only relevant when `followRedirects` is `true`.\n *\n * @default 5\n *\n * @example\n * ```ts\n * // Allow up to 10 redirects\n * await httpRequest('https://example.com/many-redirects', {\n * maxRedirects: 10\n * })\n * ```\n */\n maxRedirects?: number | undefined\n /**\n * HTTP method to use for the request.\n *\n * @default 'GET'\n *\n * @example\n * ```ts\n * // GET request (default)\n * await httpRequest('https://api.example.com/data')\n *\n * // POST request\n * await httpRequest('https://api.example.com/data', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Alice' })\n * })\n *\n * // DELETE request\n * await httpRequest('https://api.example.com/data/123', {\n * method: 'DELETE'\n * })\n * ```\n */\n method?: string | undefined\n /**\n * Number of retry attempts for failed requests.\n * Uses exponential backoff: delay = `retryDelay` * 2^attempt.\n *\n * @default 0\n *\n * @example\n * ```ts\n * // Retry up to 3 times with exponential backoff\n * await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000 // 1s, then 2s, then 4s\n * })\n * ```\n */\n retries?: number | undefined\n /**\n * Initial delay in milliseconds before first retry.\n * Subsequent retries use exponential backoff.\n *\n * @default 1000\n *\n * @example\n * ```ts\n * // Start with 2 second delay, then 4s, 8s, etc.\n * await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 2000\n * })\n * ```\n */\n retryDelay?: number | undefined\n /**\n * Request timeout in milliseconds.\n * If the request takes longer than this, it will be aborted.\n *\n * @default 30000\n *\n * @example\n * ```ts\n * // 60 second timeout\n * await httpRequest('https://api.example.com/slow-endpoint', {\n * timeout: 60000\n * })\n * ```\n */\n timeout?: number | undefined\n}\n\n/**\n * HTTP response object with fetch-like interface.\n * Provides multiple ways to access the response body.\n */\nexport interface HttpResponse {\n /**\n * Get response body as ArrayBuffer.\n * Useful for binary data or when you need compatibility with browser APIs.\n *\n * @returns The response body as an ArrayBuffer\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/image.png')\n * const arrayBuffer = response.arrayBuffer()\n * console.log(arrayBuffer.byteLength)\n * ```\n */\n arrayBuffer(): ArrayBuffer\n /**\n * Raw response body as Buffer.\n * Direct access to the underlying Node.js Buffer.\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/data')\n * console.log(response.body.length) // Size in bytes\n * console.log(response.body.toString('hex')) // View as hex\n * ```\n */\n body: Buffer\n /**\n * HTTP response headers.\n * Keys are lowercase header names, values can be strings or string arrays.\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.headers['content-type'])\n * console.log(response.headers['set-cookie']) // May be string[]\n * ```\n */\n headers: Record<string, string | string[] | undefined>\n /**\n * Parse response body as JSON.\n * Type parameter `T` allows specifying the expected JSON structure.\n *\n * @template T - Expected JSON type (defaults to `unknown`)\n * @returns Parsed JSON data\n * @throws {SyntaxError} When response body is not valid JSON\n *\n * @example\n * ```ts\n * interface User { name: string; id: number }\n * const response = await httpRequest('https://api.example.com/user')\n * const user = response.json<User>()\n * console.log(user.name, user.id)\n * ```\n */\n json<T = unknown>(): T\n /**\n * Whether the request was successful (status code 200-299).\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/data')\n * if (response.ok) {\n * console.log('Success:', response.json())\n * } else {\n * console.error('Failed:', response.status, response.statusText)\n * }\n * ```\n */\n ok: boolean\n /**\n * HTTP status code (e.g., 200, 404, 500).\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.status) // 200, 404, etc.\n * ```\n */\n status: number\n /**\n * HTTP status message (e.g., \"OK\", \"Not Found\", \"Internal Server Error\").\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.statusText) // \"OK\"\n * ```\n */\n statusText: string\n /**\n * Get response body as UTF-8 text string.\n *\n * @returns The response body as a string\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * const html = response.text()\n * console.log(html.includes('<html>'))\n * ```\n */\n text(): string\n}\n\n/**\n * Configuration options for file downloads.\n */\nexport interface HttpDownloadOptions {\n /**\n * HTTP headers to send with the download request.\n * A `User-Agent` header is automatically added if not provided.\n *\n * @example\n * ```ts\n * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {\n * headers: {\n * 'Authorization': 'Bearer token123'\n * }\n * })\n * ```\n */\n headers?: Record<string, string> | undefined\n /**\n * Callback for tracking download progress.\n * Called periodically as data is received.\n *\n * @param downloaded - Number of bytes downloaded so far\n * @param total - Total file size in bytes (from Content-Length header)\n *\n * @example\n * ```ts\n * await httpDownload('https://example.com/large-file.zip', '/tmp/file.zip', {\n * onProgress: (downloaded, total) => {\n * const percent = ((downloaded / total) * 100).toFixed(1)\n * console.log(`Progress: ${percent}%`)\n * }\n * })\n * ```\n */\n onProgress?: ((downloaded: number, total: number) => void) | undefined\n /**\n * Number of retry attempts for failed downloads.\n * Uses exponential backoff: delay = `retryDelay` * 2^attempt.\n *\n * @default 0\n *\n * @example\n * ```ts\n * // Retry up to 3 times for unreliable connections\n * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {\n * retries: 3,\n * retryDelay: 2000\n * })\n * ```\n */\n retries?: number | undefined\n /**\n * Initial delay in milliseconds before first retry.\n * Subsequent retries use exponential backoff.\n *\n * @default 1000\n */\n retryDelay?: number | undefined\n /**\n * Download timeout in milliseconds.\n * If the download takes longer than this, it will be aborted.\n *\n * @default 120000\n *\n * @example\n * ```ts\n * // 5 minute timeout for large files\n * await httpDownload('https://example.com/huge-file.zip', '/tmp/file.zip', {\n * timeout: 300000\n * })\n * ```\n */\n timeout?: number | undefined\n}\n\n/**\n * Result of a successful file download.\n */\nexport interface HttpDownloadResult {\n /**\n * Absolute path where the file was saved.\n *\n * @example\n * ```ts\n * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')\n * console.log(`Downloaded to: ${result.path}`)\n * ```\n */\n path: string\n /**\n * Total size of downloaded file in bytes.\n *\n * @example\n * ```ts\n * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')\n * console.log(`Downloaded ${result.size} bytes`)\n * ```\n */\n size: number\n}\n\n/**\n * Make an HTTP/HTTPS request with retry logic and redirect support.\n * Provides a fetch-like API using Node.js native http/https modules.\n *\n * This is the main entry point for making HTTP requests. It handles retries,\n * redirects, timeouts, and provides a fetch-compatible response interface.\n *\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to response object with `.json()`, `.text()`, etc.\n * @throws {Error} When all retries are exhausted, timeout occurs, or non-retryable error happens\n *\n * @example\n * ```ts\n * // Simple GET request\n * const response = await httpRequest('https://api.example.com/data')\n * const data = response.json()\n *\n * // POST with JSON body\n * const response = await httpRequest('https://api.example.com/users', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })\n * })\n *\n * // With retries and timeout\n * const response = await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000,\n * timeout: 60000\n * })\n *\n * // Don't follow redirects\n * const response = await httpRequest('https://example.com/redirect', {\n * followRedirects: false\n * })\n * console.log(response.status) // 301, 302, etc.\n * ```\n */\nexport async function httpRequest(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<HttpResponse> {\n const {\n body,\n followRedirects = true,\n headers = {},\n maxRedirects = 5,\n method = 'GET',\n retries = 0,\n retryDelay = 1000,\n timeout = 30_000,\n } = { __proto__: null, ...options } as HttpRequestOptions\n\n // Retry logic with exponential backoff\n let lastError: Error | undefined\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n // eslint-disable-next-line no-await-in-loop\n return await httpRequestAttempt(url, {\n body,\n followRedirects,\n headers,\n maxRedirects,\n method,\n timeout,\n })\n } catch (e) {\n lastError = e as Error\n\n // Last attempt - throw error\n if (attempt === retries) {\n break\n }\n\n // Retry with exponential backoff\n const delayMs = retryDelay * 2 ** attempt\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, delayMs))\n }\n }\n\n throw lastError || new Error('Request failed after retries')\n}\n\n/**\n * Single HTTP request attempt (used internally by httpRequest with retry logic).\n * @private\n */\nasync function httpRequestAttempt(\n url: string,\n options: HttpRequestOptions,\n): Promise<HttpResponse> {\n const {\n body,\n followRedirects = true,\n headers = {},\n maxRedirects = 5,\n method = 'GET',\n timeout = 30_000,\n } = { __proto__: null, ...options } as HttpRequestOptions\n\n return await new Promise((resolve, reject) => {\n const parsedUrl = new URL(url)\n const isHttps = parsedUrl.protocol === 'https:'\n const httpModule = isHttps ? getHttps() : getHttp()\n\n const requestOptions = {\n headers: {\n 'User-Agent': 'socket-registry/1.0',\n ...headers,\n },\n hostname: parsedUrl.hostname,\n method,\n path: parsedUrl.pathname + parsedUrl.search,\n port: parsedUrl.port,\n timeout,\n }\n\n const request = httpModule.request(\n requestOptions,\n (res: IncomingMessage) => {\n // Handle redirects\n if (\n followRedirects &&\n res.statusCode &&\n res.statusCode >= 300 &&\n res.statusCode < 400 &&\n res.headers.location\n ) {\n if (maxRedirects <= 0) {\n reject(\n new Error(\n `Too many redirects (exceeded maximum: ${maxRedirects})`,\n ),\n )\n return\n }\n\n // Follow redirect\n const redirectUrl = res.headers.location.startsWith('http')\n ? res.headers.location\n : new URL(res.headers.location, url).toString()\n\n resolve(\n httpRequestAttempt(redirectUrl, {\n body,\n followRedirects,\n headers,\n maxRedirects: maxRedirects - 1,\n method,\n timeout,\n }),\n )\n return\n }\n\n // Collect response data\n const chunks: Buffer[] = []\n res.on('data', (chunk: Buffer) => {\n chunks.push(chunk)\n })\n\n res.on('end', () => {\n const responseBody = Buffer.concat(chunks)\n const ok =\n res.statusCode !== undefined &&\n res.statusCode >= 200 &&\n res.statusCode < 300\n\n const response: HttpResponse = {\n arrayBuffer(): ArrayBuffer {\n return responseBody.buffer.slice(\n responseBody.byteOffset,\n responseBody.byteOffset + responseBody.byteLength,\n )\n },\n body: responseBody,\n headers: res.headers as Record<\n string,\n string | string[] | undefined\n >,\n json<T = unknown>(): T {\n return JSON.parse(responseBody.toString('utf8')) as T\n },\n ok,\n status: res.statusCode || 0,\n statusText: res.statusMessage || '',\n text(): string {\n return responseBody.toString('utf8')\n },\n }\n\n resolve(response)\n })\n },\n )\n\n request.on('error', (error: Error) => {\n const err = new Error(`HTTP request failed: ${error.message}`, {\n cause: error,\n })\n reject(err)\n })\n\n request.on('timeout', () => {\n request.destroy()\n reject(new Error(`Request timed out after ${timeout}ms`))\n })\n\n // Send body if present\n if (body) {\n request.write(body)\n }\n\n request.end()\n })\n}\n\n/**\n * Download a file from a URL to a local path with retry logic and progress callbacks.\n * Uses streaming to avoid loading entire file in memory.\n *\n * The download is streamed directly to disk, making it memory-efficient even for\n * large files. Progress callbacks allow for real-time download status updates.\n *\n * @param url - The URL to download from (must start with http:// or https://)\n * @param destPath - Absolute path where the file should be saved\n * @param options - Download configuration options\n * @returns Promise resolving to download result with path and size\n * @throws {Error} When all retries are exhausted, download fails, or file cannot be written\n *\n * @example\n * ```ts\n * // Simple download\n * const result = await httpDownload(\n * 'https://example.com/file.zip',\n * '/tmp/file.zip'\n * )\n * console.log(`Downloaded ${result.size} bytes to ${result.path}`)\n *\n * // With progress tracking\n * await httpDownload(\n * 'https://example.com/large-file.zip',\n * '/tmp/file.zip',\n * {\n * onProgress: (downloaded, total) => {\n * const percent = ((downloaded / total) * 100).toFixed(1)\n * console.log(`Progress: ${percent}% (${downloaded}/${total} bytes)`)\n * }\n * }\n * )\n *\n * // With retries and custom timeout\n * await httpDownload(\n * 'https://example.com/file.zip',\n * '/tmp/file.zip',\n * {\n * retries: 3,\n * retryDelay: 2000,\n * timeout: 300000, // 5 minutes\n * headers: { 'Authorization': 'Bearer token123' }\n * }\n * )\n * ```\n */\nexport async function httpDownload(\n url: string,\n destPath: string,\n options?: HttpDownloadOptions | undefined,\n): Promise<HttpDownloadResult> {\n const {\n headers = {},\n onProgress,\n retries = 0,\n retryDelay = 1000,\n timeout = 120_000,\n } = { __proto__: null, ...options } as HttpDownloadOptions\n\n // Retry logic with exponential backoff\n let lastError: Error | undefined\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n // eslint-disable-next-line no-await-in-loop\n return await httpDownloadAttempt(url, destPath, {\n headers,\n onProgress,\n timeout,\n })\n } catch (e) {\n lastError = e as Error\n\n // Last attempt - throw error\n if (attempt === retries) {\n break\n }\n\n // Retry with exponential backoff\n const delayMs = retryDelay * 2 ** attempt\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, delayMs))\n }\n }\n\n throw lastError || new Error('Download failed after retries')\n}\n\n/**\n * Single download attempt (used internally by httpDownload with retry logic).\n * @private\n */\nasync function httpDownloadAttempt(\n url: string,\n destPath: string,\n options: HttpDownloadOptions,\n): Promise<HttpDownloadResult> {\n const {\n headers = {},\n onProgress,\n timeout = 120_000,\n } = { __proto__: null, ...options } as HttpDownloadOptions\n\n return await new Promise((resolve, reject) => {\n const parsedUrl = new URL(url)\n const isHttps = parsedUrl.protocol === 'https:'\n const httpModule = isHttps ? getHttps() : getHttp()\n\n const requestOptions = {\n headers: {\n 'User-Agent': 'socket-registry/1.0',\n ...headers,\n },\n hostname: parsedUrl.hostname,\n method: 'GET',\n path: parsedUrl.pathname + parsedUrl.search,\n port: parsedUrl.port,\n timeout,\n }\n\n let fileStream: ReturnType<typeof createWriteStream> | undefined\n let streamClosed = false\n\n const closeStream = () => {\n if (!streamClosed && fileStream) {\n streamClosed = true\n fileStream.close()\n }\n }\n\n const request = httpModule.request(\n requestOptions,\n (res: IncomingMessage) => {\n // Check status code\n if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {\n closeStream()\n reject(\n new Error(\n `Download failed: HTTP ${res.statusCode} ${res.statusMessage}`,\n ),\n )\n return\n }\n\n const totalSize = Number.parseInt(\n res.headers['content-length'] || '0',\n 10,\n )\n let downloadedSize = 0\n\n // Create write stream\n fileStream = createWriteStream(destPath)\n\n fileStream.on('error', (error: Error) => {\n closeStream()\n const err = new Error(`Failed to write file: ${error.message}`, {\n cause: error,\n })\n reject(err)\n })\n\n res.on('data', (chunk: Buffer) => {\n downloadedSize += chunk.length\n if (onProgress && totalSize > 0) {\n onProgress(downloadedSize, totalSize)\n }\n })\n\n res.on('end', () => {\n fileStream?.close(() => {\n streamClosed = true\n resolve({\n path: destPath,\n size: downloadedSize,\n })\n })\n })\n\n res.on('error', (error: Error) => {\n closeStream()\n reject(error)\n })\n\n // Pipe response to file\n res.pipe(fileStream)\n },\n )\n\n request.on('error', (error: Error) => {\n closeStream()\n const err = new Error(`HTTP download failed: ${error.message}`, {\n cause: error,\n })\n reject(err)\n })\n\n request.on('timeout', () => {\n request.destroy()\n closeStream()\n reject(new Error(`Download timed out after ${timeout}ms`))\n })\n\n request.end()\n })\n}\n\n/**\n * Perform a GET request and parse JSON response.\n * Convenience wrapper around `httpRequest` for common JSON API calls.\n *\n * @template T - Expected JSON response type (defaults to `unknown`)\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to parsed JSON data\n * @throws {Error} When request fails, response is not ok (status < 200 or >= 300), or JSON parsing fails\n *\n * @example\n * ```ts\n * // Simple JSON GET\n * const data = await httpGetJson('https://api.example.com/data')\n * console.log(data)\n *\n * // With type safety\n * interface User { id: number; name: string; email: string }\n * const user = await httpGetJson<User>('https://api.example.com/user/123')\n * console.log(user.name, user.email)\n *\n * // With custom headers\n * const data = await httpGetJson('https://api.example.com/data', {\n * headers: {\n * 'Authorization': 'Bearer token123',\n * 'Accept': 'application/json'\n * }\n * })\n *\n * // With retries\n * const data = await httpGetJson('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000\n * })\n * ```\n */\nexport async function httpGetJson<T = unknown>(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<T> {\n const response = await httpRequest(url, { ...options, method: 'GET' })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n try {\n return response.json<T>()\n } catch (e) {\n throw new Error('Failed to parse JSON response', { cause: e })\n }\n}\n\n/**\n * Perform a GET request and return text response.\n * Convenience wrapper around `httpRequest` for fetching text content.\n *\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to response body as UTF-8 string\n * @throws {Error} When request fails or response is not ok (status < 200 or >= 300)\n *\n * @example\n * ```ts\n * // Fetch HTML\n * const html = await httpGetText('https://example.com')\n * console.log(html.includes('<!DOCTYPE html>'))\n *\n * // Fetch plain text\n * const text = await httpGetText('https://example.com/file.txt')\n * console.log(text)\n *\n * // With custom headers\n * const text = await httpGetText('https://example.com/data.txt', {\n * headers: {\n * 'Authorization': 'Bearer token123'\n * }\n * })\n *\n * // With timeout\n * const text = await httpGetText('https://example.com/large-file.txt', {\n * timeout: 60000 // 1 minute\n * })\n * ```\n */\nexport async function httpGetText(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<string> {\n const response = await httpRequest(url, { ...options, method: 'GET' })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n return response.text()\n}\n"],
5
- "mappings": "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",
6
- "names": ["http_request_exports", "__export", "httpDownload", "httpGetJson", "httpGetText", "httpRequest", "__toCommonJS", "import_node_fs", "_http", "_https", "getHttp", "getHttps", "url", "options", "body", "followRedirects", "headers", "maxRedirects", "method", "retries", "retryDelay", "timeout", "lastError", "attempt", "httpRequestAttempt", "e", "delayMs", "resolve", "reject", "parsedUrl", "httpModule", "requestOptions", "request", "res", "redirectUrl", "chunks", "chunk", "responseBody", "ok", "response", "error", "err", "destPath", "onProgress", "httpDownloadAttempt", "fileStream", "streamClosed", "closeStream", "totalSize", "downloadedSize"]
4
+ "sourcesContent": ["/**\n * @fileoverview HTTP/HTTPS request utilities using Node.js built-in modules with retry logic, redirects, and download support.\n *\n * This module provides a fetch-like API built on top of Node.js native `http` and `https` modules.\n * It supports automatic retries with exponential backoff, redirect following, streaming downloads,\n * and provides a familiar fetch-style response interface.\n *\n * Key Features:\n * - Automatic retries with exponential backoff for failed requests.\n * - Redirect following with configurable max redirects.\n * - Streaming downloads with progress callbacks.\n * - Fetch-like response interface (`.json()`, `.text()`, `.arrayBuffer()`).\n * - Timeout support for all operations.\n * - Zero dependencies on external HTTP libraries.\n */\n\nimport { createWriteStream } from 'node:fs'\n\nimport type { IncomingMessage } from 'node:http'\n\nlet _http: typeof import('http') | undefined\nlet _https: typeof import('https') | undefined\n/**\n * Lazily load http and https modules to avoid Webpack errors.\n * @private\n */\n/*@__NO_SIDE_EFFECTS__*/\nfunction getHttp() {\n if (_http === undefined) {\n // Use non-'node:' prefixed require to avoid Webpack errors.\n\n _http = /*@__PURE__*/ require('node:http')\n }\n return _http as typeof import('http')\n}\n\n/*@__NO_SIDE_EFFECTS__*/\nfunction getHttps() {\n if (_https === undefined) {\n // Use non-'node:' prefixed require to avoid Webpack errors.\n\n _https = /*@__PURE__*/ require('node:https')\n }\n return _https as typeof import('https')\n}\n\n/**\n * Configuration options for HTTP/HTTPS requests.\n */\nexport interface HttpRequestOptions {\n /**\n * Request body to send.\n * Can be a string (e.g., JSON) or Buffer (e.g., binary data).\n *\n * @example\n * ```ts\n * // Send JSON data\n * await httpRequest('https://api.example.com/data', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Alice' }),\n * headers: { 'Content-Type': 'application/json' }\n * })\n *\n * // Send binary data\n * const buffer = Buffer.from([0x00, 0x01, 0x02])\n * await httpRequest('https://api.example.com/upload', {\n * method: 'POST',\n * body: buffer\n * })\n * ```\n */\n body?: Buffer | string | undefined\n /**\n * Whether to automatically follow HTTP redirects (3xx status codes).\n *\n * @default true\n *\n * @example\n * ```ts\n * // Follow redirects (default)\n * await httpRequest('https://example.com/redirect')\n *\n * // Don't follow redirects\n * const response = await httpRequest('https://example.com/redirect', {\n * followRedirects: false\n * })\n * console.log(response.status) // 301 or 302\n * ```\n */\n followRedirects?: boolean | undefined\n /**\n * HTTP headers to send with the request.\n * A `User-Agent` header is automatically added if not provided.\n *\n * @example\n * ```ts\n * await httpRequest('https://api.example.com/data', {\n * headers: {\n * 'Authorization': 'Bearer token123',\n * 'Content-Type': 'application/json',\n * 'Accept': 'application/json'\n * }\n * })\n * ```\n */\n headers?: Record<string, string> | undefined\n /**\n * Maximum number of redirects to follow before throwing an error.\n * Only relevant when `followRedirects` is `true`.\n *\n * @default 5\n *\n * @example\n * ```ts\n * // Allow up to 10 redirects\n * await httpRequest('https://example.com/many-redirects', {\n * maxRedirects: 10\n * })\n * ```\n */\n maxRedirects?: number | undefined\n /**\n * HTTP method to use for the request.\n *\n * @default 'GET'\n *\n * @example\n * ```ts\n * // GET request (default)\n * await httpRequest('https://api.example.com/data')\n *\n * // POST request\n * await httpRequest('https://api.example.com/data', {\n * method: 'POST',\n * body: JSON.stringify({ name: 'Alice' })\n * })\n *\n * // DELETE request\n * await httpRequest('https://api.example.com/data/123', {\n * method: 'DELETE'\n * })\n * ```\n */\n method?: string | undefined\n /**\n * Number of retry attempts for failed requests.\n * Uses exponential backoff: delay = `retryDelay` * 2^attempt.\n *\n * @default 0\n *\n * @example\n * ```ts\n * // Retry up to 3 times with exponential backoff\n * await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000 // 1s, then 2s, then 4s\n * })\n * ```\n */\n retries?: number | undefined\n /**\n * Initial delay in milliseconds before first retry.\n * Subsequent retries use exponential backoff.\n *\n * @default 1000\n *\n * @example\n * ```ts\n * // Start with 2 second delay, then 4s, 8s, etc.\n * await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 2000\n * })\n * ```\n */\n retryDelay?: number | undefined\n /**\n * Request timeout in milliseconds.\n * If the request takes longer than this, it will be aborted.\n *\n * @default 30000\n *\n * @example\n * ```ts\n * // 60 second timeout\n * await httpRequest('https://api.example.com/slow-endpoint', {\n * timeout: 60000\n * })\n * ```\n */\n timeout?: number | undefined\n}\n\n/**\n * HTTP response object with fetch-like interface.\n * Provides multiple ways to access the response body.\n */\nexport interface HttpResponse {\n /**\n * Get response body as ArrayBuffer.\n * Useful for binary data or when you need compatibility with browser APIs.\n *\n * @returns The response body as an ArrayBuffer\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/image.png')\n * const arrayBuffer = response.arrayBuffer()\n * console.log(arrayBuffer.byteLength)\n * ```\n */\n arrayBuffer(): ArrayBuffer\n /**\n * Raw response body as Buffer.\n * Direct access to the underlying Node.js Buffer.\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/data')\n * console.log(response.body.length) // Size in bytes\n * console.log(response.body.toString('hex')) // View as hex\n * ```\n */\n body: Buffer\n /**\n * HTTP response headers.\n * Keys are lowercase header names, values can be strings or string arrays.\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.headers['content-type'])\n * console.log(response.headers['set-cookie']) // May be string[]\n * ```\n */\n headers: Record<string, string | string[] | undefined>\n /**\n * Parse response body as JSON.\n * Type parameter `T` allows specifying the expected JSON structure.\n *\n * @template T - Expected JSON type (defaults to `unknown`)\n * @returns Parsed JSON data\n * @throws {SyntaxError} When response body is not valid JSON\n *\n * @example\n * ```ts\n * interface User { name: string; id: number }\n * const response = await httpRequest('https://api.example.com/user')\n * const user = response.json<User>()\n * console.log(user.name, user.id)\n * ```\n */\n json<T = unknown>(): T\n /**\n * Whether the request was successful (status code 200-299).\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com/data')\n * if (response.ok) {\n * console.log('Success:', response.json())\n * } else {\n * console.error('Failed:', response.status, response.statusText)\n * }\n * ```\n */\n ok: boolean\n /**\n * HTTP status code (e.g., 200, 404, 500).\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.status) // 200, 404, etc.\n * ```\n */\n status: number\n /**\n * HTTP status message (e.g., \"OK\", \"Not Found\", \"Internal Server Error\").\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * console.log(response.statusText) // \"OK\"\n * ```\n */\n statusText: string\n /**\n * Get response body as UTF-8 text string.\n *\n * @returns The response body as a string\n *\n * @example\n * ```ts\n * const response = await httpRequest('https://example.com')\n * const html = response.text()\n * console.log(html.includes('<html>'))\n * ```\n */\n text(): string\n}\n\n/**\n * Configuration options for file downloads.\n */\nexport interface HttpDownloadOptions {\n /**\n * HTTP headers to send with the download request.\n * A `User-Agent` header is automatically added if not provided.\n *\n * @example\n * ```ts\n * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {\n * headers: {\n * 'Authorization': 'Bearer token123'\n * }\n * })\n * ```\n */\n headers?: Record<string, string> | undefined\n /**\n * Callback for tracking download progress.\n * Called periodically as data is received.\n *\n * @param downloaded - Number of bytes downloaded so far\n * @param total - Total file size in bytes (from Content-Length header)\n *\n * @example\n * ```ts\n * await httpDownload('https://example.com/large-file.zip', '/tmp/file.zip', {\n * onProgress: (downloaded, total) => {\n * const percent = ((downloaded / total) * 100).toFixed(1)\n * console.log(`Progress: ${percent}%`)\n * }\n * })\n * ```\n */\n onProgress?: ((downloaded: number, total: number) => void) | undefined\n /**\n * Number of retry attempts for failed downloads.\n * Uses exponential backoff: delay = `retryDelay` * 2^attempt.\n *\n * @default 0\n *\n * @example\n * ```ts\n * // Retry up to 3 times for unreliable connections\n * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {\n * retries: 3,\n * retryDelay: 2000\n * })\n * ```\n */\n retries?: number | undefined\n /**\n * Initial delay in milliseconds before first retry.\n * Subsequent retries use exponential backoff.\n *\n * @default 1000\n */\n retryDelay?: number | undefined\n /**\n * Download timeout in milliseconds.\n * If the download takes longer than this, it will be aborted.\n *\n * @default 120000\n *\n * @example\n * ```ts\n * // 5 minute timeout for large files\n * await httpDownload('https://example.com/huge-file.zip', '/tmp/file.zip', {\n * timeout: 300000\n * })\n * ```\n */\n timeout?: number | undefined\n}\n\n/**\n * Result of a successful file download.\n */\nexport interface HttpDownloadResult {\n /**\n * Absolute path where the file was saved.\n *\n * @example\n * ```ts\n * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')\n * console.log(`Downloaded to: ${result.path}`)\n * ```\n */\n path: string\n /**\n * Total size of downloaded file in bytes.\n *\n * @example\n * ```ts\n * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')\n * console.log(`Downloaded ${result.size} bytes`)\n * ```\n */\n size: number\n}\n\n/**\n * Make an HTTP/HTTPS request with retry logic and redirect support.\n * Provides a fetch-like API using Node.js native http/https modules.\n *\n * This is the main entry point for making HTTP requests. It handles retries,\n * redirects, timeouts, and provides a fetch-compatible response interface.\n *\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to response object with `.json()`, `.text()`, etc.\n * @throws {Error} When all retries are exhausted, timeout occurs, or non-retryable error happens\n *\n * @example\n * ```ts\n * // Simple GET request\n * const response = await httpRequest('https://api.example.com/data')\n * const data = response.json()\n *\n * // POST with JSON body\n * const response = await httpRequest('https://api.example.com/users', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })\n * })\n *\n * // With retries and timeout\n * const response = await httpRequest('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000,\n * timeout: 60000\n * })\n *\n * // Don't follow redirects\n * const response = await httpRequest('https://example.com/redirect', {\n * followRedirects: false\n * })\n * console.log(response.status) // 301, 302, etc.\n * ```\n */\nexport async function httpRequest(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<HttpResponse> {\n const {\n body,\n followRedirects = true,\n headers = {},\n maxRedirects = 5,\n method = 'GET',\n retries = 0,\n retryDelay = 1000,\n timeout = 30_000,\n } = { __proto__: null, ...options } as HttpRequestOptions\n\n // Retry logic with exponential backoff\n let lastError: Error | undefined\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n // eslint-disable-next-line no-await-in-loop\n return await httpRequestAttempt(url, {\n body,\n followRedirects,\n headers,\n maxRedirects,\n method,\n timeout,\n })\n } catch (e) {\n lastError = e as Error\n\n // Last attempt - throw error\n if (attempt === retries) {\n break\n }\n\n // Retry with exponential backoff\n const delayMs = retryDelay * 2 ** attempt\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, delayMs))\n }\n }\n\n throw lastError || new Error('Request failed after retries')\n}\n\n/**\n * Single HTTP request attempt (used internally by httpRequest with retry logic).\n * @private\n */\nasync function httpRequestAttempt(\n url: string,\n options: HttpRequestOptions,\n): Promise<HttpResponse> {\n const {\n body,\n followRedirects = true,\n headers = {},\n maxRedirects = 5,\n method = 'GET',\n timeout = 30_000,\n } = { __proto__: null, ...options } as HttpRequestOptions\n\n return await new Promise((resolve, reject) => {\n const parsedUrl = new URL(url)\n const isHttps = parsedUrl.protocol === 'https:'\n const httpModule = isHttps ? getHttps() : getHttp()\n\n const requestOptions = {\n headers: {\n 'User-Agent': 'socket-registry/1.0',\n ...headers,\n },\n hostname: parsedUrl.hostname,\n method,\n path: parsedUrl.pathname + parsedUrl.search,\n port: parsedUrl.port,\n timeout,\n }\n\n const request = httpModule.request(\n requestOptions,\n (res: IncomingMessage) => {\n // Handle redirects\n if (\n followRedirects &&\n res.statusCode &&\n res.statusCode >= 300 &&\n res.statusCode < 400 &&\n res.headers.location\n ) {\n if (maxRedirects <= 0) {\n reject(\n new Error(\n `Too many redirects (exceeded maximum: ${maxRedirects})`,\n ),\n )\n return\n }\n\n // Follow redirect\n const redirectUrl = res.headers.location.startsWith('http')\n ? res.headers.location\n : new URL(res.headers.location, url).toString()\n\n resolve(\n httpRequestAttempt(redirectUrl, {\n body,\n followRedirects,\n headers,\n maxRedirects: maxRedirects - 1,\n method,\n timeout,\n }),\n )\n return\n }\n\n // Collect response data\n const chunks: Buffer[] = []\n res.on('data', (chunk: Buffer) => {\n chunks.push(chunk)\n })\n\n res.on('end', () => {\n const responseBody = Buffer.concat(chunks)\n const ok =\n res.statusCode !== undefined &&\n res.statusCode >= 200 &&\n res.statusCode < 300\n\n const response: HttpResponse = {\n arrayBuffer(): ArrayBuffer {\n return responseBody.buffer.slice(\n responseBody.byteOffset,\n responseBody.byteOffset + responseBody.byteLength,\n )\n },\n body: responseBody,\n headers: res.headers as Record<\n string,\n string | string[] | undefined\n >,\n json<T = unknown>(): T {\n return JSON.parse(responseBody.toString('utf8')) as T\n },\n ok,\n status: res.statusCode || 0,\n statusText: res.statusMessage || '',\n text(): string {\n return responseBody.toString('utf8')\n },\n }\n\n resolve(response)\n })\n },\n )\n\n request.on('error', (error: Error) => {\n const code = (error as NodeJS.ErrnoException).code\n let message = `HTTP request failed for ${url}: ${error.message}\\n`\n\n if (code === 'ENOTFOUND') {\n message +=\n 'DNS lookup failed. Check the hostname and your network connection.'\n } else if (code === 'ECONNREFUSED') {\n message +=\n 'Connection refused. Verify the server is running and accessible.'\n } else if (code === 'ETIMEDOUT') {\n message +=\n 'Request timed out. Check your network or increase the timeout value.'\n } else if (code === 'ECONNRESET') {\n message +=\n 'Connection reset. The server may have closed the connection unexpectedly.'\n } else {\n message +=\n 'Check your network connection and verify the URL is correct.'\n }\n\n reject(new Error(message, { cause: error }))\n })\n\n request.on('timeout', () => {\n request.destroy()\n reject(new Error(`Request timed out after ${timeout}ms`))\n })\n\n // Send body if present\n if (body) {\n request.write(body)\n }\n\n request.end()\n })\n}\n\n/**\n * Download a file from a URL to a local path with retry logic and progress callbacks.\n * Uses streaming to avoid loading entire file in memory.\n *\n * The download is streamed directly to disk, making it memory-efficient even for\n * large files. Progress callbacks allow for real-time download status updates.\n *\n * @param url - The URL to download from (must start with http:// or https://)\n * @param destPath - Absolute path where the file should be saved\n * @param options - Download configuration options\n * @returns Promise resolving to download result with path and size\n * @throws {Error} When all retries are exhausted, download fails, or file cannot be written\n *\n * @example\n * ```ts\n * // Simple download\n * const result = await httpDownload(\n * 'https://example.com/file.zip',\n * '/tmp/file.zip'\n * )\n * console.log(`Downloaded ${result.size} bytes to ${result.path}`)\n *\n * // With progress tracking\n * await httpDownload(\n * 'https://example.com/large-file.zip',\n * '/tmp/file.zip',\n * {\n * onProgress: (downloaded, total) => {\n * const percent = ((downloaded / total) * 100).toFixed(1)\n * console.log(`Progress: ${percent}% (${downloaded}/${total} bytes)`)\n * }\n * }\n * )\n *\n * // With retries and custom timeout\n * await httpDownload(\n * 'https://example.com/file.zip',\n * '/tmp/file.zip',\n * {\n * retries: 3,\n * retryDelay: 2000,\n * timeout: 300000, // 5 minutes\n * headers: { 'Authorization': 'Bearer token123' }\n * }\n * )\n * ```\n */\nexport async function httpDownload(\n url: string,\n destPath: string,\n options?: HttpDownloadOptions | undefined,\n): Promise<HttpDownloadResult> {\n const {\n headers = {},\n onProgress,\n retries = 0,\n retryDelay = 1000,\n timeout = 120_000,\n } = { __proto__: null, ...options } as HttpDownloadOptions\n\n // Retry logic with exponential backoff\n let lastError: Error | undefined\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n // eslint-disable-next-line no-await-in-loop\n return await httpDownloadAttempt(url, destPath, {\n headers,\n onProgress,\n timeout,\n })\n } catch (e) {\n lastError = e as Error\n\n // Last attempt - throw error\n if (attempt === retries) {\n break\n }\n\n // Retry with exponential backoff\n const delayMs = retryDelay * 2 ** attempt\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, delayMs))\n }\n }\n\n throw lastError || new Error('Download failed after retries')\n}\n\n/**\n * Single download attempt (used internally by httpDownload with retry logic).\n * @private\n */\nasync function httpDownloadAttempt(\n url: string,\n destPath: string,\n options: HttpDownloadOptions,\n): Promise<HttpDownloadResult> {\n const {\n headers = {},\n onProgress,\n timeout = 120_000,\n } = { __proto__: null, ...options } as HttpDownloadOptions\n\n return await new Promise((resolve, reject) => {\n const parsedUrl = new URL(url)\n const isHttps = parsedUrl.protocol === 'https:'\n const httpModule = isHttps ? getHttps() : getHttp()\n\n const requestOptions = {\n headers: {\n 'User-Agent': 'socket-registry/1.0',\n ...headers,\n },\n hostname: parsedUrl.hostname,\n method: 'GET',\n path: parsedUrl.pathname + parsedUrl.search,\n port: parsedUrl.port,\n timeout,\n }\n\n let fileStream: ReturnType<typeof createWriteStream> | undefined\n let streamClosed = false\n\n const closeStream = () => {\n if (!streamClosed && fileStream) {\n streamClosed = true\n fileStream.close()\n }\n }\n\n const request = httpModule.request(\n requestOptions,\n (res: IncomingMessage) => {\n // Check status code\n if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {\n closeStream()\n reject(\n new Error(\n `Download failed: HTTP ${res.statusCode} ${res.statusMessage}`,\n ),\n )\n return\n }\n\n const totalSize = Number.parseInt(\n res.headers['content-length'] || '0',\n 10,\n )\n let downloadedSize = 0\n\n // Create write stream\n fileStream = createWriteStream(destPath)\n\n fileStream.on('error', (error: Error) => {\n closeStream()\n const err = new Error(`Failed to write file: ${error.message}`, {\n cause: error,\n })\n reject(err)\n })\n\n res.on('data', (chunk: Buffer) => {\n downloadedSize += chunk.length\n if (onProgress && totalSize > 0) {\n onProgress(downloadedSize, totalSize)\n }\n })\n\n res.on('end', () => {\n fileStream?.close(() => {\n streamClosed = true\n resolve({\n path: destPath,\n size: downloadedSize,\n })\n })\n })\n\n res.on('error', (error: Error) => {\n closeStream()\n reject(error)\n })\n\n // Pipe response to file\n res.pipe(fileStream)\n },\n )\n\n request.on('error', (error: Error) => {\n closeStream()\n const code = (error as NodeJS.ErrnoException).code\n let message = `HTTP download failed for ${url}: ${error.message}\\n`\n\n if (code === 'ENOTFOUND') {\n message +=\n 'DNS lookup failed. Check the hostname and your network connection.'\n } else if (code === 'ECONNREFUSED') {\n message +=\n 'Connection refused. Verify the server is running and accessible.'\n } else if (code === 'ETIMEDOUT') {\n message +=\n 'Request timed out. Check your network or increase the timeout value.'\n } else if (code === 'ECONNRESET') {\n message +=\n 'Connection reset. The server may have closed the connection unexpectedly.'\n } else {\n message +=\n 'Check your network connection and verify the URL is correct.'\n }\n\n reject(new Error(message, { cause: error }))\n })\n\n request.on('timeout', () => {\n request.destroy()\n closeStream()\n reject(new Error(`Download timed out after ${timeout}ms`))\n })\n\n request.end()\n })\n}\n\n/**\n * Perform a GET request and parse JSON response.\n * Convenience wrapper around `httpRequest` for common JSON API calls.\n *\n * @template T - Expected JSON response type (defaults to `unknown`)\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to parsed JSON data\n * @throws {Error} When request fails, response is not ok (status < 200 or >= 300), or JSON parsing fails\n *\n * @example\n * ```ts\n * // Simple JSON GET\n * const data = await httpGetJson('https://api.example.com/data')\n * console.log(data)\n *\n * // With type safety\n * interface User { id: number; name: string; email: string }\n * const user = await httpGetJson<User>('https://api.example.com/user/123')\n * console.log(user.name, user.email)\n *\n * // With custom headers\n * const data = await httpGetJson('https://api.example.com/data', {\n * headers: {\n * 'Authorization': 'Bearer token123',\n * 'Accept': 'application/json'\n * }\n * })\n *\n * // With retries\n * const data = await httpGetJson('https://api.example.com/data', {\n * retries: 3,\n * retryDelay: 1000\n * })\n * ```\n */\nexport async function httpGetJson<T = unknown>(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<T> {\n const response = await httpRequest(url, { ...options, method: 'GET' })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n try {\n return response.json<T>()\n } catch (e) {\n throw new Error('Failed to parse JSON response', { cause: e })\n }\n}\n\n/**\n * Perform a GET request and return text response.\n * Convenience wrapper around `httpRequest` for fetching text content.\n *\n * @param url - The URL to request (must start with http:// or https://)\n * @param options - Request configuration options\n * @returns Promise resolving to response body as UTF-8 string\n * @throws {Error} When request fails or response is not ok (status < 200 or >= 300)\n *\n * @example\n * ```ts\n * // Fetch HTML\n * const html = await httpGetText('https://example.com')\n * console.log(html.includes('<!DOCTYPE html>'))\n *\n * // Fetch plain text\n * const text = await httpGetText('https://example.com/file.txt')\n * console.log(text)\n *\n * // With custom headers\n * const text = await httpGetText('https://example.com/data.txt', {\n * headers: {\n * 'Authorization': 'Bearer token123'\n * }\n * })\n *\n * // With timeout\n * const text = await httpGetText('https://example.com/large-file.txt', {\n * timeout: 60000 // 1 minute\n * })\n * ```\n */\nexport async function httpGetText(\n url: string,\n options?: HttpRequestOptions | undefined,\n): Promise<string> {\n const response = await httpRequest(url, { ...options, method: 'GET' })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n\n return response.text()\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["http_request_exports", "__export", "httpDownload", "httpGetJson", "httpGetText", "httpRequest", "__toCommonJS", "import_node_fs", "_http", "_https", "getHttp", "getHttps", "url", "options", "body", "followRedirects", "headers", "maxRedirects", "method", "retries", "retryDelay", "timeout", "lastError", "attempt", "httpRequestAttempt", "e", "delayMs", "resolve", "reject", "parsedUrl", "httpModule", "requestOptions", "request", "res", "redirectUrl", "chunks", "chunk", "responseBody", "ok", "response", "error", "code", "message", "destPath", "onProgress", "httpDownloadAttempt", "fileStream", "streamClosed", "closeStream", "totalSize", "downloadedSize", "err"]
7
7
  }
@@ -1,3 +1,14 @@
1
1
  /* Socket Lib - Built with esbuild */
2
- var m=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var y=(n,e)=>{for(var r in e)m(n,r,{get:e[r],enumerable:!0})},T=(n,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of g(e))!p.call(n,i)&&i!==r&&m(n,i,{get:()=>e[i],enumerable:!(s=v(e,i))||s.enumerable});return n};var w=n=>T(m({},"__esModule",{value:!0}),n);var x={};y(x,{processLock:()=>L});module.exports=w(x);var t=require("node:fs"),o=require("./fs"),a=require("./logger"),f=require("./promises"),l=require("./signal-exit");class S{activeLocks=new Set;touchTimers=new Map;exitHandlerRegistered=!1;ensureExitHandler(){this.exitHandlerRegistered||((0,l.onExit)(()=>{for(const e of this.touchTimers.values())clearInterval(e);this.touchTimers.clear();for(const e of this.activeLocks)try{(0,t.existsSync)(e)&&(0,o.safeDeleteSync)(e,{recursive:!0})}catch{}}),this.exitHandlerRegistered=!0)}touchLock(e){try{if((0,t.existsSync)(e)){const r=new Date;(0,t.utimesSync)(e,r,r)}}catch(r){a.logger.warn(`Failed to touch lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}startTouchTimer(e,r){if(r<=0||this.touchTimers.has(e))return;const s=setInterval(()=>{this.touchLock(e)},r);s.unref(),this.touchTimers.set(e,s)}stopTouchTimer(e){const r=this.touchTimers.get(e);r&&(clearInterval(r),this.touchTimers.delete(e))}isStale(e,r){try{if(!(0,t.existsSync)(e))return!1;const s=(0,t.statSync)(e),i=Math.floor((Date.now()-s.mtime.getTime())/1e3),c=Math.floor(r/1e3);return i>c}catch{return!1}}async acquire(e,r={}){const{baseDelayMs:s=100,maxDelayMs:i=1e3,retries:c=3,staleMs:d=5e3,touchIntervalMs:h=2e3}=r;return this.ensureExitHandler(),await(0,f.pRetry)(async()=>{try{if((0,t.existsSync)(e)&&this.isStale(e,d)){a.logger.log(`Removing stale lock: ${e}`);try{(0,o.safeDeleteSync)(e,{recursive:!0})}catch{}}return(0,t.mkdirSync)(e,{recursive:!1}),this.activeLocks.add(e),this.startTouchTimer(e,h),()=>this.release(e)}catch(u){throw u instanceof Error&&u.code==="EEXIST"?this.isStale(e,d)?new Error(`Stale lock detected: ${e}`):new Error(`Lock already exists: ${e}`):u}},{retries:c,baseDelayMs:s,maxDelayMs:i,jitter:!0})}release(e){this.stopTouchTimer(e);try{(0,t.existsSync)(e)&&(0,o.safeDeleteSync)(e,{recursive:!0}),this.activeLocks.delete(e)}catch(r){a.logger.warn(`Failed to release lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}async withLock(e,r,s){const i=await this.acquire(e,s);try{return await r()}finally{i()}}}const L=new S;0&&(module.exports={processLock});
2
+ var f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var E=(n,e)=>{for(var r in e)f(n,r,{get:e[r],enumerable:!0})},T=(n,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of w(e))!g.call(n,i)&&i!==r&&f(n,i,{get:()=>e[i],enumerable:!(s=v(e,i))||s.enumerable});return n};var S=n=>T(f({},"__esModule",{value:!0}),n);var L={};E(L,{processLock:()=>$});module.exports=S(L);var t=require("node:fs"),u=require("./fs"),d=require("./logger"),p=require("./promises"),h=require("./signal-exit");class x{activeLocks=new Set;touchTimers=new Map;exitHandlerRegistered=!1;ensureExitHandler(){this.exitHandlerRegistered||((0,h.onExit)(()=>{for(const e of this.touchTimers.values())clearInterval(e);this.touchTimers.clear();for(const e of this.activeLocks)try{(0,t.existsSync)(e)&&(0,u.safeDeleteSync)(e,{recursive:!0})}catch{}}),this.exitHandlerRegistered=!0)}touchLock(e){try{if((0,t.existsSync)(e)){const r=new Date;(0,t.utimesSync)(e,r,r)}}catch(r){d.logger.warn(`Failed to touch lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}startTouchTimer(e,r){if(r<=0||this.touchTimers.has(e))return;const s=setInterval(()=>{this.touchLock(e)},r);s.unref(),this.touchTimers.set(e,s)}stopTouchTimer(e){const r=this.touchTimers.get(e);r&&(clearInterval(r),this.touchTimers.delete(e))}isStale(e,r){try{if(!(0,t.existsSync)(e))return!1;const s=(0,t.statSync)(e),i=Math.floor((Date.now()-s.mtime.getTime())/1e3),m=Math.floor(r/1e3);return i>m}catch{return!1}}async acquire(e,r={}){const{baseDelayMs:s=100,maxDelayMs:i=1e3,retries:m=3,staleMs:l=5e3,touchIntervalMs:y=2e3}=r;return this.ensureExitHandler(),await(0,p.pRetry)(async()=>{try{if((0,t.existsSync)(e)&&this.isStale(e,l)){d.logger.log(`Removing stale lock: ${e}`);try{(0,u.safeDeleteSync)(e,{recursive:!0})}catch{}}return(0,t.mkdirSync)(e,{recursive:!1}),this.activeLocks.add(e),this.startTouchTimer(e,y),()=>this.release(e)}catch(o){const a=o.code;if(a==="EEXIST")throw this.isStale(e,l)?new Error(`Stale lock detected: ${e}`):new Error(`Lock already exists: ${e}`);if(a==="EACCES"||a==="EPERM")throw new Error(`Permission denied creating lock: ${e}. Check directory permissions or run with appropriate access.`,{cause:o});if(a==="EROFS")throw new Error(`Cannot create lock on read-only filesystem: ${e}`,{cause:o});if(a==="ENOTDIR"){const c=e.slice(0,e.lastIndexOf("/"));throw new Error(`Cannot create lock directory: ${e}
3
+ A path component is a file when it should be a directory.
4
+ Parent path: ${c}
5
+ To resolve:
6
+ 1. Check if "${c}" contains a file instead of a directory
7
+ 2. Remove any conflicting files in the path
8
+ 3. Ensure the full parent directory structure exists`,{cause:o})}if(a==="ENOENT"){const c=e.slice(0,e.lastIndexOf("/"));throw new Error(`Cannot create lock directory: ${e}
9
+ Parent directory does not exist: ${c}
10
+ To resolve:
11
+ 1. Ensure the parent directory "${c}" exists
12
+ 2. Create the directory structure: mkdir -p "${c}"
13
+ 3. Check filesystem permissions allow directory creation`,{cause:o})}throw new Error(`Failed to acquire lock: ${e}`,{cause:o})}},{retries:m,baseDelayMs:s,maxDelayMs:i,jitter:!0})}release(e){this.stopTouchTimer(e);try{(0,t.existsSync)(e)&&(0,u.safeDeleteSync)(e,{recursive:!0}),this.activeLocks.delete(e)}catch(r){d.logger.warn(`Failed to release lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}async withLock(e,r,s){const i=await this.acquire(e,s);try{return await r()}finally{i()}}}const $=new x;0&&(module.exports={processLock});
3
14
  //# sourceMappingURL=process-lock.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/process-lock.ts"],
4
- "sourcesContent": ["/**\n * @fileoverview Process locking utilities with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system based locks.\n * Aligned with npm's npx locking strategy (5-second stale timeout, periodic touching).\n */\n\nimport { existsSync, mkdirSync, statSync, utimesSync } from 'node:fs'\n\nimport { safeDeleteSync } from './fs'\nimport { logger } from './logger'\nimport { pRetry } from './promises'\nimport { onExit } from './signal-exit'\n\n/**\n * Lock acquisition options.\n */\nexport interface ProcessLockOptions {\n /**\n * Maximum number of retry attempts.\n * @default 3\n */\n retries?: number | undefined\n\n /**\n * Base delay between retries in milliseconds.\n * @default 100\n */\n baseDelayMs?: number | undefined\n\n /**\n * Maximum delay between retries in milliseconds.\n * @default 1000\n */\n maxDelayMs?: number | undefined\n\n /**\n * Stale lock timeout in milliseconds.\n * Locks older than this are considered abandoned and can be reclaimed.\n * Aligned with npm's npx locking strategy (5 seconds).\n * @default 5000 (5 seconds)\n */\n staleMs?: number | undefined\n\n /**\n * Interval for touching lock file to keep it fresh in milliseconds.\n * Set to 0 to disable periodic touching.\n * @default 2000 (2 seconds)\n */\n touchIntervalMs?: number | undefined\n}\n\n/**\n * Process lock manager with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system\n * based locks.\n */\nclass ProcessLockManager {\n private activeLocks = new Set<string>()\n private touchTimers = new Map<string, NodeJS.Timeout>()\n private exitHandlerRegistered = false\n\n /**\n * Ensure process exit handler is registered for cleanup.\n * Registers a handler that cleans up all active locks when the process exits.\n */\n private ensureExitHandler() {\n if (this.exitHandlerRegistered) {\n return\n }\n\n onExit(() => {\n // Clear all touch timers.\n for (const timer of this.touchTimers.values()) {\n clearInterval(timer)\n }\n this.touchTimers.clear()\n\n // Clean up all active locks.\n for (const lockPath of this.activeLocks) {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n } catch {\n // Ignore cleanup errors during exit.\n }\n }\n })\n\n this.exitHandlerRegistered = true\n }\n\n /**\n * Touch a lock file to update its mtime.\n * This prevents the lock from being detected as stale during long operations.\n *\n * @param lockPath - Path to the lock directory\n */\n private touchLock(lockPath: string): void {\n try {\n if (existsSync(lockPath)) {\n const now = new Date()\n utimesSync(lockPath, now, now)\n }\n } catch (error) {\n logger.warn(\n `Failed to touch lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Start periodic touching of a lock file.\n * Aligned with npm npx strategy to prevent false stale detection.\n *\n * @param lockPath - Path to the lock directory\n * @param intervalMs - Touch interval in milliseconds\n */\n private startTouchTimer(lockPath: string, intervalMs: number): void {\n if (intervalMs <= 0 || this.touchTimers.has(lockPath)) {\n return\n }\n\n const timer = setInterval(() => {\n this.touchLock(lockPath)\n }, intervalMs)\n\n // Prevent timer from keeping process alive.\n timer.unref()\n\n this.touchTimers.set(lockPath, timer)\n }\n\n /**\n * Stop periodic touching of a lock file.\n *\n * @param lockPath - Path to the lock directory\n */\n private stopTouchTimer(lockPath: string): void {\n const timer = this.touchTimers.get(lockPath)\n if (timer) {\n clearInterval(timer)\n this.touchTimers.delete(lockPath)\n }\n }\n\n /**\n * Check if a lock is stale based on mtime.\n * Uses second-level granularity to avoid APFS floating-point precision issues.\n * Aligned with npm's npx locking strategy.\n *\n * @param lockPath - Path to the lock directory\n * @param staleMs - Stale timeout in milliseconds\n * @returns True if lock exists and is stale\n */\n private isStale(lockPath: string, staleMs: number): boolean {\n try {\n if (!existsSync(lockPath)) {\n return false\n }\n\n const stats = statSync(lockPath)\n // Use second-level granularity to avoid APFS issues.\n const ageSeconds = Math.floor((Date.now() - stats.mtime.getTime()) / 1000)\n const staleSeconds = Math.floor(staleMs / 1000)\n return ageSeconds > staleSeconds\n } catch {\n return false\n }\n }\n\n /**\n * Acquire a lock using mkdir for atomic operation.\n * Handles stale locks and includes exit cleanup.\n *\n * This method attempts to create a lock directory atomically. If the lock\n * already exists, it checks if it's stale and removes it before retrying.\n * Uses exponential backoff with jitter for retry attempts.\n *\n * @param lockPath - Path to the lock directory\n * @param options - Lock acquisition options\n * @returns Release function to unlock\n * @throws Error if lock cannot be acquired after all retries\n *\n * @example\n * ```typescript\n * const release = await processLock.acquire('/tmp/my-lock')\n * try {\n * // Critical section\n * } finally {\n * release()\n * }\n * ```\n */\n async acquire(\n lockPath: string,\n options: ProcessLockOptions = {},\n ): Promise<() => void> {\n const {\n baseDelayMs = 100,\n maxDelayMs = 1000,\n retries = 3,\n staleMs = 5000,\n touchIntervalMs = 2000,\n } = options\n\n // Ensure exit handler is registered before any lock acquisition.\n this.ensureExitHandler()\n\n return await pRetry(\n async () => {\n try {\n // Check for stale lock and remove if necessary.\n if (existsSync(lockPath) && this.isStale(lockPath, staleMs)) {\n logger.log(`Removing stale lock: ${lockPath}`)\n try {\n safeDeleteSync(lockPath, { recursive: true })\n } catch {\n // Ignore errors removing stale lock - will retry.\n }\n }\n\n // Atomic lock acquisition via mkdir.\n mkdirSync(lockPath, { recursive: false })\n\n // Track lock for cleanup.\n this.activeLocks.add(lockPath)\n\n // Start periodic touching to prevent stale detection.\n this.startTouchTimer(lockPath, touchIntervalMs)\n\n // Return release function.\n return () => this.release(lockPath)\n } catch (error) {\n // Handle lock contention.\n if (error instanceof Error && (error as any).code === 'EEXIST') {\n if (this.isStale(lockPath, staleMs)) {\n throw new Error(`Stale lock detected: ${lockPath}`)\n }\n throw new Error(`Lock already exists: ${lockPath}`)\n }\n throw error\n }\n },\n {\n retries,\n baseDelayMs,\n maxDelayMs,\n jitter: true,\n },\n )\n }\n\n /**\n * Release a lock and remove from tracking.\n * Stops periodic touching and removes the lock directory.\n *\n * @param lockPath - Path to the lock directory\n *\n * @example\n * ```typescript\n * processLock.release('/tmp/my-lock')\n * ```\n */\n release(lockPath: string): void {\n // Stop periodic touching.\n this.stopTouchTimer(lockPath)\n\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n this.activeLocks.delete(lockPath)\n } catch (error) {\n logger.warn(\n `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Execute a function with exclusive lock protection.\n * Automatically handles lock acquisition, execution, and cleanup.\n *\n * This is the recommended way to use process locks, as it guarantees\n * cleanup even if the callback throws an error.\n *\n * @param lockPath - Path to the lock directory\n * @param fn - Function to execute while holding the lock\n * @param options - Lock acquisition options\n * @returns Result of the callback function\n * @throws Error from callback or lock acquisition failure\n *\n * @example\n * ```typescript\n * const result = await processLock.withLock('/tmp/my-lock', async () => {\n * // Critical section\n * return someValue\n * })\n * ```\n */\n async withLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options?: ProcessLockOptions,\n ): Promise<T> {\n const release = await this.acquire(lockPath, options)\n try {\n return await fn()\n } finally {\n release()\n }\n }\n}\n\n// Export singleton instance.\nexport const processLock = new ProcessLockManager()\n"],
5
- "mappings": "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",
6
- "names": ["process_lock_exports", "__export", "processLock", "__toCommonJS", "import_node_fs", "import_fs", "import_logger", "import_promises", "import_signal_exit", "ProcessLockManager", "timer", "lockPath", "now", "error", "intervalMs", "staleMs", "stats", "ageSeconds", "staleSeconds", "options", "baseDelayMs", "maxDelayMs", "retries", "touchIntervalMs", "fn", "release"]
4
+ "sourcesContent": ["/**\n * @fileoverview Process locking utilities with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using directory-based locks.\n * Aligned with npm's npx locking strategy (5-second stale timeout, periodic touching).\n *\n * ## Why directories instead of files?\n *\n * This implementation uses `mkdir()` to create lock directories (not files) because:\n *\n * 1. **Atomic guarantee**: `mkdir()` is guaranteed atomic across ALL filesystems,\n * including NFS. Only ONE process can successfully create the directory. If it\n * exists, `mkdir()` fails with EEXIST instantly with no race conditions.\n *\n * 2. **File-based locking issues**:\n * - `writeFile()` with `flag: 'wx'` - atomicity can fail on NFS\n * - `open()` with `O_EXCL` - not guaranteed atomic on older NFS\n * - Traditional lockfiles - can have race conditions on network filesystems\n *\n * 3. **Simplicity**: No need to write/read file content, track PIDs, or manage\n * file descriptors. Just create/delete directory and check mtime.\n *\n * 4. **Historical precedent**: Well-known Unix locking pattern used by package\n * managers for decades. Git uses similar approach for `.git/index.lock`.\n *\n * ## The mtime trick\n *\n * We periodically update the lock directory's mtime (modification time) by\n * \"touching\" it to signal \"I'm still actively working\". This prevents other\n * processes from treating the lock as stale and removing it.\n *\n * **The lock directory remains empty** - it's just a sentinel that signals\n * \"locked\". The mtime is the only data needed to track lock freshness.\n *\n * ## npm npx compatibility\n *\n * This implementation matches npm npx's concurrency.lock approach:\n * - Lock created via `mkdir(path.join(installDir, 'concurrency.lock'))`\n * - 5-second stale timeout (if mtime is older than 5s, lock is stale)\n * - 2-second touching interval (updates mtime every 2s to keep lock fresh)\n * - Automatic cleanup on process exit\n */\n\nimport { existsSync, mkdirSync, statSync, utimesSync } from 'node:fs'\n\nimport { safeDeleteSync } from './fs'\nimport { logger } from './logger'\nimport { pRetry } from './promises'\nimport { onExit } from './signal-exit'\n\n/**\n * Lock acquisition options.\n */\nexport interface ProcessLockOptions {\n /**\n * Maximum number of retry attempts.\n * @default 3\n */\n retries?: number | undefined\n\n /**\n * Base delay between retries in milliseconds.\n * @default 100\n */\n baseDelayMs?: number | undefined\n\n /**\n * Maximum delay between retries in milliseconds.\n * @default 1000\n */\n maxDelayMs?: number | undefined\n\n /**\n * Stale lock timeout in milliseconds.\n * Locks older than this are considered abandoned and can be reclaimed.\n * Aligned with npm's npx locking strategy (5 seconds).\n * @default 5000 (5 seconds)\n */\n staleMs?: number | undefined\n\n /**\n * Interval for touching lock file to keep it fresh in milliseconds.\n * Set to 0 to disable periodic touching.\n * @default 2000 (2 seconds)\n */\n touchIntervalMs?: number | undefined\n}\n\n/**\n * Process lock manager with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system\n * based locks.\n */\nclass ProcessLockManager {\n private activeLocks = new Set<string>()\n private touchTimers = new Map<string, NodeJS.Timeout>()\n private exitHandlerRegistered = false\n\n /**\n * Ensure process exit handler is registered for cleanup.\n * Registers a handler that cleans up all active locks when the process exits.\n */\n private ensureExitHandler() {\n if (this.exitHandlerRegistered) {\n return\n }\n\n onExit(() => {\n // Clear all touch timers.\n for (const timer of this.touchTimers.values()) {\n clearInterval(timer)\n }\n this.touchTimers.clear()\n\n // Clean up all active locks.\n for (const lockPath of this.activeLocks) {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n } catch {\n // Ignore cleanup errors during exit.\n }\n }\n })\n\n this.exitHandlerRegistered = true\n }\n\n /**\n * Touch a lock file to update its mtime.\n * This prevents the lock from being detected as stale during long operations.\n *\n * @param lockPath - Path to the lock directory\n */\n private touchLock(lockPath: string): void {\n try {\n if (existsSync(lockPath)) {\n const now = new Date()\n utimesSync(lockPath, now, now)\n }\n } catch (error) {\n logger.warn(\n `Failed to touch lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Start periodic touching of a lock file.\n * Aligned with npm npx strategy to prevent false stale detection.\n *\n * @param lockPath - Path to the lock directory\n * @param intervalMs - Touch interval in milliseconds\n */\n private startTouchTimer(lockPath: string, intervalMs: number): void {\n if (intervalMs <= 0 || this.touchTimers.has(lockPath)) {\n return\n }\n\n const timer = setInterval(() => {\n this.touchLock(lockPath)\n }, intervalMs)\n\n // Prevent timer from keeping process alive.\n timer.unref()\n\n this.touchTimers.set(lockPath, timer)\n }\n\n /**\n * Stop periodic touching of a lock file.\n *\n * @param lockPath - Path to the lock directory\n */\n private stopTouchTimer(lockPath: string): void {\n const timer = this.touchTimers.get(lockPath)\n if (timer) {\n clearInterval(timer)\n this.touchTimers.delete(lockPath)\n }\n }\n\n /**\n * Check if a lock is stale based on mtime.\n * Uses second-level granularity to avoid APFS floating-point precision issues.\n * Aligned with npm's npx locking strategy.\n *\n * @param lockPath - Path to the lock directory\n * @param staleMs - Stale timeout in milliseconds\n * @returns True if lock exists and is stale\n */\n private isStale(lockPath: string, staleMs: number): boolean {\n try {\n if (!existsSync(lockPath)) {\n return false\n }\n\n const stats = statSync(lockPath)\n // Use second-level granularity to avoid APFS issues.\n const ageSeconds = Math.floor((Date.now() - stats.mtime.getTime()) / 1000)\n const staleSeconds = Math.floor(staleMs / 1000)\n return ageSeconds > staleSeconds\n } catch {\n return false\n }\n }\n\n /**\n * Acquire a lock using mkdir for atomic operation.\n * Handles stale locks and includes exit cleanup.\n *\n * This method attempts to create a lock directory atomically. If the lock\n * already exists, it checks if it's stale and removes it before retrying.\n * Uses exponential backoff with jitter for retry attempts.\n *\n * @param lockPath - Path to the lock directory\n * @param options - Lock acquisition options\n * @returns Release function to unlock\n * @throws Error if lock cannot be acquired after all retries\n *\n * @example\n * ```typescript\n * const release = await processLock.acquire('/tmp/my-lock')\n * try {\n * // Critical section\n * } finally {\n * release()\n * }\n * ```\n */\n async acquire(\n lockPath: string,\n options: ProcessLockOptions = {},\n ): Promise<() => void> {\n const {\n baseDelayMs = 100,\n maxDelayMs = 1000,\n retries = 3,\n staleMs = 5000,\n touchIntervalMs = 2000,\n } = options\n\n // Ensure exit handler is registered before any lock acquisition.\n this.ensureExitHandler()\n\n return await pRetry(\n async () => {\n try {\n // Check for stale lock and remove if necessary.\n if (existsSync(lockPath) && this.isStale(lockPath, staleMs)) {\n logger.log(`Removing stale lock: ${lockPath}`)\n try {\n safeDeleteSync(lockPath, { recursive: true })\n } catch {\n // Ignore errors removing stale lock - will retry.\n }\n }\n\n // Atomic lock acquisition via mkdir.\n mkdirSync(lockPath, { recursive: false })\n\n // Track lock for cleanup.\n this.activeLocks.add(lockPath)\n\n // Start periodic touching to prevent stale detection.\n this.startTouchTimer(lockPath, touchIntervalMs)\n\n // Return release function.\n return () => this.release(lockPath)\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n\n // Handle lock contention - lock already exists.\n if (code === 'EEXIST') {\n if (this.isStale(lockPath, staleMs)) {\n throw new Error(`Stale lock detected: ${lockPath}`)\n }\n throw new Error(`Lock already exists: ${lockPath}`)\n }\n\n // Handle permission errors - not retryable.\n if (code === 'EACCES' || code === 'EPERM') {\n throw new Error(\n `Permission denied creating lock: ${lockPath}. ` +\n 'Check directory permissions or run with appropriate access.',\n { cause: error },\n )\n }\n\n // Handle read-only filesystem - not retryable.\n if (code === 'EROFS') {\n throw new Error(\n `Cannot create lock on read-only filesystem: ${lockPath}`,\n { cause: error },\n )\n }\n\n // Handle parent path issues - not retryable.\n if (code === 'ENOTDIR') {\n const parentDir = lockPath.slice(0, lockPath.lastIndexOf('/'))\n throw new Error(\n `Cannot create lock directory: ${lockPath}\\n` +\n 'A path component is a file when it should be a directory.\\n' +\n `Parent path: ${parentDir}\\n` +\n 'To resolve:\\n' +\n ` 1. Check if \"${parentDir}\" contains a file instead of a directory\\n` +\n ' 2. Remove any conflicting files in the path\\n' +\n ' 3. Ensure the full parent directory structure exists',\n { cause: error },\n )\n }\n\n if (code === 'ENOENT') {\n const parentDir = lockPath.slice(0, lockPath.lastIndexOf('/'))\n throw new Error(\n `Cannot create lock directory: ${lockPath}\\n` +\n `Parent directory does not exist: ${parentDir}\\n` +\n 'To resolve:\\n' +\n ` 1. Ensure the parent directory \"${parentDir}\" exists\\n` +\n ` 2. Create the directory structure: mkdir -p \"${parentDir}\"\\n` +\n ' 3. Check filesystem permissions allow directory creation',\n { cause: error },\n )\n }\n\n // Re-throw other errors with context.\n throw new Error(`Failed to acquire lock: ${lockPath}`, {\n cause: error,\n })\n }\n },\n {\n retries,\n baseDelayMs,\n maxDelayMs,\n jitter: true,\n },\n )\n }\n\n /**\n * Release a lock and remove from tracking.\n * Stops periodic touching and removes the lock directory.\n *\n * @param lockPath - Path to the lock directory\n *\n * @example\n * ```typescript\n * processLock.release('/tmp/my-lock')\n * ```\n */\n release(lockPath: string): void {\n // Stop periodic touching.\n this.stopTouchTimer(lockPath)\n\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n this.activeLocks.delete(lockPath)\n } catch (error) {\n logger.warn(\n `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Execute a function with exclusive lock protection.\n * Automatically handles lock acquisition, execution, and cleanup.\n *\n * This is the recommended way to use process locks, as it guarantees\n * cleanup even if the callback throws an error.\n *\n * @param lockPath - Path to the lock directory\n * @param fn - Function to execute while holding the lock\n * @param options - Lock acquisition options\n * @returns Result of the callback function\n * @throws Error from callback or lock acquisition failure\n *\n * @example\n * ```typescript\n * const result = await processLock.withLock('/tmp/my-lock', async () => {\n * // Critical section\n * return someValue\n * })\n * ```\n */\n async withLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options?: ProcessLockOptions,\n ): Promise<T> {\n const release = await this.acquire(lockPath, options)\n try {\n return await fn()\n } finally {\n release()\n }\n }\n}\n\n// Export singleton instance.\nexport const processLock = new ProcessLockManager()\n"],
5
+ "mappings": "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",
6
+ "names": ["process_lock_exports", "__export", "processLock", "__toCommonJS", "import_node_fs", "import_fs", "import_logger", "import_promises", "import_signal_exit", "ProcessLockManager", "timer", "lockPath", "now", "error", "intervalMs", "staleMs", "stats", "ageSeconds", "staleSeconds", "options", "baseDelayMs", "maxDelayMs", "retries", "touchIntervalMs", "code", "parentDir", "fn", "release"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "2.8.4",
3
+ "version": "2.9.0",
4
4
  "license": "MIT",
5
5
  "description": "Core utilities and infrastructure for Socket.dev security tools",
6
6
  "keywords": [