🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@nimblebrain/mpak-sdk

Package Overview
Dependencies
Maintainers
2
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nimblebrain/mpak-sdk - npm Package Compare versions

Comparing version
0.5.0
to
0.6.0
+186
-29
dist/index.cjs
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;

@@ -18,2 +20,10 @@ var __export = (target, all) => {

};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

@@ -24,2 +34,3 @@

__export(index_exports, {
MAX_UNCOMPRESSED_SIZE: () => MAX_UNCOMPRESSED_SIZE,
Mpak: () => Mpak,

@@ -37,2 +48,4 @@ MpakBundleCache: () => MpakBundleCache,

MpakNotFoundError: () => MpakNotFoundError,
extractZip: () => extractZip,
isSemverEqual: () => isSemverEqual,
parsePackageSpec: () => parsePackageSpec

@@ -43,3 +56,3 @@ });

// src/mpakSDK.ts
var import_node_child_process2 = require("child_process");
var import_node_child_process = require("child_process");
var import_node_fs4 = require("fs");

@@ -439,7 +452,9 @@ var import_node_path4 = require("path");

// src/helpers.ts
var import_node_child_process = require("child_process");
var import_node_crypto2 = require("crypto");
var import_node_fs = require("fs");
var import_node_path = require("path");
var MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024;
var import_node_stream = require("stream");
var import_promises = require("stream/promises");
var import_yauzl = __toESM(require("yauzl"), 1);
var MAX_UNCOMPRESSED_SIZE = 2 * 1024 * 1024 * 1024;
var UPDATE_CHECK_TTL_MS = 60 * 60 * 1e3;

@@ -449,34 +464,163 @@ function isSemverEqual(a, b) {

}
function extractZip(zipPath, destDir) {
async function extractZip(zipPath, destDir, options = {}) {
const maxSize = options.maxUncompressedSize ?? MAX_UNCOMPRESSED_SIZE;
let zipfile;
try {
const listOutput = (0, import_node_child_process.execFileSync)("unzip", ["-l", zipPath], {
stdio: "pipe",
encoding: "utf8"
});
const totalMatch = listOutput.match(/^\s*(\d+)\s+\d+\s+files?$/m);
if (totalMatch) {
const totalSize = parseInt(totalMatch[1] ?? "0", 10);
if (totalSize > MAX_UNCOMPRESSED_SIZE) {
zipfile = await openZip(zipPath);
} catch (error) {
throw new MpakCacheCorruptedError(
`Cannot open bundle archive: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
try {
let entries;
try {
entries = await readAllEntries(zipfile);
} catch (error) {
throw new MpakCacheCorruptedError(
`Invalid bundle entry: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
const declaredTotal = entries.reduce((sum, e) => sum + e.uncompressedSize, 0);
if (declaredTotal > maxSize) {
throw new MpakCacheCorruptedError(
`Bundle uncompressed size (${formatMB(declaredTotal)}MB) exceeds maximum allowed (${formatMB(maxSize)}MB)`,
zipPath
);
}
const normalizedDest = (0, import_node_path.resolve)(destDir);
(0, import_node_fs.mkdirSync)(normalizedDest, { recursive: true });
let cumulativeBytes = 0;
for (const entry of entries) {
const safePath = resolveEntryPath(normalizedDest, entry.fileName, zipPath);
if (isSymlink(entry)) {
throw new MpakCacheCorruptedError(
`Bundle uncompressed size (${Math.round(totalSize / 1024 / 1024)}MB) exceeds maximum allowed (${MAX_UNCOMPRESSED_SIZE / (1024 * 1024)}MB)`,
`Bundle contains a symlink entry (${entry.fileName}); symlinks are not permitted`,
zipPath
);
}
if (isDirectoryEntry(entry)) {
(0, import_node_fs.mkdirSync)(safePath, { recursive: true });
continue;
}
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(safePath), { recursive: true });
try {
const readStream = await openReadStream(zipfile, entry);
const counter = createByteCounter(
entry,
zipPath,
() => cumulativeBytes,
maxSize,
(n) => {
cumulativeBytes = n;
}
);
await (0, import_promises.pipeline)(readStream, counter, (0, import_node_fs.createWriteStream)(safePath));
} catch (error) {
if (error instanceof MpakCacheCorruptedError) throw error;
throw new MpakCacheCorruptedError(
`Failed to extract entry ${entry.fileName}: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
const mode = unixMode(entry);
if (mode !== null) {
(0, import_node_fs.chmodSync)(safePath, mode);
}
}
} catch (error) {
if (error instanceof MpakCacheCorruptedError) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
} finally {
zipfile.close();
}
}
function openZip(path) {
return new Promise((res, rej) => {
import_yauzl.default.open(path, { lazyEntries: false, autoClose: false }, (err, zipfile) => {
if (err || !zipfile) rej(err ?? new Error("yauzl returned no zipfile"));
else res(zipfile);
});
});
}
function readAllEntries(zipfile) {
return new Promise((res, rej) => {
const entries = [];
zipfile.on("entry", (entry) => entries.push(entry));
zipfile.on("end", () => res(entries));
zipfile.on("error", rej);
});
}
function openReadStream(zipfile, entry) {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, (err, stream) => {
if (err || !stream) rej(err ?? new Error("yauzl returned no read stream"));
else res(stream);
});
});
}
function resolveEntryPath(normalizedDest, entryName, zipPath) {
if (entryName.includes("\0")) {
throw new MpakCacheCorruptedError(
`Cannot verify bundle size before extraction: ${message}`,
zipPath,
error instanceof Error ? error : void 0
`Bundle entry name contains NUL byte: ${JSON.stringify(entryName)}`,
zipPath
);
}
(0, import_node_fs.mkdirSync)(destDir, { recursive: true });
(0, import_node_child_process.execFileSync)("unzip", ["-o", "-q", zipPath, "-d", destDir], {
stdio: "pipe"
const candidate = (0, import_node_path.resolve)(normalizedDest, entryName);
if (candidate !== normalizedDest && !candidate.startsWith(normalizedDest + import_node_path.sep)) {
throw new MpakCacheCorruptedError(
`Bundle entry escapes destination directory: ${entryName}`,
zipPath
);
}
return candidate;
}
function isDirectoryEntry(entry) {
return /\/$/.test(entry.fileName);
}
function isSymlink(entry) {
const mode = entry.externalFileAttributes >>> 16;
return (mode & 61440) === 40960;
}
function unixMode(entry) {
const mode = entry.externalFileAttributes >>> 16 & 511;
return mode === 0 ? null : mode;
}
function createByteCounter(entry, zipPath, getCumulative, maxSize, setCumulative) {
let entryBytes = 0;
return new import_node_stream.Transform({
transform(chunk, _enc, cb) {
entryBytes += chunk.length;
if (entryBytes > entry.uncompressedSize) {
cb(
new MpakCacheCorruptedError(
`Entry ${entry.fileName} exceeds its declared uncompressed size (possible zip bomb)`,
zipPath
)
);
return;
}
const next = getCumulative() + chunk.length;
if (next > maxSize) {
cb(
new MpakCacheCorruptedError(
`Bundle exceeds maximum uncompressed size (${formatMB(maxSize)}MB) during extraction`,
zipPath
)
);
return;
}
setCumulative(next);
cb(null, chunk);
}
});
}
function formatMB(bytes) {
return Math.round(bytes / (1024 * 1024));
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function hashBundlePath(bundlePath) {

@@ -520,2 +664,3 @@ return (0, import_node_crypto2.createHash)("md5").update((0, import_node_path.resolve)(bundlePath)).digest("hex").slice(0, 12);

cacheHome;
maxUncompressedSize;
mpakClient;

@@ -525,2 +670,3 @@ constructor(client, options) {

this.cacheHome = (0, import_node_path2.join)(options?.mpakHome ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".mpak"), "cache");
this.maxUncompressedSize = options?.maxUncompressedSize;
}

@@ -711,3 +857,3 @@ /**

}
extractZip(tempPath, cacheDir);
await extractZip(tempPath, cacheDir, this.extractOptions());
this.writeCacheMetadata(name, {

@@ -722,2 +868,6 @@ version: bundle.version,

}
/** Options threaded into every `extractZip` call. */
extractOptions() {
return this.maxUncompressedSize !== void 0 ? { maxUncompressedSize: this.maxUncompressedSize } : {};
}
};

@@ -952,5 +1102,9 @@

this.client = new MpakClient(clientConfig);
this.bundleCache = new MpakBundleCache(this.client, {
const cacheOptions = {
mpakHome: this.configManager.mpakHome
});
};
if (options?.maxUncompressedSize !== void 0) {
cacheOptions.maxUncompressedSize = options.maxUncompressedSize;
}
this.bundleCache = new MpakBundleCache(this.client, cacheOptions);
}

@@ -1035,3 +1189,3 @@ /**

try {
extractZip(absolutePath, cacheDir);
await extractZip(absolutePath, cacheDir, this.bundleCache.extractOptions());
} catch (err) {

@@ -1226,3 +1380,3 @@ throw new MpakInvalidBundleError(

static findPythonCommand() {
const result = (0, import_node_child_process2.spawnSync)("python3", ["--version"], { stdio: "pipe" });
const result = (0, import_node_child_process.spawnSync)("python3", ["--version"], { stdio: "pipe" });
if (result.status === 0) {

@@ -1272,2 +1426,3 @@ return "python3";

0 && (module.exports = {
MAX_UNCOMPRESSED_SIZE,
Mpak,

@@ -1285,4 +1440,6 @@ MpakBundleCache,

MpakNotFoundError,
extractZip,
isSemverEqual,
parsePackageSpec
});
//# sourceMappingURL=index.cjs.map

@@ -129,2 +129,7 @@ import { BundleSearchParamsInput, BundleSearchResponse, BundleDetail, VersionsResponse, VersionDetail, PlatformInfo, DownloadInfo, SkillSearchParamsInput, SkillSearchResponse, SkillDetail, SkillDownloadInfo, CacheMetadata, McpbManifest, CachedBundleInfo } from '@nimblebrain/mpak-schemas';

mpakHome?: string;
/**
* Maximum allowed uncompressed size (bytes) for any bundle this cache
* extracts. Defaults to {@link MAX_UNCOMPRESSED_SIZE}.
*/
maxUncompressedSize?: number;
}

@@ -155,2 +160,3 @@ /**

readonly cacheHome: string;
readonly maxUncompressedSize: number | undefined;
private readonly mpakClient;

@@ -231,2 +237,6 @@ constructor(client: MpakClient, options?: MpakBundleCacheOptions);

private downloadAndExtract;
/** Options threaded into every `extractZip` call. */
extractOptions(): {
maxUncompressedSize?: number;
};
}

@@ -376,2 +386,10 @@

userAgent?: string;
/**
* Maximum allowed uncompressed size (bytes) for any bundle this SDK
* extracts. Defaults to the SDK's built-in cap (see
* `MAX_UNCOMPRESSED_SIZE` in `helpers.ts`). Raise this for bundles that
* vendor heavy ML/Python dependencies; lower it to enforce a stricter
* policy.
*/
maxUncompressedSize?: number;
}

@@ -579,2 +597,44 @@ /**

/**
* Default maximum allowed uncompressed size for a bundle (2GB).
*
* Override per-consumer via `MpakBundleCache` / `Mpak` options
* (`maxUncompressedSize`), or per-call via the `extractZip` options.
*/
declare const MAX_UNCOMPRESSED_SIZE: number;
/** Options for {@link extractZip}. */
interface ExtractZipOptions {
/**
* Maximum total uncompressed size in bytes. Defaults to
* {@link MAX_UNCOMPRESSED_SIZE}. Enforced against both the declared
* central-directory sizes and the actual bytes written during extraction
* (the latter defeats zip bombs that lie about declared sizes).
*/
maxUncompressedSize?: number;
}
/**
* Compare two semver strings for equality, ignoring leading 'v' prefix.
*/
declare function isSemverEqual(a: string, b: string): boolean;
/**
* Extract a ZIP file to a directory, streaming entries to disk.
*
* Guarantees:
* - **Zip-bomb protection:** the declared total uncompressed size (from the
* central directory) is checked before any files are written; during
* extraction, per-entry and cumulative byte counts are tracked and
* extraction aborts if either would exceed limits.
* - **Path-traversal safe:** entry paths are resolved against `destDir` and
* any entry that escapes the destination is rejected.
* - **Symlinks rejected:** bundles should not contain symlinks; their
* presence fails extraction.
* - **Unix mode bits preserved** for entries that declare them (matches the
* behavior of the prior `unzip` shell-out).
*
* Uses a pure-JS zip library — no dependency on the `unzip` binary.
*
* @throws {MpakCacheCorruptedError} for any format, size, or safety violation.
*/
declare function extractZip(zipPath: string, destDir: string, options?: ExtractZipOptions): Promise<void>;
/**
* Base error class for mpak SDK errors

@@ -692,2 +752,2 @@ */

export { Mpak, MpakBundleCache, type MpakBundleCacheOptions, MpakCacheCorruptedError, MpakClient, type MpakClientConfig, MpakConfigCorruptedError, MpakConfigError, MpakConfigManager, type MpakConfigManagerOptions, MpakError, MpakIntegrityError, MpakInvalidBundleError, MpakNetworkError, MpakNotFoundError, type MpakOptions, type PackageConfig, type PrepareServerOptions, type PrepareServerSpec, type ServerCommand, parsePackageSpec };
export { type ExtractZipOptions, MAX_UNCOMPRESSED_SIZE, Mpak, MpakBundleCache, type MpakBundleCacheOptions, MpakCacheCorruptedError, MpakClient, type MpakClientConfig, MpakConfigCorruptedError, MpakConfigError, MpakConfigManager, type MpakConfigManagerOptions, MpakError, MpakIntegrityError, MpakInvalidBundleError, MpakNetworkError, MpakNotFoundError, type MpakOptions, type PackageConfig, type PrepareServerOptions, type PrepareServerSpec, type ServerCommand, extractZip, isSemverEqual, parsePackageSpec };

@@ -129,2 +129,7 @@ import { BundleSearchParamsInput, BundleSearchResponse, BundleDetail, VersionsResponse, VersionDetail, PlatformInfo, DownloadInfo, SkillSearchParamsInput, SkillSearchResponse, SkillDetail, SkillDownloadInfo, CacheMetadata, McpbManifest, CachedBundleInfo } from '@nimblebrain/mpak-schemas';

mpakHome?: string;
/**
* Maximum allowed uncompressed size (bytes) for any bundle this cache
* extracts. Defaults to {@link MAX_UNCOMPRESSED_SIZE}.
*/
maxUncompressedSize?: number;
}

@@ -155,2 +160,3 @@ /**

readonly cacheHome: string;
readonly maxUncompressedSize: number | undefined;
private readonly mpakClient;

@@ -231,2 +237,6 @@ constructor(client: MpakClient, options?: MpakBundleCacheOptions);

private downloadAndExtract;
/** Options threaded into every `extractZip` call. */
extractOptions(): {
maxUncompressedSize?: number;
};
}

@@ -376,2 +386,10 @@

userAgent?: string;
/**
* Maximum allowed uncompressed size (bytes) for any bundle this SDK
* extracts. Defaults to the SDK's built-in cap (see
* `MAX_UNCOMPRESSED_SIZE` in `helpers.ts`). Raise this for bundles that
* vendor heavy ML/Python dependencies; lower it to enforce a stricter
* policy.
*/
maxUncompressedSize?: number;
}

@@ -579,2 +597,44 @@ /**

/**
* Default maximum allowed uncompressed size for a bundle (2GB).
*
* Override per-consumer via `MpakBundleCache` / `Mpak` options
* (`maxUncompressedSize`), or per-call via the `extractZip` options.
*/
declare const MAX_UNCOMPRESSED_SIZE: number;
/** Options for {@link extractZip}. */
interface ExtractZipOptions {
/**
* Maximum total uncompressed size in bytes. Defaults to
* {@link MAX_UNCOMPRESSED_SIZE}. Enforced against both the declared
* central-directory sizes and the actual bytes written during extraction
* (the latter defeats zip bombs that lie about declared sizes).
*/
maxUncompressedSize?: number;
}
/**
* Compare two semver strings for equality, ignoring leading 'v' prefix.
*/
declare function isSemverEqual(a: string, b: string): boolean;
/**
* Extract a ZIP file to a directory, streaming entries to disk.
*
* Guarantees:
* - **Zip-bomb protection:** the declared total uncompressed size (from the
* central directory) is checked before any files are written; during
* extraction, per-entry and cumulative byte counts are tracked and
* extraction aborts if either would exceed limits.
* - **Path-traversal safe:** entry paths are resolved against `destDir` and
* any entry that escapes the destination is rejected.
* - **Symlinks rejected:** bundles should not contain symlinks; their
* presence fails extraction.
* - **Unix mode bits preserved** for entries that declare them (matches the
* behavior of the prior `unzip` shell-out).
*
* Uses a pure-JS zip library — no dependency on the `unzip` binary.
*
* @throws {MpakCacheCorruptedError} for any format, size, or safety violation.
*/
declare function extractZip(zipPath: string, destDir: string, options?: ExtractZipOptions): Promise<void>;
/**
* Base error class for mpak SDK errors

@@ -692,2 +752,2 @@ */

export { Mpak, MpakBundleCache, type MpakBundleCacheOptions, MpakCacheCorruptedError, MpakClient, type MpakClientConfig, MpakConfigCorruptedError, MpakConfigError, MpakConfigManager, type MpakConfigManagerOptions, MpakError, MpakIntegrityError, MpakInvalidBundleError, MpakNetworkError, MpakNotFoundError, type MpakOptions, type PackageConfig, type PrepareServerOptions, type PrepareServerSpec, type ServerCommand, parsePackageSpec };
export { type ExtractZipOptions, MAX_UNCOMPRESSED_SIZE, Mpak, MpakBundleCache, type MpakBundleCacheOptions, MpakCacheCorruptedError, MpakClient, type MpakClientConfig, MpakConfigCorruptedError, MpakConfigError, MpakConfigManager, type MpakConfigManagerOptions, MpakError, MpakIntegrityError, MpakInvalidBundleError, MpakNetworkError, MpakNotFoundError, type MpakOptions, type PackageConfig, type PrepareServerOptions, type PrepareServerSpec, type ServerCommand, extractZip, isSemverEqual, parsePackageSpec };
// src/mpakSDK.ts
import { spawnSync } from "child_process";
import { chmodSync, existsSync as existsSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
import { chmodSync as chmodSync2, existsSync as existsSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
import { join as join4, resolve as resolve3 } from "path";

@@ -397,7 +397,16 @@ import { McpbManifestSchema as McpbManifestSchema2 } from "@nimblebrain/mpak-schemas";

// src/helpers.ts
import { execFileSync } from "child_process";
import { createHash as createHash2 } from "crypto";
import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
import { resolve, join } from "path";
var MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024;
import {
chmodSync,
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
statSync
} from "fs";
import { dirname, join, resolve, sep } from "path";
import { Transform } from "stream";
import { pipeline } from "stream/promises";
import yauzl from "yauzl";
var MAX_UNCOMPRESSED_SIZE = 2 * 1024 * 1024 * 1024;
var UPDATE_CHECK_TTL_MS = 60 * 60 * 1e3;

@@ -407,34 +416,163 @@ function isSemverEqual(a, b) {

}
function extractZip(zipPath, destDir) {
async function extractZip(zipPath, destDir, options = {}) {
const maxSize = options.maxUncompressedSize ?? MAX_UNCOMPRESSED_SIZE;
let zipfile;
try {
const listOutput = execFileSync("unzip", ["-l", zipPath], {
stdio: "pipe",
encoding: "utf8"
});
const totalMatch = listOutput.match(/^\s*(\d+)\s+\d+\s+files?$/m);
if (totalMatch) {
const totalSize = parseInt(totalMatch[1] ?? "0", 10);
if (totalSize > MAX_UNCOMPRESSED_SIZE) {
zipfile = await openZip(zipPath);
} catch (error) {
throw new MpakCacheCorruptedError(
`Cannot open bundle archive: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
try {
let entries;
try {
entries = await readAllEntries(zipfile);
} catch (error) {
throw new MpakCacheCorruptedError(
`Invalid bundle entry: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
const declaredTotal = entries.reduce((sum, e) => sum + e.uncompressedSize, 0);
if (declaredTotal > maxSize) {
throw new MpakCacheCorruptedError(
`Bundle uncompressed size (${formatMB(declaredTotal)}MB) exceeds maximum allowed (${formatMB(maxSize)}MB)`,
zipPath
);
}
const normalizedDest = resolve(destDir);
mkdirSync(normalizedDest, { recursive: true });
let cumulativeBytes = 0;
for (const entry of entries) {
const safePath = resolveEntryPath(normalizedDest, entry.fileName, zipPath);
if (isSymlink(entry)) {
throw new MpakCacheCorruptedError(
`Bundle uncompressed size (${Math.round(totalSize / 1024 / 1024)}MB) exceeds maximum allowed (${MAX_UNCOMPRESSED_SIZE / (1024 * 1024)}MB)`,
`Bundle contains a symlink entry (${entry.fileName}); symlinks are not permitted`,
zipPath
);
}
if (isDirectoryEntry(entry)) {
mkdirSync(safePath, { recursive: true });
continue;
}
mkdirSync(dirname(safePath), { recursive: true });
try {
const readStream = await openReadStream(zipfile, entry);
const counter = createByteCounter(
entry,
zipPath,
() => cumulativeBytes,
maxSize,
(n) => {
cumulativeBytes = n;
}
);
await pipeline(readStream, counter, createWriteStream(safePath));
} catch (error) {
if (error instanceof MpakCacheCorruptedError) throw error;
throw new MpakCacheCorruptedError(
`Failed to extract entry ${entry.fileName}: ${errorMessage(error)}`,
zipPath,
error instanceof Error ? error : void 0
);
}
const mode = unixMode(entry);
if (mode !== null) {
chmodSync(safePath, mode);
}
}
} catch (error) {
if (error instanceof MpakCacheCorruptedError) {
throw error;
}
const message = error instanceof Error ? error.message : String(error);
} finally {
zipfile.close();
}
}
function openZip(path) {
return new Promise((res, rej) => {
yauzl.open(path, { lazyEntries: false, autoClose: false }, (err, zipfile) => {
if (err || !zipfile) rej(err ?? new Error("yauzl returned no zipfile"));
else res(zipfile);
});
});
}
function readAllEntries(zipfile) {
return new Promise((res, rej) => {
const entries = [];
zipfile.on("entry", (entry) => entries.push(entry));
zipfile.on("end", () => res(entries));
zipfile.on("error", rej);
});
}
function openReadStream(zipfile, entry) {
return new Promise((res, rej) => {
zipfile.openReadStream(entry, (err, stream) => {
if (err || !stream) rej(err ?? new Error("yauzl returned no read stream"));
else res(stream);
});
});
}
function resolveEntryPath(normalizedDest, entryName, zipPath) {
if (entryName.includes("\0")) {
throw new MpakCacheCorruptedError(
`Cannot verify bundle size before extraction: ${message}`,
zipPath,
error instanceof Error ? error : void 0
`Bundle entry name contains NUL byte: ${JSON.stringify(entryName)}`,
zipPath
);
}
mkdirSync(destDir, { recursive: true });
execFileSync("unzip", ["-o", "-q", zipPath, "-d", destDir], {
stdio: "pipe"
const candidate = resolve(normalizedDest, entryName);
if (candidate !== normalizedDest && !candidate.startsWith(normalizedDest + sep)) {
throw new MpakCacheCorruptedError(
`Bundle entry escapes destination directory: ${entryName}`,
zipPath
);
}
return candidate;
}
function isDirectoryEntry(entry) {
return /\/$/.test(entry.fileName);
}
function isSymlink(entry) {
const mode = entry.externalFileAttributes >>> 16;
return (mode & 61440) === 40960;
}
function unixMode(entry) {
const mode = entry.externalFileAttributes >>> 16 & 511;
return mode === 0 ? null : mode;
}
function createByteCounter(entry, zipPath, getCumulative, maxSize, setCumulative) {
let entryBytes = 0;
return new Transform({
transform(chunk, _enc, cb) {
entryBytes += chunk.length;
if (entryBytes > entry.uncompressedSize) {
cb(
new MpakCacheCorruptedError(
`Entry ${entry.fileName} exceeds its declared uncompressed size (possible zip bomb)`,
zipPath
)
);
return;
}
const next = getCumulative() + chunk.length;
if (next > maxSize) {
cb(
new MpakCacheCorruptedError(
`Bundle exceeds maximum uncompressed size (${formatMB(maxSize)}MB) during extraction`,
zipPath
)
);
return;
}
setCumulative(next);
cb(null, chunk);
}
});
}
function formatMB(bytes) {
return Math.round(bytes / (1024 * 1024));
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function hashBundlePath(bundlePath) {

@@ -478,2 +616,3 @@ return createHash2("md5").update(resolve(bundlePath)).digest("hex").slice(0, 12);

cacheHome;
maxUncompressedSize;
mpakClient;

@@ -483,2 +622,3 @@ constructor(client, options) {

this.cacheHome = join2(options?.mpakHome ?? join2(homedir(), ".mpak"), "cache");
this.maxUncompressedSize = options?.maxUncompressedSize;
}

@@ -669,3 +809,3 @@ /**

}
extractZip(tempPath, cacheDir);
await extractZip(tempPath, cacheDir, this.extractOptions());
this.writeCacheMetadata(name, {

@@ -680,2 +820,6 @@ version: bundle.version,

}
/** Options threaded into every `extractZip` call. */
extractOptions() {
return this.maxUncompressedSize !== void 0 ? { maxUncompressedSize: this.maxUncompressedSize } : {};
}
};

@@ -910,5 +1054,9 @@

this.client = new MpakClient(clientConfig);
this.bundleCache = new MpakBundleCache(this.client, {
const cacheOptions = {
mpakHome: this.configManager.mpakHome
});
};
if (options?.maxUncompressedSize !== void 0) {
cacheOptions.maxUncompressedSize = options.maxUncompressedSize;
}
this.bundleCache = new MpakBundleCache(this.client, cacheOptions);
}

@@ -993,3 +1141,3 @@ /**

try {
extractZip(absolutePath, cacheDir);
await extractZip(absolutePath, cacheDir, this.bundleCache.extractOptions());
} catch (err) {

@@ -1117,3 +1265,3 @@ throw new MpakInvalidBundleError(

try {
chmodSync(command, 493);
chmodSync2(command, 493);
} catch {

@@ -1229,2 +1377,3 @@ }

export {
MAX_UNCOMPRESSED_SIZE,
Mpak,

@@ -1242,4 +1391,6 @@ MpakBundleCache,

MpakNotFoundError,
extractZip,
isSemverEqual,
parsePackageSpec
};
//# sourceMappingURL=index.js.map
+3
-1
{
"name": "@nimblebrain/mpak-sdk",
"version": "0.5.0",
"version": "0.6.0",
"description": "TypeScript SDK for mpak registry - MCPB bundles and Agent Skills",

@@ -53,2 +53,3 @@ "author": "NimbleBrain Inc <engineering@mpak.dev>",

"dependencies": {
"yauzl": "^3.2.0",
"zod": "^4.3.6",

@@ -59,2 +60,3 @@ "@nimblebrain/mpak-schemas": "0.2.0"

"@types/node": "^22.0.0",
"@types/yauzl": "^2.10.3",
"jszip": "^3.10.1",

@@ -61,0 +63,0 @@ "tsup": "^8.4.0",

@@ -289,8 +289,11 @@ # @nimblebrain/mpak-sdk

#### Static Methods
#### Utility Exports
| Method | Description |
Standalone functions exported from the package (not methods on any class):
| Export | Description |
|---|---|
| `BundleCache.extractZip(zipPath, destDir)` | Extract a ZIP with zip-bomb protection |
| `BundleCache.isSemverEqual(a, b)` | Compare semver strings (ignores `v` prefix) |
| `extractZip(zipPath, destDir, options?)` | Async. Stream-extract a ZIP with zip-bomb, path-traversal, and symlink protection. Pass `{ maxUncompressedSize }` to override the default cap. |
| `MAX_UNCOMPRESSED_SIZE` | Default uncompressed-size cap applied by `extractZip` (2 GB). |
| `isSemverEqual(a, b)` | Compare semver strings (ignores `v` prefix) |

@@ -297,0 +300,0 @@ ### ConfigManager (`mpak.config`)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display