@socketsecurity/lib 5.20.1 → 5.21.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.
package/CHANGELOG.md CHANGED
@@ -5,92 +5,100 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [5.20.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.20.1) - 2026-04-19
8
+ ## [5.21.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.21.0) - 2026-04-20
9
9
 
10
- ### Fixed
10
+ ### Added
11
11
 
12
- - `src/ipc.ts`: harden stub-file writes against local symlink/TOCTOU. Previously `writeIpcStub` used `mkdir {recursive, mode: 0o700}` + `writeFile`, which on multi-user Linux (`/tmp` sticky-bit but world-writable) let a pre-positioned attacker-owned `.socket-ipc/<app>/` survive the mode argument and redirect the subsequent `writeFile` through symlinks to victim files. Now validates directory ownership + mode on POSIX after `mkdir`, then opens the stub with `O_CREAT | O_WRONLY | O_EXCL | O_NOFOLLOW` so pre-existing inodes trigger EEXIST and final-component symlinks trigger ELOOP rather than silent file overwrite
13
- - `src/cache-with-ttl.ts` `getOrFetch()` the inflight-map check ran _after_ `await get(key)`, so two concurrent cold-cache callers both suspended on the same disk read, both saw no cached value, both skipped the inflight check, and both fired `fetcher()`. Moves the inflight check before the persistent-cache lookup (with a re-check afterward) so the advertised dedupe guarantee actually holds
14
- - `src/cache-with-ttl.ts` — cap the in-memory `memoCache` with LRU eviction (`memoMaxSize`, default 1000). Previously a long-running process (devserver, editor extension) querying many distinct keys grew memory without bound — expired entries were only reclaimed when the same key was read again
15
- - `src/memoization.ts` `memoizeAsync()` — `entry.timestamp` was set when a cache miss STARTED its `fn(...)` call, so a fn taking longer than `ttl` produced a value classified as expired the moment it resolved; every subsequent caller past the first ttl window re-fetched instead of hitting the cache. Now refreshes the timestamp in the resolve handler. Also bumps `accessOrder` on the stale-dedup branch so an entry mid-refresh isn't evicted while a peer is computing on its behalf
16
- - `src/tables.ts` — `displayWidth` measured columns by `.length` of the ANSI-stripped string, i.e. UTF-16 code units rather than rendered terminal cells. CJK, emoji, and combined code points misaligned tables. Routes measurement through `stringWidth` (Intl.Segmenter + East Asian Width)
17
- - `src/paths/packages.ts` — `resolvePackageJsonDirname` / `resolvePackageJsonPath` gated on `filepath.endsWith('package.json')`, which misidentified any file whose name ended in that suffix (e.g. `/foo/my-package.json`) as a manifest. Now checks for the literal final segment
18
- - `src/json/edit.ts` — `@example` for `getEditableJsonClass` imported from `@socketsecurity/lib/json`, which is not a package export; fixed to `@socketsecurity/lib/json/edit`
12
+ - `@socketsecurity/lib/schema/validate` non-throwing Zod/TypeBox validator returning `{ ok, value } | { ok, errors }` with normalized paths
13
+ - `@socketsecurity/lib/schema/parse` — throwing variant for fail-fast trust boundaries
14
+ - `@socketsecurity/lib/schema/types` — `Schema<T>`, `ValidateResult<T>`, `ValidationIssue`, `AnySchema`, `Infer<S>`
15
+ - `@socketsecurity/lib/promises` `withResolvers()` — spec-compliant [`Promise.withResolvers`](https://tc39.es/ecma262/#sec-promise.withResolvers) helper with `PromiseWithResolvers<T>` type. Uses the native implementation when available
19
16
 
20
- ## [5.20.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.20.0) - 2026-04-19
17
+ ### Changed
21
18
 
22
- ### Addedvalidation
19
+ - `@socketsecurity/lib/regexps` `escapeRegExp()` now spec-compliant with TC39 [`RegExp.escape`](https://tc39.es/ecma262/#sec-regexp.escape); uses the native implementation when available. **Caller-visible shape change**: escaped output now uses `\xHH` for many characters that previously passed through literally (e.g. `escapeRegExp('a')` is now `'\x61'`). Functional equivalence (the compiled regex matches the original input) is preserved; only callers that string-match on escape output need updates
20
+ - `@socketsecurity/lib/memoization` `MemoizeOptions<Args>` — dropped the unused second type parameter. Consumers who wrote `MemoizeOptions<Args, Result>` must drop the second argument
21
+ - `@socketsecurity/lib/packages/specs` `getRepoUrlDetails()` — now accepts `git+https://` / `git+ssh://` GitHub URLs and rejects lookalike hosts (`githubXcom`, `fake-github.com.attacker.tld`). scp-style `git@github.com:…` URLs (no `://`) now return `{ user: '', project: '' }` — callers must normalize to https/ssh upstream
22
+ - `@socketsecurity/lib/url` `urlSearchParamAsBoolean()` — accepts the same truthy vocabulary as `envAsBoolean` (`1` / `true` / `yes` / `on`, case-insensitive). Empty-string input now falls through to `defaultValue` instead of returning `false`
23
23
 
24
- - `@socketsecurity/lib/validation/validate-schema` — universal validator that accepts any Zod-style schema (Zod v3/v4, or any `safeParse`-shaped duck type) and returns a tagged `{ ok: true, value } | { ok: false, errors }` result with normalized `{ path, message }` issues. Type inference flows through: callers get `z.infer<…>`, no casts. Zod is detected purely structurally via `.safeParse` — no runtime import of the `zod` package required
25
- - `parseSchema(schema, data)` — throwing twin of `validateSchema` for fail-fast trust-boundary validation
26
- - `Infer<S>`, `ValidateResult<T>`, `ValidationIssue`, `AnySchema` supporting types exported alongside the helpers
24
+ ### Removed
25
+
26
+ - `@socketsecurity/lib/validation/*` subpath retiredexports re-homed:
27
+ - `validateSchema` / `parseSchema` → `@socketsecurity/lib/schema/validate` / `@socketsecurity/lib/schema/parse`
28
+ - `safeJsonParse` → `@socketsecurity/lib/json/parse`
29
+ - Types → `@socketsecurity/lib/schema/types` and `@socketsecurity/lib/json/types`
30
+ - `memoizeDebounced` from `@socketsecurity/lib/memoization` — was misnamed and had no consumers. Use `memoize` / `memoizeAsync` with a `ttl` instead
27
31
 
28
32
  ### Fixed
29
33
 
30
- - `src/promise-queue.ts`: wrap `task.fn()` invocation via `Promise.resolve().then()` so a **synchronous** throw inside a queued task converts to a proper rejection on `task.reject` instead of escaping as an uncaught exception
31
- - `src/stdio/progress.ts` `formatTime()`: clamp negative milliseconds so an over-ticking or clock-skewed progress bar no longer renders a negative ETA like `-1m59s`
32
- - `src/dlx/lockfile.ts`: wrap the scratch-directory cleanup in `finally` with its own `try/catch` so a cleanup failure cannot clobber the real exception from the main try-block
33
- - `src/dlx/package.ts` `parsePackageSpec`: normalize a bare trailing `@` (e.g. `"pkg@"`) to `version: undefined` so downstream "no version provided" checks behave consistently
34
- - `src/stdio/prompts.ts`: tighten the `selectModule` destructure type to the two properties actually used (`default`, `Separator`) instead of an `as any` cast
35
- - `src/http-request.ts`: hoist `CHECKSUM_BSD_RE` and `CHECKSUM_GNU_RE` regex literals to module scope so `parseChecksums()` no longer re-declares them once per line inside its loop
36
- - `src/dlx/manifest.ts`: correct the `@fileoverview` "Primary API" list to match the actual `DlxManifest` methods (`get/set/clear/clearAll/isFresh/getManifestEntry`) and flag `setPackageEntry` / `setBinaryEntry` as deprecated
34
+ - `@socketsecurity/lib/versions` `maxVersion()` / `minVersion()` return the latest/earliest prerelease for all-prerelease inputs (previously returned `undefined`)
35
+ - `@socketsecurity/lib/fs` `findUp()` / `findUpSync()` traverse up to and **including** the filesystem root (previously missed matches at `/.foo`)
36
+ - `@socketsecurity/lib/words` `capitalize()` safe for non-BMP characters (emoji, astral-plane scripts); previously produced broken surrogate pairs
37
+ - `@socketsecurity/lib/words` `determineArticle()` case-insensitive vowel match (`Apple` `an Apple`)
38
+ - `@socketsecurity/lib/archives` `extractZip()` / `extractTar()` / `extractTarGz()` missing-archive errors now uniformly surface as `ENOENT` with `code` / `path` / message (previously `extractZip` surfaced adm-zip's generic `"Invalid filename"`)
39
+ - `@socketsecurity/lib/promise-queue` bounded queue now rejects the newest submission when full, preserving in-flight work
40
+ - `@socketsecurity/lib/cacache` / `@socketsecurity/lib/cache-with-ttl` wildcard key deletion anchors both ends of the pattern (`deleteAll('foo*bar')` no longer sweeps `foo123bar-extra`)
41
+ - `@socketsecurity/lib/process-lock` — sub-second `staleMs` values now honored at full precision; TOCTOU window on lock acquisition closed
42
+ - `@socketsecurity/lib/suppress-warnings` `withSuppressedWarnings()` — no longer wipes concurrent suppressions on exit
43
+ - Unbounded LRU caches in `@socketsecurity/lib/dlx` capped (binary path, package.json path); negative package.json lookups now expire after 10s
44
+ - Glob cache keys for array-valued options (e.g. `ignore`) are order-insensitive
37
45
 
38
- ## [5.19.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.19.1) - 2026-04-19
46
+ ### Performance
39
47
 
40
- ### Fixedstdio (restore accidentally-dropped modules)
48
+ - `@socketsecurity/lib/memoization``memoize()` / `memoizeAsync()` cache-hit bookkeeping dropped from O(n) to O(1). Noticeable on caches with many entries
49
+ - `@socketsecurity/lib/cacache` — wildcard `clear()` no longer recompiles the match regex per streamed entry
41
50
 
42
- 5.19.0 shipped a breaking change that was not called out in its changelog or version bump: a refactor commit removed `stdio/prompts`, `stdio/progress`, `stdio/clear`, and the vendored `external/@inquirer/*` shims. socket-cli and other consumers import `stdio/prompts` directly and broke on upgrade.
51
+ ## [5.20.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.20.1) - 2026-04-19
43
52
 
44
- Restored:
53
+ ### Fixed
45
54
 
46
- - `@socketsecurity/lib/stdio/prompts` — inquirer wrappers (`password`, `confirm`, `input`, `select`, `checkbox`, `search`)
47
- - `@socketsecurity/lib/stdio/progress` — progress-bar utility (`ProgressBar` class)
48
- - `@socketsecurity/lib/stdio/clear` — terminal line/screen/cursor helpers
49
- - `src/external/@inquirer/{checkbox,confirm,input,password,search,select}.js` vendor shims
50
- - Corresponding test suites
55
+ - `@socketsecurity/lib/ipc` — harden stub-file writes against symlink/TOCTOU attacks on shared-tmp filesystems (POSIX ownership + mode validation, `O_EXCL | O_NOFOLLOW` open)
56
+ - `@socketsecurity/lib/cache-with-ttl` `getOrFetch()` close concurrent-caller race that let two cold-cache awaits both skip the inflight-dedupe check and fire the fetcher twice
57
+ - `@socketsecurity/lib/cache-with-ttl` — cap the in-memory memo layer with LRU eviction (`memoMaxSize`, default 1000); long-running processes no longer grow unbounded
58
+ - `@socketsecurity/lib/memoization` `memoizeAsync()` — refresh cache entry timestamp on resolve so slow fetches (longer than `ttl`) aren't classified as expired the moment they land
59
+ - `@socketsecurity/lib/tables` `displayWidth` now measures rendered terminal cells (via `stringWidth`) instead of UTF-16 code units; CJK / emoji / combining marks align correctly
60
+ - `@socketsecurity/lib/paths/packages` — `resolvePackageJsonDirname` / `resolvePackageJsonPath` no longer mis-identify files like `/foo/my-package.json` as package manifests
61
+ - `@socketsecurity/lib/json/edit` — `@example` import path corrected
51
62
 
52
- ## [5.19.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.19.0) - 2026-04-19
63
+ ## [5.20.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.20.0) - 2026-04-19
53
64
 
54
- ### Added — dlx/integrity (new module)
65
+ ### Added
55
66
 
56
- - `HashSpec`, `NormalizedHash`, `ComputedHashes` types. `HashSpec` accepts a bare string (sha512 SRI or sha256 hex, sniffed) or an explicit `{ type, value }` object
57
- - `normalizeHash()`, `computeHashes()`, `verifyHash()` — `verifyHash` uses `crypto.timingSafeEqual` for constant-time comparison
58
- - `DlxHashMismatchError` — carries `expected` + `actual` for diagnostics
67
+ - `@socketsecurity/lib/validation/validate-schema` universal Zod-style schema validator with `validateSchema` (tagged result) and `parseSchema` (throwing); `Infer<S>`, `ValidateResult<T>`, `ValidationIssue`, `AnySchema` types. No runtime `zod` dependency
59
68
 
60
- ### Added dlx/arborist (new module)
69
+ > **Deprecated in 5.21.0**: moved to `@socketsecurity/lib/schema/*`.
70
+
71
+ ### Fixed
61
72
 
62
- - `safeIdealTree()`, `safeReify()` — hardened `@npmcli/arborist` wrappers mirroring socket-cli v1.1.79 `SafeArborist` overrides (`audit: false`, `fund: false`, `ignoreScripts: true`, `progress: false`, `saveBundle: false`, `silent: true`)
63
- - `writeSafeNpmrc()` — defense-in-depth `.npmrc` writer matching the Arborist overrides
64
- - Optional `before?: Date` on `safeIdealTree` for release-age enforcement during resolution
73
+ - `@socketsecurity/lib/promise-queue` synchronous throws inside a queued task now convert to proper rejections instead of escaping as uncaught exceptions
74
+ - `@socketsecurity/lib/stdio/progress` `formatTime()` — clamp negative milliseconds so over-ticking / clock-skewed bars don't render negative ETAs
75
+ - `@socketsecurity/lib/dlx/lockfile` scratch-directory cleanup can no longer clobber the real exception from the main block
76
+ - `@socketsecurity/lib/dlx/package` `parsePackageSpec` — normalize a bare trailing `@` (e.g. `"pkg@"`) to `version: undefined`
77
+ - `@socketsecurity/lib/stdio/prompts` — tighten an internal destructure type away from `as any`
78
+ - `@socketsecurity/lib/http-request` — hoist checksum regex literals out of a per-line loop
65
79
 
66
- ### Added — dlx/lockfile (new module)
80
+ ## [5.19.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.19.1) - 2026-04-19
67
81
 
68
- - `generatePackagePin({ package, minReleaseDays?, minReleaseMins? })` — returns `PinDetails { name, version, hash: ComputedHashes, packageJson, lockfile }`. Runs Arborist in `packageLockOnly: true` mode against a tmp directory and auto-cleans
69
- - **Default `minReleaseDays: 7`** — resolution refuses to select versions published in the last week. Pass `0` to disable. `minReleaseMins` is a pnpm-style alias (mutually exclusive with `minReleaseDays`)
70
- - `LockfileSpec` type — export for use as the new `lockfile` option on `downloadPackage`
82
+ ### Fixed
71
83
 
72
- ### Addeddlx existing modules
84
+ Restore `@socketsecurity/lib/stdio/prompts`, `@socketsecurity/lib/stdio/progress`, and `@socketsecurity/lib/stdio/clear` accidentally removed in 5.19.0 without a major-bump callout. Downstream consumers that import `stdio/prompts` directly are unbroken.
73
85
 
74
- - `DlxPackageOptions.hash?: HashSpec` and `DlxPackageOptions.lockfile?: LockfileSpec` — passing a lockfile materializes it into the install dir (path → `fs.copyFileSync`, content → `fs.writeFileSync`) and drops a hardened `.npmrc` alongside before Arborist runs
75
- - `DlxBinaryOptions.hash?: HashSpec` — ergonomic alternative to the lower-level `integrity` and `sha256` fields (both still accepted)
86
+ ## [5.19.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.19.0) - 2026-04-19
76
87
 
77
- ### Fixed — external
88
+ ### Added
78
89
 
79
- - `pacote` shim now exposes `tarball`, `manifest`, `packument` alongside `extract`. **Fixes a latent runtime crash** in `src/packages/manifest.ts` callers (`fetchPackageManifest` / `fetchPackagePackument` called `.manifest(...)` / `.packument(...)` on a shim that previously only had `extract`, raising `TypeError: not a function`)
90
+ - `@socketsecurity/lib/dlx/integrity` hash verification utilities: `HashSpec`, `NormalizedHash`, `ComputedHashes`, `normalizeHash()`, `computeHashes()`, `verifyHash()` (constant-time via `crypto.timingSafeEqual`), `DlxHashMismatchError`
91
+ - `@socketsecurity/lib/dlx/arborist` — hardened `@npmcli/arborist` wrappers: `safeIdealTree()`, `safeReify()`, `writeSafeNpmrc()`. Locks down `audit`, `fund`, `ignoreScripts`, `saveBundle`, etc. Supports `before?: Date` for release-age enforcement
92
+ - `@socketsecurity/lib/dlx/lockfile` — `generatePackagePin()` returns `{ name, version, hash, packageJson, lockfile }` for a resolved package. Default `minReleaseDays: 7` refuses versions published in the last week (`0` to disable); `minReleaseMins` accepted as pnpm-style alias
93
+ - `DlxPackageOptions.hash`, `DlxPackageOptions.lockfile`, `DlxBinaryOptions.hash` — first-class integrity + lockfile options on the dlx entry points
80
94
 
81
- ### Changed — build (bundle size)
95
+ ### Fixed
82
96
 
83
- - `dist/external/npm-pack.js`: 2,526,598 1,755,460 bytes (−771 KB, −30.5%). New `STUB_MAP` entries for code paths our callers never reach:
84
- - `@sigstore/{bundle,core,protobuf-specs,sign,tuf,verify}`, `sigstore`, `tuf-js`, `@tufjs/{canonical-json,models}` — Sigstore attestation, only reached via `arb.audit()`
85
- - `@npmcli/metavuln-calculator` — audit-only
86
- - `@npmcli/query`, `postcss-selector-parser` — `arb.query()` unused
87
- - `@npmcli/run-script`, `@npmcli/node-gyp` guarded out by `ignoreScripts: true`
88
- - `@npmcli/git`, `pacote/lib/{git,file,dir,remote}.js` — registry specs only
89
- - arborist `audit-report.js`, `yarn-lock.js`, `isolated-reifier.js`, `query-selector-all.js`, `printable.js` — each gated or unused
90
- - `cacache/lib/verify.js` — `cacache.verify` (npm cache verify) unused
91
- - `proggy` — progress tracker, gated by `progress: false`
92
- - `debug/src/browser.js` — Node-only bundle
93
- - `dist/external/zod.js`: 597,238 → 291,430 bytes (−306 KB, −51.2%). Stubbed `zod/v4/{core,classic,mini}`'s eager `locales/index.cjs` barrel (40+ translation modules). Opt-in via `z.config(z.locales.xx())` is never called by us
97
+ - `pacote` shim exposes `tarball`, `manifest`, `packument` alongside `extract`. Fixes a latent runtime crash in `fetchPackageManifest` / `fetchPackagePackument` callers
98
+
99
+ ### Changed
100
+
101
+ Reduced bundle size of `dist/external/npm-pack.js` (−771 KB, −30.5%) and `dist/external/zod.js` (−306 KB, −51.2%) by stubbing code paths our callers never reach (Sigstore attestation, arborist audit/query, zod locale translations, etc.)
94
102
 
95
103
  ## [5.18.2](https://github.com/SocketDev/socket-lib/releases/tag/v5.18.2) - 2026-04-14
96
104
 
@@ -100,70 +108,63 @@ Restored:
100
108
 
101
109
  ## [5.18.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.18.1) - 2026-04-14
102
110
 
103
- ### Changed — build
111
+ ### Changed
104
112
 
105
- - Dedup npm-pack.js bundle via pnpm overrides: pacote 21.5.0, make-fetch-happen 15.0.5, and 7 transitive npm packages (npm-bundled, npm-normalize-package-bin, json-parse-even-better-errors, @npmcli/installed-package-contents, @npmcli/name-from-folder, @npmcli/promise-spawn, @npmcli/redact)
106
- - npm-pack.js: 69,738 → 66,443 lines (2.59MB → 2.46MB), 22 duplicate packages removed
113
+ - Deduplicated the `dist/external/npm-pack` bundle via `pnpm overrides` (pacote 21.5.0, make-fetch-happen 15.0.5, and 7 transitive `@npmcli/*` packages) 22 duplicate packages removed, ~130 KB smaller
107
114
 
108
115
  ## [5.18.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.18.0) - 2026-04-14
109
116
 
110
- ### Added — dlx
117
+ ### Added
111
118
 
112
- - Socket Firewall API check before package downloads resolves dependency tree via `buildIdealTree`, checks all packages against `firewall-api.socket.dev/purl` in parallel, blocks on critical/high severity alerts
119
+ - `@socketsecurity/lib/dlx` — Socket Firewall API check before package downloads. Resolves the dependency tree and blocks on critical/high severity alerts
113
120
 
114
- ### Changed — http-request
121
+ ### Changed
115
122
 
116
- - Default `User-Agent` header updated from `socket-registry/1.0` to `socketsecurity-lib/{version}`
123
+ - `@socketsecurity/lib/http-request` — default `User-Agent` updated from `socket-registry/1.0` to `socketsecurity-lib/{version}`
117
124
 
118
125
  ## [5.17.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.17.0) - 2026-04-14
119
126
 
120
- ### Added — paths
127
+ ### Added
121
128
 
122
- - `isUnixPath()` — detect MSYS/Git Bash drive letter notation (`/c/...`)
129
+ - `@socketsecurity/lib/paths` `isUnixPath()` — detect MSYS/Git Bash drive-letter notation (`/c/...`)
123
130
 
124
- ### Changed — paths
131
+ ### Changed
125
132
 
126
- - `normalizePath()` now converts MSYS drive letters on Windows (`/c/path` → `C:/path`)
127
- - `fromUnixPath()` now produces native Windows paths with backslashes (`/c/path` → `C:\path`), making it the true inverse of `toUnixPath()`
133
+ - `@socketsecurity/lib/paths` `normalizePath()` converts MSYS drive letters on Windows (`/c/path` → `C:/path`)
134
+ - `@socketsecurity/lib/paths` `fromUnixPath()` produces native Windows paths with backslashes (`/c/path` → `C:\path`), making it the true inverse of `toUnixPath()`
128
135
 
129
136
  ## [5.16.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.16.0) - 2026-04-14
130
137
 
131
- ### Added — paths
138
+ ### Added
132
139
 
133
- - `fromUnixPath()` — convert MSYS/Git Bash Unix-style paths (`/c/path`) back to native Windows format (`C:/path`), inverse of `toUnixPath` (#168)
140
+ - `@socketsecurity/lib/paths` `fromUnixPath()` — convert MSYS/Git Bash Unix-style paths (`/c/path`) back to native Windows format (`C:/path`), inverse of `toUnixPath` (#168)
134
141
 
135
- ### Fixed — dlx
142
+ ### Fixed
136
143
 
137
- - Normalize dlx directory path in `isInSocketDlx` for Windows compatibility
144
+ - `@socketsecurity/lib/dlx` `isInSocketDlx` — normalize the dlx directory path for Windows compatibility
138
145
 
139
146
  ## [5.15.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.15.0) - 2026-04-06
140
147
 
141
- ### Added — http-request
142
-
143
- - `stream` option on `HttpRequestOptions` — resolves with `HttpResponse` immediately after headers arrive, leaving `rawResponse` unconsumed for piping to files
144
- - `headers`, `ok`, `status`, `statusText` fields on `HttpDownloadResult`
145
-
146
- ### Changed — http-request
148
+ ### Added
147
149
 
148
- - `httpDownload` now uses `httpRequest` with `stream: true` internally, eliminating ~120 lines of duplicated HTTP plumbing
150
+ - `@socketsecurity/lib/http-request``stream` option on `HttpRequestOptions` resolves with `HttpResponse` immediately after headers arrive, leaving `rawResponse` unconsumed for piping to files
151
+ - `@socketsecurity/lib/http-request` — `headers`, `ok`, `status`, `statusText` fields on `HttpDownloadResult`
149
152
 
150
153
  ## [5.14.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.14.0) - 2026-04-06
151
154
 
152
- ### Added — http-request
155
+ ### Added
153
156
 
154
- - `HttpResponseError` class — thrown on non-2xx when `throwOnError` is enabled, carries the full `HttpResponse`
155
- - `throwOnError` option on `HttpRequestOptions` non-2xx responses throw instead of resolving with `ok: false`, enabling retry of HTTP errors
156
- - `onRetry` callback on `HttpRequestOptions` customize retry behavior per-attempt (return `false` to stop, a `number` to override delay, `undefined` for default backoff)
157
- - Streaming body support `body` accepts `Readable` streams (incl. `form-data` npm package), auto-merges `getHeaders()` when present
158
- - `parseRetryAfterHeader()` standalone RFC 7231 §7.1.3 `Retry-After` header parser (strict integer seconds + HTTP-date formats)
159
- - `sanitizeHeaders()` — redact sensitive headers (`authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `proxy-authenticate`, `www-authenticate`) for safe logging
157
+ - `@socketsecurity/lib/http-request`:
158
+ - `HttpResponseError` class thrown on non-2xx when `throwOnError` is enabled; carries the full `HttpResponse`
159
+ - `throwOnError` optionnon-2xx responses throw instead of resolving with `ok: false`
160
+ - `onRetry` callbackcustomize retry behavior per-attempt (`false` to stop, a `number` to override delay, `undefined` for default backoff)
161
+ - Streaming body support — `body` accepts `Readable` streams (incl. `form-data`), auto-merges `getHeaders()` when present
162
+ - `parseRetryAfterHeader()` — standalone RFC 7231 §7.1.3 parser
163
+ - `sanitizeHeaders()` — redact sensitive headers for safe logging
160
164
 
161
- ### Changed — http-request
165
+ ### Changed
162
166
 
163
- - `HttpRequestOptions.body` type widened from `Buffer | string` to `Buffer | Readable | string`
164
- - Redirect responses now drained via `res.resume()` to free sockets
165
- - `maxResponseSize` exceeded now cleans up both response and request
166
- - `onResponse` hooks wrapped in try/catch — user hook errors can no longer leave promises pending
167
+ - `@socketsecurity/lib/http-request` — `HttpRequestOptions.body` widened to `Buffer | Readable | string`; `onResponse` hook errors no longer leave promises pending
167
168
 
168
169
  ## [5.13.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.13.0) - 2026-04-05
169
170
 
package/dist/archives.js CHANGED
@@ -80,6 +80,16 @@ function validatePathWithinBase(targetPath, baseDir, entryName) {
80
80
  );
81
81
  }
82
82
  }
83
+ function assertArchiveExists(archivePath) {
84
+ if (!(0, import_node_fs.existsSync)(archivePath)) {
85
+ const err = new Error(
86
+ `ENOENT: no such file or directory, open '${archivePath}'`
87
+ );
88
+ err.code = "ENOENT";
89
+ err.path = archivePath;
90
+ throw err;
91
+ }
92
+ }
83
93
  function detectArchiveFormat(filePath) {
84
94
  const lower = filePath.toLowerCase();
85
95
  if (lower.endsWith(".tar.gz")) {
@@ -116,6 +126,7 @@ async function extractArchive(archivePath, outputDir, options = {}) {
116
126
  }
117
127
  }
118
128
  async function extractTar(archivePath, outputDir, options = {}) {
129
+ assertArchiveExists(archivePath);
119
130
  const {
120
131
  maxEntries = DEFAULT_MAX_ENTRIES,
121
132
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
@@ -207,6 +218,7 @@ async function extractTar(archivePath, outputDir, options = {}) {
207
218
  }
208
219
  }
209
220
  async function extractTarGz(archivePath, outputDir, options = {}) {
221
+ assertArchiveExists(archivePath);
210
222
  const {
211
223
  maxEntries = DEFAULT_MAX_ENTRIES,
212
224
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
@@ -298,6 +310,7 @@ async function extractTarGz(archivePath, outputDir, options = {}) {
298
310
  }
299
311
  }
300
312
  async function extractZip(archivePath, outputDir, options = {}) {
313
+ assertArchiveExists(archivePath);
301
314
  const {
302
315
  maxEntries = DEFAULT_MAX_ENTRIES,
303
316
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
package/dist/cacache.js CHANGED
@@ -41,17 +41,14 @@ __export(cacache_exports, {
41
41
  module.exports = __toCommonJS(cacache_exports);
42
42
  var import_cacache = __toESM(require("./external/cacache"));
43
43
  var import_socket = require("./paths/socket");
44
- function matchesPattern(key, pattern) {
44
+ function createPatternMatcher(pattern) {
45
45
  if (!pattern.includes("*")) {
46
- return key.startsWith(pattern);
46
+ return (key) => key.startsWith(pattern);
47
47
  }
48
- const regex = patternToRegex(pattern);
49
- return regex.test(key);
50
- }
51
- function patternToRegex(pattern) {
52
48
  const escaped = pattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&");
53
49
  const regexPattern = escaped.replaceAll("*", ".*");
54
- return new RegExp(`^${regexPattern}`);
50
+ const regex = new RegExp(`^${regexPattern}$`);
51
+ return (key) => regex.test(key);
55
52
  }
56
53
  async function clear(options) {
57
54
  const opts = { __proto__: null, ...options };
@@ -84,9 +81,10 @@ async function clear(options) {
84
81
  return removed2;
85
82
  }
86
83
  let removed = 0;
84
+ const matches = createPatternMatcher(opts.prefix);
87
85
  const stream = cacache2.ls.stream(cacheDir);
88
86
  for await (const entry of stream) {
89
- if (matchesPattern(entry.key, opts.prefix)) {
87
+ if (matches(entry.key)) {
90
88
  try {
91
89
  await cacache2.rm.entry(cacheDir, entry.key);
92
90
  removed++;
@@ -84,7 +84,7 @@ function createTtlCache(options) {
84
84
  }
85
85
  const escaped = fullPattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&");
86
86
  const regexPattern = escaped.replaceAll("*", ".*");
87
- const regex = new RegExp(`^${regexPattern}`);
87
+ const regex = new RegExp(`^${regexPattern}$`);
88
88
  return (key) => regex.test(key);
89
89
  }
90
90
  async function get(key) {
@@ -77,7 +77,7 @@ const SOCKET_FIREWALL_APP_NAME = "sfw";
77
77
  const SOCKET_REGISTRY_APP_NAME = "registry";
78
78
  const SOCKET_APP_PREFIX = "_";
79
79
  const SOCKET_LIB_NAME = "@socketsecurity/lib";
80
- const SOCKET_LIB_VERSION = "5.20.1";
80
+ const SOCKET_LIB_VERSION = "5.21.0";
81
81
  const SOCKET_LIB_URL = "https://github.com/SocketDev/socket-lib";
82
82
  const SOCKET_LIB_USER_AGENT = `socketsecurity-lib/${SOCKET_LIB_VERSION} (${SOCKET_LIB_URL})`;
83
83
  const SOCKET_IPC_HANDSHAKE = "SOCKET_IPC_HANDSHAKE";
@@ -33,7 +33,20 @@ var import_socket = require("../paths/socket");
33
33
  let _fs;
34
34
  let _path;
35
35
  const NODE_JS_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs"]);
36
+ const PACKAGE_JSON_PATH_CACHE_MAX_SIZE = 200;
37
+ const PACKAGE_JSON_NEGATIVE_TTL_MS = 1e4;
36
38
  const packageJsonPathCache = /* @__PURE__ */ new Map();
39
+ function packageJsonPathCacheSet(key, value) {
40
+ if (packageJsonPathCache.has(key)) {
41
+ packageJsonPathCache.delete(key);
42
+ } else if (packageJsonPathCache.size >= PACKAGE_JSON_PATH_CACHE_MAX_SIZE) {
43
+ const oldest = packageJsonPathCache.keys().next().value;
44
+ if (oldest !== void 0) {
45
+ packageJsonPathCache.delete(oldest);
46
+ }
47
+ }
48
+ packageJsonPathCache.set(key, { path: value, at: Date.now() });
49
+ }
37
50
  const packageJsonContentCache = /* @__PURE__ */ new Map();
38
51
  function findPackageJson(filePath) {
39
52
  const fs = /* @__PURE__ */ getFs();
@@ -41,25 +54,29 @@ function findPackageJson(filePath) {
41
54
  const startDir = path.dirname(path.resolve(filePath));
42
55
  const cached = packageJsonPathCache.get(startDir);
43
56
  if (cached !== void 0) {
44
- if (cached === null) {
45
- return void 0;
46
- }
47
- if (fs.existsSync(cached)) {
48
- return cached;
57
+ if (cached.path === null) {
58
+ if (Date.now() - cached.at < PACKAGE_JSON_NEGATIVE_TTL_MS) {
59
+ return void 0;
60
+ }
61
+ packageJsonPathCache.delete(startDir);
62
+ } else if (fs.existsSync(cached.path)) {
63
+ packageJsonPathCacheSet(startDir, cached.path);
64
+ return cached.path;
65
+ } else {
66
+ packageJsonPathCache.delete(startDir);
49
67
  }
50
- packageJsonPathCache.delete(startDir);
51
68
  }
52
69
  let currentDir = startDir;
53
70
  const root = path.parse(currentDir).root;
54
71
  while (currentDir !== root) {
55
72
  const packageJsonPath = path.join(currentDir, "package.json");
56
73
  if (fs.existsSync(packageJsonPath)) {
57
- packageJsonPathCache.set(startDir, packageJsonPath);
74
+ packageJsonPathCacheSet(startDir, packageJsonPath);
58
75
  return packageJsonPath;
59
76
  }
60
77
  currentDir = path.dirname(currentDir);
61
78
  }
62
- packageJsonPathCache.set(startDir, null);
79
+ packageJsonPathCacheSet(startDir, null);
63
80
  return void 0;
64
81
  }
65
82
  // @__NO_SIDE_EFFECTS__
@@ -62,7 +62,19 @@ const FIREWALL_BLOCK_SEVERITIES = /* @__PURE__ */ new Set([
62
62
  "critical",
63
63
  "high"
64
64
  ]);
65
+ const BINARY_PATH_CACHE_MAX_SIZE = 200;
65
66
  const binaryPathCache = /* @__PURE__ */ new Map();
67
+ function binaryPathCacheSet(key, value) {
68
+ if (binaryPathCache.has(key)) {
69
+ binaryPathCache.delete(key);
70
+ } else if (binaryPathCache.size >= BINARY_PATH_CACHE_MAX_SIZE) {
71
+ const oldest = binaryPathCache.keys().next().value;
72
+ if (oldest !== void 0) {
73
+ binaryPathCache.delete(oldest);
74
+ }
75
+ }
76
+ binaryPathCache.set(key, value);
77
+ }
66
78
  async function checkFirewallPurls(arb, requestedPackage) {
67
79
  const idealTree = arb.idealTree;
68
80
  if (!idealTree) {
@@ -431,6 +443,7 @@ function resolveBinaryPath(basePath) {
431
443
  const cached = binaryPathCache.get(basePath);
432
444
  if (cached) {
433
445
  if (fs.existsSync(cached)) {
446
+ binaryPathCacheSet(basePath, cached);
434
447
  return cached;
435
448
  }
436
449
  binaryPathCache.delete(basePath);
@@ -439,7 +452,7 @@ function resolveBinaryPath(basePath) {
439
452
  for (const ext of extensions) {
440
453
  const testPath = basePath + ext;
441
454
  if (fs.existsSync(testPath)) {
442
- binaryPathCache.set(basePath, testPath);
455
+ binaryPathCacheSet(basePath, testPath);
443
456
  return testPath;
444
457
  }
445
458
  }
package/dist/fs.js CHANGED
@@ -167,7 +167,7 @@ async function findUp(name, options) {
167
167
  let dir = path.resolve(cwd);
168
168
  const { root } = path.parse(dir);
169
169
  const names = (0, import_arrays.isArray)(name) ? name : [name];
170
- while (dir && dir !== root) {
170
+ while (dir) {
171
171
  for (const n of names) {
172
172
  if (signal?.aborted) {
173
173
  return void 0;
@@ -184,6 +184,9 @@ async function findUp(name, options) {
184
184
  } catch {
185
185
  }
186
186
  }
187
+ if (dir === root) {
188
+ break;
189
+ }
187
190
  dir = path.dirname(dir);
188
191
  }
189
192
  return void 0;
@@ -210,7 +213,7 @@ function findUpSync(name, options) {
210
213
  const { root } = path.parse(dir);
211
214
  const stopDir = stopAt ? path.resolve(stopAt) : void 0;
212
215
  const names = (0, import_arrays.isArray)(name) ? name : [name];
213
- while (dir && dir !== root) {
216
+ while (dir) {
214
217
  if (stopDir && dir === stopDir) {
215
218
  for (const n of names) {
216
219
  const thePath = path.join(dir, n);
@@ -240,6 +243,9 @@ function findUpSync(name, options) {
240
243
  } catch {
241
244
  }
242
245
  }
246
+ if (dir === root) {
247
+ break;
248
+ }
243
249
  dir = path.dirname(dir);
244
250
  }
245
251
  return void 0;
package/dist/globs.js CHANGED
@@ -91,7 +91,11 @@ function getPicomatch() {
91
91
  function getGlobMatcher(glob2, options) {
92
92
  const patterns = Array.isArray(glob2) ? glob2 : [glob2];
93
93
  const sortedPatterns = [...patterns].sort();
94
- const sortedOptions = options ? Object.keys(options).sort().map((k) => `${k}:${JSON.stringify(options[k])}`).join(",") : "";
94
+ const sortedOptions = options ? Object.keys(options).sort().map((k) => {
95
+ const value = options[k];
96
+ const normalized = Array.isArray(value) ? [...value].sort() : value;
97
+ return `${k}:${JSON.stringify(normalized)}`;
98
+ }).join(",") : "";
95
99
  const key = `${sortedPatterns.join("|")}:${sortedOptions}`;
96
100
  const existing = matcherCache.get(key);
97
101
  if (existing) {
package/dist/ipc.js CHANGED
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(ipc_exports);
37
37
  var import_node_process = __toESM(require("node:process"));
38
38
  var import_typebox = require("./external/@sinclair/typebox");
39
39
  var import_socket = require("./paths/socket");
40
- var import_validate_schema = require("./validation/validate-schema");
40
+ var import_parse = require("./schema/parse");
41
41
  const IpcStubSchema = import_typebox.Type.Object({
42
42
  /** Process ID that created the stub. */
43
43
  pid: import_typebox.Type.Integer({ minimum: 1 }),
@@ -100,7 +100,7 @@ async function writeIpcStub(appName, data) {
100
100
  pid: import_node_process.default.pid,
101
101
  timestamp: Date.now()
102
102
  };
103
- const validated = (0, import_validate_schema.parseSchema)(IpcStubSchema, ipcData);
103
+ const validated = (0, import_parse.parseSchema)(IpcStubSchema, ipcData);
104
104
  const fs = /* @__PURE__ */ getFs();
105
105
  const flags = fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW;
106
106
  let handle;
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * @fileoverview JSON parsing utilities with Buffer detection and BOM stripping.
3
- * Provides safe JSON parsing with automatic encoding handling.
3
+ * Provides safe JSON parsing with automatic encoding handling, plus
4
+ * `safeJsonParse` for untrusted input (prototype-pollution protection +
5
+ * size limits + optional schema validation).
4
6
  */
5
- import type { JsonParseOptions, JsonPrimitive, JsonValue } from './types';
7
+ import type { Schema } from '../schema/types';
8
+ import type { JsonParseOptions, JsonPrimitive, JsonValue, SafeJsonParseOptions } from './types';
6
9
  /**
7
10
  * Check if a value is a JSON primitive type.
8
11
  * JSON primitives are: `null`, `boolean`, `number`, or `string`.
@@ -76,3 +79,45 @@ export declare function isJsonPrimitive(value: unknown): value is JsonPrimitive;
76
79
  * ```
77
80
  */
78
81
  export declare function jsonParse(content: string | Buffer, options?: JsonParseOptions | undefined): JsonValue | undefined;
82
+ /**
83
+ * Safely parse JSON with optional schema validation and security controls.
84
+ * Throws on parse failure, validation failure, or security violation.
85
+ *
86
+ * Recommended for parsing untrusted JSON (user input, network payloads,
87
+ * anything beyond a trust boundary). Layers:
88
+ * 1. Size cap (default 10 MB) prevents memory exhaustion.
89
+ * 2. Prototype-pollution reviver rejects `__proto__` / `constructor` /
90
+ * `prototype` keys at any depth (unless `allowPrototype: true`).
91
+ * 3. Optional Zod-shaped schema validation via
92
+ * `@socketsecurity/lib/schema/validate`.
93
+ *
94
+ * For trusted-source reads (package.json, local config files), prefer
95
+ * `jsonParse()` — it offers Buffer/BOM handling and filepath-aware error
96
+ * messages, without the untrusted-input overhead.
97
+ *
98
+ * @throws {Error} When `jsonString` exceeds `maxSize`.
99
+ * @throws {Error} When JSON parsing fails.
100
+ * @throws {Error} When prototype-pollution keys are detected (and
101
+ * `allowPrototype` is not `true`).
102
+ * @throws {Error} When schema validation fails.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * // Basic parsing with type inference.
107
+ * const data = safeJsonParse<User>('{"name":"Alice","age":30}')
108
+ *
109
+ * // With schema validation.
110
+ * import { z } from 'zod'
111
+ * const userSchema = z.object({ name: z.string(), age: z.number() })
112
+ * const user = safeJsonParse('{"name":"Alice","age":30}', userSchema)
113
+ *
114
+ * // With size limit.
115
+ * const data = safeJsonParse(jsonString, undefined, { maxSize: 1024 })
116
+ *
117
+ * // Allow prototype keys (DANGEROUS — only for trusted sources).
118
+ * const data = safeJsonParse('{"__proto__":{}}', undefined, {
119
+ * allowPrototype: true,
120
+ * })
121
+ * ```
122
+ */
123
+ export declare function safeJsonParse<T = unknown>(jsonString: string, schema?: Schema<T> | undefined, options?: SafeJsonParseOptions): T;