@happyvertical/files 0.78.2 → 0.79.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/README.md CHANGED
@@ -77,6 +77,80 @@ const buf = await fetchBuffer('https://example.com/image.png');
77
77
  await fetchToFile('https://example.com/file.zip', './downloads/file.zip');
78
78
  ```
79
79
 
80
+ ### Secure ZIP Manifest Inspection
81
+
82
+ Inspect untrusted ZIP metadata before deciding whether to accept an upload:
83
+
84
+ ```typescript
85
+ import {
86
+ inspectZipManifest,
87
+ ZipManifestError,
88
+ ZipManifestLimitError,
89
+ } from '@happyvertical/files';
90
+
91
+ try {
92
+ const manifest = inspectZipManifest(zipBytes, {
93
+ maxEntries: 2_000,
94
+ maxEntryUncompressedBytes: 50 * 1024 * 1024,
95
+ maxTotalUncompressedBytes: 500 * 1024 * 1024,
96
+ });
97
+
98
+ const files = manifest.entries
99
+ .filter((entry) => entry.type === 'file')
100
+ .map(({ path, size }) => ({ path, size }));
101
+ } catch (error) {
102
+ if (error instanceof ZipManifestLimitError) {
103
+ console.error(error.limit, error.actual, error.maximum);
104
+ } else if (error instanceof ZipManifestError) {
105
+ console.error(error.code, error.message);
106
+ }
107
+ }
108
+ ```
109
+
110
+ `inspectZipManifest()` reads and cross-checks central-directory and local-header
111
+ metadata only. It does not decompress or materialize file bodies. Paths are
112
+ returned with `/` separators and `.`/empty segments removed; names containing
113
+ ordinary spaces remain valid. The whole archive is rejected for parent
114
+ traversal, absolute/drive-qualified paths, NTFS alternate data stream paths
115
+ containing colons, NUL bytes, Unix symlinks and special files, Windows reparse
116
+ points and reserved device names, path segments ending in dots or spaces,
117
+ case-insensitive or Unicode-normalization path collisions, file/descendant path
118
+ conflicts, overlapping local entry ranges, or unreferenced data before the
119
+ central directory.
120
+
121
+ Default limits are 10,000 entries, 200 MiB per entry, 2 GiB aggregate declared
122
+ uncompressed size, and 1,024 encoded path bytes. Entry count includes directory
123
+ entries, and aggregate size includes every entry. All limits are configurable
124
+ with non-negative safe integers. The entry limit also bounds cumulative
125
+ central-directory work while disambiguating end records in hostile comments.
126
+
127
+ Policy is deliberately strict: ZIP64, encrypted, multi-disk, malformed,
128
+ truncated, ambiguous-end-record, and non-UTF-8-name archives are rejected with
129
+ typed errors. Stored entries must also declare identical compressed and
130
+ uncompressed sizes, and each local header and compressed payload must occupy a
131
+ distinct range. Those referenced ranges must collectively cover every byte
132
+ before the central directory, so archive preambles, padding gaps, and local
133
+ entries omitted from the central directory are rejected. Data descriptors are
134
+ rejected for both stored and compressed entries because their payload boundary
135
+ cannot be verified without decompression or extractor-specific scanning. Entry
136
+ names use strict UTF-8 decoding whether or not the UTF-8 flag is set. Leading
137
+ UTF-8 BOMs are rejected because filename decoders disagree on whether the BOM
138
+ is part of the extracted path; common macOS ZIPs with valid UTF-8 names remain
139
+ compatible.
140
+ PKWARE alternate-encoding, Info-ZIP, and Xceed path extra fields are rejected
141
+ so an extractor cannot select a different path or character encoding from the
142
+ one inspected. PKWARE and ASi Unix extra fields are likewise rejected because
143
+ they can supply link targets;
144
+ libarchive's `xl` field is rejected because it can override the inspected file
145
+ type. Unix file-type bits are honored only for Unix- or Darwin-origin central
146
+ headers; file-type bits claimed by DOS, NTFS, or other creator systems are
147
+ rejected. These alternate-metadata cases, along with contradictory Unix file
148
+ and directory attributes, use
149
+ `UnsupportedZipFeatureError` with the `ambiguous-metadata` feature. This API is
150
+ a metadata preflight, not an extraction API; consumers that later extract an
151
+ accepted archive must still use an extraction destination and library with
152
+ equivalent path and symlink protections.
153
+
80
154
  ### Error Handling
81
155
 
82
156
  ```typescript
@@ -126,6 +200,8 @@ const files = await listFiles('/path/to/dir', { match: /\.json$/ });
126
200
 
127
201
  **Fetch**: `fetchText`, `fetchJSON`, `fetchBuffer`, `fetchToFile`, `addRateLimit`, `getRateLimit`
128
202
 
203
+ **Archive inspection**: `inspectZipManifest`, `DEFAULT_ZIP_MANIFEST_LIMITS`, `ZipManifestError`, `InvalidZipArchiveError`, `UnsafeZipEntryError`, `ZipManifestLimitError`, `UnsupportedZipFeatureError`
204
+
129
205
  **Errors**: `FilesystemError`, `FileNotFoundError`, `PermissionError`, `DirectoryNotEmptyError`, `InvalidPathError`
130
206
 
131
207
  **Legacy**: `isFile`, `isDirectory`, `ensureDirectoryExists`, `download`, `upload`, `downloadFileWithCache`, `listFiles`, `getCached`, `setCached`, `getMimeType`
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Bounded, zero-decompression ZIP manifest inspection.
3
+ *
4
+ * The inspector reads ZIP metadata only. It never inflates or materializes an
5
+ * entry body, and it rejects archive features that would make the classic ZIP
6
+ * metadata ambiguous (ZIP64, encryption, and multi-disk archives).
7
+ */
8
+ /** Limits applied while inspecting ZIP metadata. */
9
+ export interface ZipManifestLimits {
10
+ /** Maximum number of central-directory entries, including directories. */
11
+ maxEntries?: number;
12
+ /** Maximum declared uncompressed size of one entry. */
13
+ maxEntryUncompressedBytes?: number;
14
+ /** Maximum aggregate declared uncompressed size across all entries. */
15
+ maxTotalUncompressedBytes?: number;
16
+ /** Maximum encoded byte length of one entry path. */
17
+ maxPathBytes?: number;
18
+ }
19
+ /** Conservative defaults suitable for rejecting untrusted uploads. */
20
+ export declare const DEFAULT_ZIP_MANIFEST_LIMITS: Readonly<Required<ZipManifestLimits>>;
21
+ /** One normalized entry from a ZIP archive. */
22
+ export interface ZipManifestEntry {
23
+ /** Normalized relative path using `/` separators and no `.` segments. */
24
+ path: string;
25
+ /** Whether the entry represents a file or directory. */
26
+ type: 'file' | 'directory';
27
+ /** Declared uncompressed size in bytes. */
28
+ size: number;
29
+ /** Declared compressed size in bytes. */
30
+ compressedSize: number;
31
+ /** ZIP compression method identifier. Entry bodies are not decompressed. */
32
+ compressionMethod: number;
33
+ }
34
+ /** A bounded metadata-only view of a ZIP archive. */
35
+ export interface ZipManifest {
36
+ entries: ZipManifestEntry[];
37
+ entryCount: number;
38
+ fileCount: number;
39
+ directoryCount: number;
40
+ totalUncompressedBytes: number;
41
+ }
42
+ export type ZipManifestErrorCode = 'ZIP_INVALID' | 'ZIP_LIMIT_EXCEEDED' | 'ZIP_UNSAFE_ENTRY' | 'ZIP_UNSUPPORTED_FEATURE';
43
+ /** Base class for failures caused by untrusted ZIP input. */
44
+ export declare class ZipManifestError extends Error {
45
+ readonly code: ZipManifestErrorCode;
46
+ readonly entryPath?: string | undefined;
47
+ constructor(message: string, code: ZipManifestErrorCode, entryPath?: string | undefined);
48
+ }
49
+ /** The archive metadata is malformed, truncated, or internally inconsistent. */
50
+ export declare class InvalidZipArchiveError extends ZipManifestError {
51
+ constructor(message: string);
52
+ }
53
+ export type ZipUnsafeEntryReason = 'absolute-path' | 'alternate-data-stream' | 'drive-qualified-path' | 'duplicate-path' | 'empty-path' | 'nul-byte' | 'path-conflict' | 'path-traversal' | 'portable-path-collision' | 'reparse-point' | 'special-file' | 'symlink' | 'windows-reserved-path';
54
+ /** An entry is unsafe to expose to an extraction workflow. */
55
+ export declare class UnsafeZipEntryError extends ZipManifestError {
56
+ readonly reason: ZipUnsafeEntryReason;
57
+ constructor(reason: ZipUnsafeEntryReason, entryPath: string, message: string);
58
+ }
59
+ export type ZipLimitName = 'entry-count' | 'entry-uncompressed-size' | 'path-bytes' | 'total-uncompressed-size';
60
+ /** A configured inspection limit was exceeded. */
61
+ export declare class ZipManifestLimitError extends ZipManifestError {
62
+ readonly limit: ZipLimitName;
63
+ readonly actual: number;
64
+ readonly maximum: number;
65
+ constructor(limit: ZipLimitName, actual: number, maximum: number, message: string, entryPath?: string);
66
+ }
67
+ export type UnsupportedZipFeature = 'ambiguous-metadata' | 'encryption' | 'multi-disk' | 'zip64';
68
+ /** The archive uses a feature deliberately outside this inspector's policy. */
69
+ export declare class UnsupportedZipFeatureError extends ZipManifestError {
70
+ readonly feature: UnsupportedZipFeature;
71
+ constructor(feature: UnsupportedZipFeature, message: string, entryPath?: string);
72
+ }
73
+ /**
74
+ * Inspect a ZIP archive without decompressing entry bodies.
75
+ *
76
+ * ZIP64, encrypted, and multi-disk archives are rejected. Entry paths are
77
+ * normalized and checked for traversal, absolute/drive-qualified forms, NTFS
78
+ * alternate data streams, NUL bytes, Unix symlinks and special files, Windows
79
+ * reparse points and reserved names, and portable case/normalization
80
+ * collisions. Local entry ranges, declared entry sizes, and end-record
81
+ * validation work are bounded and cross-checked before a manifest is returned.
82
+ */
83
+ export declare function inspectZipManifest(data: Uint8Array, limits?: ZipManifestLimits): ZipManifest;
84
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4CH,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,uEAAuE;IACvE,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,EAAE,QAAQ,CAChD,QAAQ,CAAC,iBAAiB,CAAC,CAM3B,CAAC;AAEH,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,oBAAoB,GAC5B,aAAa,GACb,oBAAoB,GACpB,kBAAkB,GAClB,yBAAyB,CAAC;AAE9B,6DAA6D;AAC7D,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,IAAI,EAAE,oBAAoB;aAC1B,SAAS,CAAC,EAAE,MAAM;gBAFlC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,oBAAoB,EAC1B,SAAS,CAAC,EAAE,MAAM,YAAA;CAKrC;AAED,gFAAgF;AAChF,qBAAa,sBAAuB,SAAQ,gBAAgB;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,uBAAuB,GACvB,sBAAsB,GACtB,gBAAgB,GAChB,YAAY,GACZ,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,yBAAyB,GACzB,eAAe,GACf,cAAc,GACd,SAAS,GACT,uBAAuB,CAAC;AAE5B,8DAA8D;AAC9D,qBAAa,mBAAoB,SAAQ,gBAAgB;aAErC,MAAM,EAAE,oBAAoB;gBAA5B,MAAM,EAAE,oBAAoB,EAC5C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,yBAAyB,GACzB,YAAY,GACZ,yBAAyB,CAAC;AAE9B,kDAAkD;AAClD,qBAAa,qBAAsB,SAAQ,gBAAgB;aAEvC,KAAK,EAAE,YAAY;aACnB,MAAM,EAAE,MAAM;aACd,OAAO,EAAE,MAAM;gBAFf,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EAC/B,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM;CAKrB;AAED,MAAM,MAAM,qBAAqB,GAC7B,oBAAoB,GACpB,YAAY,GACZ,YAAY,GACZ,OAAO,CAAC;AAEZ,+EAA+E;AAC/E,qBAAa,0BAA2B,SAAQ,gBAAgB;aAE5C,OAAO,EAAE,qBAAqB;gBAA9B,OAAO,EAAE,qBAAqB,EAC9C,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM;CAKrB;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAChB,MAAM,GAAE,iBAAsB,GAC7B,WAAW,CAoRb"}
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@
23
23
  *
24
24
  * @packageDocumentation
25
25
  */
26
+ export * from './archive';
26
27
  export type { FetchToFileOptions, WriteResponseToFileOptions, } from './fetch';
27
28
  export { addRateLimit, fetchBuffer, fetchJSON, fetchText, fetchToFile, getRateLimit, writeResponseToFile, } from './fetch';
28
29
  export * from './filesystem';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EACV,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAEjB,cAAc,cAAc,CAAC;AAE7B,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,WAAW,EACX,MAAM,EACN,SAAS,EACT,SAAS,EACT,MAAM,GACP,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAY/B,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAE5C;;;GAGG;AACH,eAAe,OAAO,CAAC;AAEvB,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,cAAc,WAAW,CAAC;AAC1B,YAAY,EACV,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAEjB,cAAc,cAAc,CAAC;AAE7B,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,WAAW,EACX,MAAM,EACN,SAAS,EACT,SAAS,EACT,MAAM,GACP,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAY/B,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAE5C;;;GAGG;AACH,eAAe,OAAO,CAAC;AAEvB,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}