@prompd/core 0.5.0-beta.15 → 0.5.0-beta.18

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/dist/index.cjs CHANGED
@@ -3136,12 +3136,36 @@ function isBinaryAsset(name) {
3136
3136
  const dot = name.lastIndexOf(".");
3137
3137
  return dot >= 0 && BINARY_ASSET_EXT.has(name.slice(dot + 1).toLowerCase());
3138
3138
  }
3139
+ var MAX_FILE_SIZE_IN_ZIP = 10 * 1024 * 1024;
3140
+ var MAX_TOTAL_EXTRACTED_SIZE = 500 * 1024 * 1024;
3141
+ function isSymlinkEntry(perms) {
3142
+ return typeof perms === "number" && (perms & 61440) === 40960;
3143
+ }
3144
+ var pdpkgDecoder = new TextDecoder();
3139
3145
  async function extractPdpkg(buffer) {
3140
3146
  const zip = await JSZip__default.default.loadAsync(buffer);
3141
3147
  const out = /* @__PURE__ */ new Map();
3148
+ let total = 0;
3142
3149
  for (const entry of Object.values(zip.files)) {
3150
+ if (entry.name.includes("\0")) {
3151
+ throw new Error(`Security violation: null byte in entry name: ${entry.name}`);
3152
+ }
3153
+ if (entry.name.startsWith("/") || /(^|\/)\.\.(\/|$)/.test(entry.name)) {
3154
+ throw new Error(`Security violation: path traversal in entry: ${entry.name}`);
3155
+ }
3156
+ if (isSymlinkEntry(entry.unixPermissions)) {
3157
+ throw new Error(`Security violation: symlink entry in archive: ${entry.name}`);
3158
+ }
3143
3159
  if (entry.dir || isBinaryAsset(entry.name)) continue;
3144
- out.set(entry.name, await entry.async("string"));
3160
+ const bytes = await entry.async("uint8array");
3161
+ if (bytes.length > MAX_FILE_SIZE_IN_ZIP) {
3162
+ throw new Error(`File too large in package: ${entry.name} (${bytes.length} bytes, max ${MAX_FILE_SIZE_IN_ZIP})`);
3163
+ }
3164
+ total += bytes.length;
3165
+ if (total > MAX_TOTAL_EXTRACTED_SIZE) {
3166
+ throw new Error(`Package total decompressed size exceeds limit (${MAX_TOTAL_EXTRACTED_SIZE} bytes). Possible decompression bomb.`);
3167
+ }
3168
+ out.set(entry.name, pdpkgDecoder.decode(bytes));
3145
3169
  }
3146
3170
  return out;
3147
3171
  }
@@ -3368,28 +3392,18 @@ var HybridFileSystem = class {
3368
3392
 
3369
3393
  // src/lib/compiler/install.ts
3370
3394
  var MAX_PACKAGE_SIZE = 50 * 1024 * 1024;
3371
- function joinPosix3(...parts) {
3372
- const joined = parts.filter((p) => p && p !== ".").join("/").replace(/\/{2,}/g, "/");
3373
- return joined.replace(/\/+$/, "") || "/";
3395
+ function slugify(name) {
3396
+ return name.toLowerCase().replace(/[@/]+/g, "-").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3374
3397
  }
3375
- function normalizeSegments(p) {
3376
- const out = [];
3377
- for (const seg of p.split("/")) {
3378
- if (!seg || seg === ".") continue;
3379
- if (seg === "..") {
3380
- out.pop();
3381
- continue;
3382
- }
3383
- out.push(seg);
3384
- }
3385
- return out.join("/");
3398
+ function resolvePackageDir(root, type, name) {
3399
+ return joinPosix(root, ".prompd", getInstallDirForType(type), name);
3386
3400
  }
3387
- function resolveInstallDir(root, type, name) {
3388
- return joinPosix3(root, ".prompd", getInstallDirForType(type), name);
3401
+ function resolveInstallDir(root, type, name, version) {
3402
+ return joinPosix(resolvePackageDir(root, type, name), version);
3389
3403
  }
3390
3404
  async function addWorkspaceDependency(store, root, name, version) {
3391
3405
  if (!store.readFile) return;
3392
- const manifestPath = joinPosix3(root, "prompd.json");
3406
+ const manifestPath = joinPosix(root, "prompd.json");
3393
3407
  try {
3394
3408
  const existing = await store.readFile(manifestPath);
3395
3409
  const manifest = existing && existing.trim() ? JSON.parse(existing) : {};
@@ -3401,7 +3415,7 @@ async function addWorkspaceDependency(store, root, name, version) {
3401
3415
  }
3402
3416
  async function removeWorkspaceDependency(store, root, name) {
3403
3417
  if (!store.readFile) return;
3404
- const manifestPath = joinPosix3(root, "prompd.json");
3418
+ const manifestPath = joinPosix(root, "prompd.json");
3405
3419
  try {
3406
3420
  const existing = await store.readFile(manifestPath);
3407
3421
  if (!existing || !existing.trim()) return;
@@ -3413,8 +3427,20 @@ async function removeWorkspaceDependency(store, root, name) {
3413
3427
  } catch {
3414
3428
  }
3415
3429
  }
3430
+ function readManifest(files) {
3431
+ const raw = files.get("manifest.json") ?? files.get("prompd.json");
3432
+ if (!raw) return {};
3433
+ try {
3434
+ return JSON.parse(raw);
3435
+ } catch {
3436
+ return {};
3437
+ }
3438
+ }
3416
3439
  async function installPackage(opts) {
3417
- const { ref, type, root, store, download } = opts;
3440
+ return installInternal(opts, /* @__PURE__ */ new Set());
3441
+ }
3442
+ async function installInternal(opts, visited) {
3443
+ const { ref, root, store, download, global, tools, deployTool } = opts;
3418
3444
  const parsed = parsePackageReference(ref);
3419
3445
  const bytes = await download(parsed.name, parsed.version);
3420
3446
  if (bytes.length > MAX_PACKAGE_SIZE) {
@@ -3424,43 +3450,81 @@ async function installPackage(opts) {
3424
3450
  if (files.size === 0) {
3425
3451
  throw new Error(`Package "${parsed.name}" contains no installable files.`);
3426
3452
  }
3427
- let name = parsed.name;
3428
- let version = parsed.version;
3429
- const manifest = files.get("manifest.json") ?? files.get("prompd.json");
3430
- if (manifest) {
3431
- try {
3432
- const m = JSON.parse(manifest);
3433
- if (typeof m.name === "string" && m.name) name = m.name;
3434
- if (typeof m.version === "string" && m.version) version = m.version;
3435
- } catch {
3453
+ const m = readManifest(files);
3454
+ const name = typeof m.name === "string" && m.name ? m.name : parsed.name;
3455
+ const version = typeof m.version === "string" && m.version ? m.version : parsed.version;
3456
+ const type = typeof m.type === "string" && isValidPackageType(m.type) ? m.type : opts.type && isValidPackageType(opts.type) ? opts.type : "package";
3457
+ visited.add(name);
3458
+ if (m.dependencies && typeof m.dependencies === "object") {
3459
+ for (const [depName, depVersion] of Object.entries(m.dependencies)) {
3460
+ if (depName === name || visited.has(depName)) continue;
3461
+ await installInternal({ ...opts, ref: `${depName}@${depVersion}`, type: void 0 }, visited);
3436
3462
  }
3437
3463
  }
3438
- const installedPath = resolveInstallDir(root, type, name);
3439
- const baseSegments = normalizeSegments(installedPath);
3440
- await store.removeDir?.(installedPath);
3464
+ let installedPath;
3441
3465
  const written = [];
3442
- for (const [rel, content] of files) {
3443
- const dest = joinPosix3(installedPath, rel);
3444
- const destSegments = normalizeSegments(dest);
3445
- if (destSegments !== baseSegments && !destSegments.startsWith(baseSegments + "/")) {
3446
- throw new Error(`Security violation: extracted path escapes install directory: ${rel}`);
3466
+ if (type === "node-template") {
3467
+ if (!store.writeBytes) {
3468
+ throw new Error("Installing a node-template requires a PackageStore with writeBytes (binary). This host does not support it.");
3469
+ }
3470
+ const dir = joinPosix(root, ".prompd", getInstallDirForType("node-template"));
3471
+ installedPath = joinPosix(dir, `${slugify(name)}-${version}.pdpkg`);
3472
+ await store.writeBytes(installedPath, bytes);
3473
+ } else {
3474
+ installedPath = resolveInstallDir(root, type, name, version);
3475
+ const base = normalizePosix(installedPath);
3476
+ await store.removeDir?.(installedPath);
3477
+ for (const [rel, content] of files) {
3478
+ const dest = joinPosix(installedPath, rel);
3479
+ if (dest !== base && !dest.startsWith(base + "/")) {
3480
+ throw new Error(`Security violation: extracted path escapes install directory: ${rel}`);
3481
+ }
3482
+ await store.writeFile(dest, content);
3483
+ written.push(rel);
3484
+ }
3485
+ await store.writeFile(joinPosix(installedPath, ".prmdmeta"), JSON.stringify(m, null, 2) + "\n");
3486
+ if (tools && tools.length > 0) {
3487
+ if (type !== "skill") throw new Error(`Tool deploy is only valid for skills, but '${name}' is a ${type}.`);
3488
+ if (!deployTool) throw new Error("tools were requested but no deployTool hook was provided.");
3489
+ for (const tool of tools) await deployTool({ installedPath, name, tool });
3447
3490
  }
3448
- await store.writeFile(dest, content);
3449
- written.push(rel);
3450
3491
  }
3451
- await addWorkspaceDependency(store, root, name, version);
3452
- return { name, version, scope: parsed.scope, installedPath, files: written };
3492
+ if (!global) await addWorkspaceDependency(store, root, name, version);
3493
+ return { name, version, scope: parsed.scope, type, installedPath, files: written };
3453
3494
  }
3454
3495
  async function uninstallPackage(opts) {
3455
- const { ref, type, root, store } = opts;
3456
- if (!store.removeDir) {
3457
- throw new Error("Uninstall requires a PackageStore with removeDir.");
3496
+ const { root, store, global } = opts;
3497
+ if (!store.removeDir) throw new Error("Uninstall requires a PackageStore with removeDir.");
3498
+ const parsed = parsePackageReference(opts.ref);
3499
+ const name = parsed.name;
3500
+ const removed = [];
3501
+ for (const type of ["package", "workflow", "skill", "node-template"]) {
3502
+ if (type === "node-template") {
3503
+ const dir = joinPosix(root, ".prompd", getInstallDirForType("node-template"));
3504
+ if (!store.readdir || !store.removeFile || !store.readBytes) continue;
3505
+ try {
3506
+ for (const entry of await store.readdir(dir)) {
3507
+ if (!entry.endsWith(".pdpkg")) continue;
3508
+ const p = joinPosix(dir, entry);
3509
+ try {
3510
+ const m = readManifest(await extractPdpkg(await store.readBytes(p)));
3511
+ if (m.name === name) {
3512
+ await store.removeFile(p);
3513
+ removed.push(p);
3514
+ }
3515
+ } catch {
3516
+ }
3517
+ }
3518
+ } catch {
3519
+ }
3520
+ } else {
3521
+ const dir = resolvePackageDir(root, type, name);
3522
+ await store.removeDir(dir);
3523
+ removed.push(dir);
3524
+ }
3458
3525
  }
3459
- const parsed = parsePackageReference(ref);
3460
- const installedPath = resolveInstallDir(root, type, parsed.name);
3461
- await store.removeDir(installedPath);
3462
- await removeWorkspaceDependency(store, root, parsed.name);
3463
- return { name: parsed.name, scope: parsed.scope, installedPath };
3526
+ if (!global) await removeWorkspaceDependency(store, root, name);
3527
+ return { name, scope: parsed.scope, removed };
3464
3528
  }
3465
3529
 
3466
3530
  // src/lib/compiler/index.ts