feat: make archive dependencies optional
This commit is contained in:
parent
1cd216921d
commit
722cb41390
@ -16,7 +16,7 @@ This is a library-level guardrail, not OS-level isolation. It does not replace c
|
||||
pnpm add @openclaw/fs-safe
|
||||
```
|
||||
|
||||
Node 20.11 or newer. Core root/path/json/temp helpers avoid framework dependencies; archive helpers use `jszip` and `tar` for ZIP/TAR support.
|
||||
Node 20.11 or newer. Core root/path/json/temp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP/TAR support; installs that omit optional dependencies can still use every non-archive subpath.
|
||||
|
||||
## Quick start
|
||||
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
|
||||
`@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. Extraction stages into a private directory and merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
|
||||
|
||||
Archive extraction uses optional runtime dependencies: `jszip` for ZIP and `tar`
|
||||
for TAR. Installs that omit optional dependencies can still import this subpath,
|
||||
inspect archive kinds, and use pure path/limit helpers, but extraction or ZIP
|
||||
loading fails with a clear message until the matching optional dependency is
|
||||
installed.
|
||||
|
||||
```ts
|
||||
import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
|
||||
```
|
||||
|
||||
@ -81,7 +81,7 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
||||
|
||||
## Runtime dependencies
|
||||
|
||||
`@openclaw/fs-safe` depends on `jszip` and `tar` for [archive extraction](archive.md). Both are loaded lazily — if your code never touches the archive subpath, the runtime cost is negligible.
|
||||
`@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for [archive extraction](archive.md). They are loaded lazily and only required when ZIP/TAR helpers run. Installs that omit optional dependencies can still import and use every non-archive subpath; archive calls fail with a clear missing-optional-dependency message.
|
||||
|
||||
There are no peer dependencies and no native build step.
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@
|
||||
"check": "pnpm build && pnpm test",
|
||||
"docs:site": "node scripts/build-docs-site.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"optionalDependencies": {
|
||||
"jszip": "^3.10.1",
|
||||
"tar": "7.5.13"
|
||||
},
|
||||
|
||||
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@ -7,13 +7,6 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
tar:
|
||||
specifier: 7.5.13
|
||||
version: 7.5.13
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.15.19
|
||||
@ -27,6 +20,13 @@ importers:
|
||||
vitest:
|
||||
specifier: ^3.1.4
|
||||
version: 3.2.4(@types/node@22.19.17)
|
||||
optionalDependencies:
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
tar:
|
||||
specifier: 7.5.13
|
||||
version: 7.5.13
|
||||
|
||||
packages:
|
||||
|
||||
@ -1002,6 +1002,7 @@ snapshots:
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
optional: true
|
||||
|
||||
'@istanbuljs/schema@0.1.6': {}
|
||||
|
||||
@ -1213,7 +1214,8 @@ snapshots:
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chownr@3.0.0: {}
|
||||
chownr@3.0.0:
|
||||
optional: true
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
@ -1221,7 +1223,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
core-util-is@1.0.3:
|
||||
optional: true
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
@ -1303,13 +1306,16 @@ snapshots:
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
immediate@3.0.6:
|
||||
optional: true
|
||||
|
||||
inherits@2.0.4: {}
|
||||
inherits@2.0.4:
|
||||
optional: true
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
isarray@1.0.0:
|
||||
optional: true
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
@ -1350,10 +1356,12 @@ snapshots:
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
optional: true
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
optional: true
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
@ -1386,6 +1394,7 @@ snapshots:
|
||||
minizlib@3.1.0:
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
optional: true
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
@ -1393,7 +1402,8 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
pako@1.0.11:
|
||||
optional: true
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
@ -1416,7 +1426,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
process-nextick-args@2.0.1:
|
||||
optional: true
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
@ -1427,6 +1438,7 @@ snapshots:
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
optional: true
|
||||
|
||||
rollup@4.60.2:
|
||||
dependencies:
|
||||
@ -1459,11 +1471,13 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
safe-buffer@5.1.2:
|
||||
optional: true
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
setimmediate@1.0.5:
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
@ -1496,6 +1510,7 @@ snapshots:
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
optional: true
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
@ -1520,6 +1535,7 @@ snapshots:
|
||||
minipass: 7.1.3
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
optional: true
|
||||
|
||||
test-exclude@7.0.2:
|
||||
dependencies:
|
||||
@ -1546,7 +1562,8 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
util-deprecate@1.0.2:
|
||||
optional: true
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.17):
|
||||
dependencies:
|
||||
@ -1643,4 +1660,5 @@ snapshots:
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
yallist@5.0.0: {}
|
||||
yallist@5.0.0:
|
||||
optional: true
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import JSZip from "jszip";
|
||||
import {
|
||||
ARCHIVE_LIMIT_ERROR_CODE,
|
||||
ArchiveLimitError,
|
||||
@ -7,6 +6,14 @@ import {
|
||||
type ArchiveExtractLimits,
|
||||
} from "./archive-limits.js";
|
||||
|
||||
export type ZipArchiveWithFiles = {
|
||||
files: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type JsZipConstructor = {
|
||||
loadAsync(buffer: Buffer | Uint8Array): Promise<ZipArchiveWithFiles>;
|
||||
};
|
||||
|
||||
const ZIP_EOCD_SIGNATURE = 0x06054b50;
|
||||
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
||||
const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
|
||||
@ -193,7 +200,7 @@ export function readZipCentralDirectoryEntryCount(buffer: Buffer | Uint8Array):
|
||||
export async function loadZipArchiveWithPreflight(
|
||||
buffer: Buffer | Uint8Array,
|
||||
limits?: ArchiveExtractLimits,
|
||||
): Promise<JSZip> {
|
||||
): Promise<ZipArchiveWithFiles> {
|
||||
const resolvedLimits = resolveExtractLimits(limits);
|
||||
if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
|
||||
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
||||
@ -202,5 +209,31 @@ export async function loadZipArchiveWithPreflight(
|
||||
if (entryCount !== null) {
|
||||
assertArchiveEntryCountWithinLimit(entryCount, resolvedLimits);
|
||||
}
|
||||
const JSZip = await importOptionalJsZip();
|
||||
return await JSZip.loadAsync(buffer);
|
||||
}
|
||||
|
||||
async function importOptionalJsZip(): Promise<JsZipConstructor> {
|
||||
try {
|
||||
const module = await import("jszip");
|
||||
const candidate: unknown =
|
||||
typeof module === "function" ? module : (module as { default?: unknown }).default;
|
||||
if (
|
||||
(typeof candidate !== "object" && typeof candidate !== "function") ||
|
||||
candidate === null ||
|
||||
typeof (candidate as { loadAsync?: unknown }).loadAsync !== "function"
|
||||
) {
|
||||
throw new Error('Optional archive dependency "jszip" does not expose loadAsync().');
|
||||
}
|
||||
return candidate as JsZipConstructor;
|
||||
} catch (err) {
|
||||
throw missingOptionalArchiveDependencyError("jszip", err);
|
||||
}
|
||||
}
|
||||
|
||||
function missingOptionalArchiveDependencyError(packageName: "jszip", cause: unknown): Error {
|
||||
return new Error(
|
||||
`Optional archive dependency "${packageName}" is not installed. Install it to use ZIP archive helpers from @openclaw/fs-safe/archive.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import * as tar from "tar";
|
||||
import {
|
||||
resolveArchiveOutputPath,
|
||||
stripArchivePath,
|
||||
@ -71,6 +70,7 @@ export { createTarEntryPreflightChecker, type TarEntryInfo } from "./archive-tar
|
||||
export {
|
||||
loadZipArchiveWithPreflight,
|
||||
readZipCentralDirectoryEntryCount,
|
||||
type ZipArchiveWithFiles,
|
||||
} from "./archive-zip-preflight.js";
|
||||
|
||||
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
||||
@ -111,6 +111,17 @@ type ZipEntry = {
|
||||
};
|
||||
|
||||
type ZipExtractBudget = ReturnType<typeof createByteBudgetTracker>;
|
||||
type TarModule = {
|
||||
x(options: {
|
||||
file: string;
|
||||
cwd: string;
|
||||
strip: number;
|
||||
gzip?: boolean;
|
||||
preservePaths: false;
|
||||
strict: true;
|
||||
onReadEntry(this: unknown, entry: unknown): void;
|
||||
}): Promise<unknown>;
|
||||
};
|
||||
|
||||
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
||||
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
||||
@ -304,6 +315,7 @@ export async function extractArchive(params: {
|
||||
if (kind === "tar") {
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
const tar = await importOptionalTar();
|
||||
const limits = resolveExtractLimits(params.limits);
|
||||
const stat = await fs.stat(params.archivePath);
|
||||
if (stat.size > limits.maxArchiveBytes) {
|
||||
@ -367,3 +379,18 @@ export async function extractArchive(params: {
|
||||
label,
|
||||
);
|
||||
}
|
||||
|
||||
async function importOptionalTar(): Promise<TarModule> {
|
||||
try {
|
||||
return await import("tar");
|
||||
} catch (err) {
|
||||
throw missingOptionalArchiveDependencyError("tar", err);
|
||||
}
|
||||
}
|
||||
|
||||
function missingOptionalArchiveDependencyError(packageName: "tar", cause: unknown): Error {
|
||||
return new Error(
|
||||
`Optional archive dependency "${packageName}" is not installed. Install it to use TAR archive helpers from @openclaw/fs-safe/archive.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user