@prompd/core 0.5.0-beta.14 → 0.5.0-beta.17

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