Skip to content

Commit 4f55a30

Browse files
committed
chore(release): v5.28.0
### Added - compression (new export) — brotli + gzip helpers with three calling shapes (in-memory Buffer, file-to-file, raw streams) and a single { inPlace: true } option. 28 named exports including BROTLI_EXTS / GZIP_EXTS constants, stripExt, resolveBrotliOptions / resolveGzipOptions for callers building their own pipelines. - socket-lib CLI (new bin entry) — pnpm exec socket-lib <command>. Initial subcommand: check primordials (alias check prim). Sectional config in .socket-lib.json with .config/socket-lib.json fallback. Flags: --config/-c, --explain, --json, --silent, --help. Coverage: compression.ts at 100% / 100% / 100% / 100% (statements / branches / funcs / lines), 64 tests.
1 parent aceb886 commit 4f55a30

4 files changed

Lines changed: 274 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [5.28.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.28.0) - 2026-05-06
9+
10+
### Added
11+
12+
- **`compression` (new export)** — brotli + gzip helpers with three calling shapes (in-memory `Buffer`, file-to-file, raw streams) and a single `{ inPlace: true }` option for compress/decompress-in-place. 28 named exports total:
13+
- In-memory: `compressBrotli`, `decompressBrotli`, `compressGzip`, `decompressGzip`
14+
- File-to-file: `compressBrotliFile`, `decompressBrotliFile`, `compressGzipFile`, `decompressGzipFile` — each with three overloads (explicit dest, in-place, options object). The gzip in-place path follows `.tgz``.tar` convention so a round-trip is lossless
15+
- Streams: `createBrotliCompressor`, `createBrotliDecompressor`, `createGzipCompressor`, `createGzipDecompressor`
16+
- Detection: `isBrotliCompressed(buffer)` / `isGzipCompressed(buffer)` (magic-byte sniffing)
17+
- Path classification: `hasBrotliExt(filePath)` / `hasGzipExt(filePath)` — case-insensitive `path.extname` match against `.br` / `.brotli` / `.gz` / `.gzip` / `.tgz`
18+
- Helpers: `BROTLI_EXTS` / `GZIP_EXTS` `ReadonlySet<string>` constants; `stripExt(filePath, exts)` for trimming a recognized extension from a path; `resolveBrotliOptions` / `resolveGzipOptions` for translating `CompressOptions` into the underlying zlib option shapes
19+
- `CompressOptions` / `CompressFileOptions` interfaces
20+
- **`socket-lib` CLI (new `bin` entry)** — fleet-wide static-analysis dispatcher invoked via `pnpm exec socket-lib <command>`. Initial subcommand: `check primordials` (alias `check prim`) — diffs every name destructured from `primordials` in scanned source against `@socketsecurity/lib`'s exposed primordials set, emitting unmapped or missing-from-lib findings. Reads sectional config from `.socket-lib.json` (with `.config/socket-lib.json` as a fallback) or a bare object for single-check setups. Flags: `--config / -c <path>` (defaults to `.socket-lib.json`, falls back to `.config/socket-lib.json`), `--explain`, `--json`, `--silent`, `--help`. Lifted from socket-btm's `scripts/check-primordials-coverage.mts` so the same drift gate now ships to every consumer.
21+
822
## [5.27.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.27.0) - 2026-05-04
923

1024
### Added

src/bin/check-primordials.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,26 @@ import { parseArgs as parseLibArgs } from '../argv/parse'
3232

3333
const logger = getDefaultLogger()
3434

35-
// Lives in `.config/` next to tsconfig.base.json, taze.config.mts,
36-
// etc. — the existing fleet pattern for tooling configs that
37-
// consumers read. One file per repo for all socket-lib checks;
38-
// section per check.
39-
const DEFAULT_CONFIG_PATH = '.config/socket-lib.json'
35+
// Default config name. We accept both the root-level dotfile (the
36+
// canonical `.<tool>rc.json` shape) and the `.config/`-rooted variant
37+
// (the fleet pattern for tooling configs). Look up in this order when
38+
// `--config` was not explicitly passed, falling through to the next
39+
// candidate if the file is missing — first hit wins. One file per
40+
// repo for all socket-lib checks; section per check.
41+
// At the repo root we use the canonical `.<tool>.json` dotfile shape.
42+
// Inside `.config/`, the directory itself is already hidden, so the
43+
// fleet convention drops the leading dot — every existing file under
44+
// `.config/` (taze.config.mts, vitest.config.mts, tsconfig.base.json,
45+
// ...) is bare-named. Match that.
46+
const DEFAULT_CONFIG_PATH = '.socket-lib.json'
47+
const FALLBACK_CONFIG_PATHS: readonly string[] = [
48+
'.socket-lib.json',
49+
'.config/socket-lib.json',
50+
]
4051
const CONFIG_SECTION = 'primordials'
4152

4253
interface ParsedArgs {
43-
readonly config: string
54+
readonly config: string | undefined
4455
readonly json: boolean
4556
readonly explain: boolean
4657
readonly silent: boolean
@@ -52,22 +63,46 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
5263
args: argv,
5364
strict: false,
5465
options: {
55-
config: { type: 'string', default: DEFAULT_CONFIG_PATH },
66+
config: { type: 'string', short: 'c' },
5667
explain: { type: 'boolean' },
5768
help: { type: 'boolean', short: 'h' },
5869
json: { type: 'boolean' },
5970
silent: { type: 'boolean' },
6071
},
6172
})
73+
// `config` is left undefined when neither `--config` nor `-c` was
74+
// passed, so the resolver below can fall back to the search list.
75+
// An explicit value short-circuits the search.
76+
const explicitConfig = values['config']
6277
return {
63-
config: String(values['config'] ?? DEFAULT_CONFIG_PATH),
78+
config: typeof explicitConfig === 'string' ? explicitConfig : undefined,
6479
json: Boolean(values['json']),
6580
explain: Boolean(values['explain']),
6681
silent: Boolean(values['silent']),
6782
help: Boolean(values['help']),
6883
}
6984
}
7085

86+
/**
87+
* Pick the config file. Returns the explicit `--config` argument when
88+
* given (even if it doesn't exist — the caller will surface the error
89+
* with the path they typed). Otherwise probes the fallback list in
90+
* order and returns the first hit. Returns the head of the list when
91+
* none exist, so the caller's "config file not found" error message
92+
* names the canonical default.
93+
*/
94+
function resolveConfigPath(explicit: string | undefined): string {
95+
if (explicit !== undefined) {
96+
return explicit
97+
}
98+
for (const candidate of FALLBACK_CONFIG_PATHS) {
99+
if (existsSync(path.resolve(candidate))) {
100+
return candidate
101+
}
102+
}
103+
return FALLBACK_CONFIG_PATHS[0]!
104+
}
105+
71106
function printHelp(): void {
72107
logger.log('socket-lib check primordials — primordials drift check')
73108
logger.log('')
@@ -76,13 +111,16 @@ function printHelp(): void {
76111
logger.log(' socket-lib check prim [opts] # short alias')
77112
logger.log('')
78113
logger.log('Options:')
79-
logger.log(` --config <path> Config file. Default: ${DEFAULT_CONFIG_PATH}`)
80-
logger.log(' --explain Print one detailed line per finding.')
81-
logger.log(' --json Machine-readable JSON output.')
82-
logger.log(' --quiet Silent on success.')
83-
logger.log(' --help, -h Print this help.')
114+
logger.log(
115+
` --config, -c <path> Config file. Default: ${DEFAULT_CONFIG_PATH}`,
116+
)
117+
logger.log(` (falls back to .config/socket-lib.json)`)
118+
logger.log(' --explain Print one detailed line per finding.')
119+
logger.log(' --json Machine-readable JSON output.')
120+
logger.log(' --silent Silent on success.')
121+
logger.log(' --help, -h Print this help.')
84122
logger.log('')
85-
logger.log('Config (.config/socket-lib.json — primordials section):')
123+
logger.log('Config (.socket-lib.json — primordials section):')
86124
logger.log(' {')
87125
logger.log(' "primordials": {')
88126
logger.log(' "aliasMap": { "Array": "ArrayCtor" },')
@@ -238,7 +276,7 @@ export async function runCheckPrimordials(
238276
}
239277
let config: PrimordialsCheckConfig
240278
try {
241-
config = loadConfig(path.resolve(args.config))
279+
config = loadConfig(path.resolve(resolveConfigPath(args.config)))
242280
} catch (e) {
243281
logger.error(`socket-lib check primordials: ${errorMessage(e)}`)
244282
return 1

src/compression.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
import { promisify } from 'node:util'
5454

5555
import { safeDelete } from './fs'
56+
import { StringPrototypeToLowerCase } from './primordials'
5657

5758
const brotliCompressAsync = promisify(brotliCompress)
5859
const brotliDecompressAsync = promisify(brotliDecompress)
@@ -99,7 +100,13 @@ interface ResolvedBrotliOptions extends BrotliOptions {
99100
params: NonNullable<BrotliOptions['params']>
100101
}
101102

102-
function resolveBrotliOptions(
103+
/**
104+
* Translate `CompressOptions` into the `BrotliOptions` zlib expects.
105+
* Defaults `quality` to 11 (max) when not provided, and forwards a
106+
* positive `size` hint. Exposed for callers building their own zlib
107+
* pipelines and for unit-test coverage.
108+
*/
109+
export function resolveBrotliOptions(
103110
options: CompressOptions | undefined,
104111
): ResolvedBrotliOptions {
105112
const level = options?.level ?? 11
@@ -112,7 +119,15 @@ function resolveBrotliOptions(
112119
return { params }
113120
}
114121

115-
function resolveGzipOptions(options: CompressOptions | undefined): ZlibOptions {
122+
/**
123+
* Translate `CompressOptions` into the `ZlibOptions` zlib expects.
124+
* Returns an empty options object when no `level` is given (zlib uses
125+
* its default, level 6). Exposed for parity with
126+
* `resolveBrotliOptions` and for unit-test coverage.
127+
*/
128+
export function resolveGzipOptions(
129+
options: CompressOptions | undefined,
130+
): ZlibOptions {
116131
const level = options?.level
117132
if (level === undefined) {
118133
return { __proto__: null } as unknown as ZlibOptions
@@ -229,7 +244,7 @@ export async function decompressBrotliFile(
229244
`decompressBrotliFile: ${p} has no .br/.brotli extension; can't infer destination`,
230245
)
231246
}
232-
return stripCompressedExt(p, BROTLI_EXTS)
247+
return stripExt(p, BROTLI_EXTS)
233248
},
234249
)
235250
await pipeline(
@@ -356,7 +371,12 @@ export async function decompressGzipFile(
356371
`decompressGzipFile: ${p} has no .gz/.gzip/.tgz extension; can't infer destination`,
357372
)
358373
}
359-
return stripCompressedExt(p, GZIP_EXTS)
374+
// .tgz is conventionally .tar.gz collapsed — recover the .tar so
375+
// a round-trip through compress/decompress is lossless.
376+
const stripped = stripExt(p, GZIP_EXTS)
377+
return StringPrototypeToLowerCase(path.extname(p)) === '.tgz'
378+
? `${stripped}.tar`
379+
: stripped
360380
},
361381
)
362382
await pipeline(
@@ -429,45 +449,48 @@ export function isGzipCompressed(input: Buffer): boolean {
429449
// is case-sensitive (it reflects the OS path semantics), but our
430450
// extension classifier is policy: ".BR" should classify the same as
431451
// ".br" regardless of host OS. Lowercase the extname before lookup.
432-
const BROTLI_EXTS: ReadonlySet<string> = new Set(['.br', '.brotli'])
433-
const GZIP_EXTS: ReadonlySet<string> = new Set(['.gz', '.gzip', '.tgz'])
452+
//
453+
// Exported so callers can introspect what counts as a "brotli" or
454+
// "gzip" extension without re-implementing the list, and so tests can
455+
// pin the recognized sets.
456+
export const BROTLI_EXTS: ReadonlySet<string> = new Set(['.br', '.brotli'])
457+
export const GZIP_EXTS: ReadonlySet<string> = new Set(['.gz', '.gzip', '.tgz'])
434458

435459
/**
436460
* Extension check for brotli paths — matches `.br` / `.brotli`
437461
* (case-insensitive). Naming follows node:path's `extname`.
438462
*/
439463
export function hasBrotliExt(filePath: string): boolean {
440-
return BROTLI_EXTS.has(path.extname(filePath).toLowerCase())
464+
return BROTLI_EXTS.has(StringPrototypeToLowerCase(path.extname(filePath)))
441465
}
442466

443467
/**
444468
* Extension check for gzip paths — matches `.gz` / `.gzip` / `.tgz`
445469
* (case-insensitive). Naming follows node:path's `extname`.
446470
*/
447471
export function hasGzipExt(filePath: string): boolean {
448-
return GZIP_EXTS.has(path.extname(filePath).toLowerCase())
472+
return GZIP_EXTS.has(StringPrototypeToLowerCase(path.extname(filePath)))
449473
}
450474

451475
/**
452-
* Strip a recognized compression extension from a filename, returning
453-
* the path without it. Returns the input unchanged when no recognized
454-
* extension is present. Case-insensitive — preserves the rest of the
455-
* path's casing. The `.tgz` extension maps to `.tar` (not "no
456-
* extension"), since `.tgz` is conventionally `.tar.gz` collapsed.
476+
* Strip the trailing extension from a filename when it matches one of
477+
* `exts`. Returns the input unchanged when the trailing extname isn't
478+
* in the set. Case-insensitive on the extension — preserves the rest
479+
* of the path's casing.
480+
*
481+
* The `exts` set decides what counts. Pass `BROTLI_EXTS` / `GZIP_EXTS`
482+
* (re-exported from this module) for the canonical compression sets,
483+
* or your own set for custom classifiers.
484+
*
485+
* This helper is generic — it does NOT know that `.tgz` is short for
486+
* `.tar.gz`. Callers that need that convention compose this with their
487+
* own follow-up (see `decompressGzipFile` for the canonical example).
457488
*/
458-
function stripCompressedExt(
459-
filePath: string,
460-
exts: ReadonlySet<string>,
461-
): string {
489+
export function stripExt(filePath: string, exts: ReadonlySet<string>): string {
462490
const ext = path.extname(filePath)
463-
const lower = ext.toLowerCase()
464-
if (!exts.has(lower)) {
491+
if (!exts.has(StringPrototypeToLowerCase(ext))) {
465492
return filePath
466493
}
467-
// .tgz is short for .tar.gz — when stripping, recover the .tar.
468-
if (lower === '.tgz') {
469-
return filePath.slice(0, -ext.length) + '.tar'
470-
}
471494
return filePath.slice(0, -ext.length)
472495
}
473496

0 commit comments

Comments
 (0)