pmx-canvas 0.1.27 → 0.1.28

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.
@@ -5,11 +5,12 @@ import {
5
5
  readdirSync,
6
6
  mkdirSync,
7
7
  readFileSync,
8
+ realpathSync,
8
9
  statSync,
9
10
  unlinkSync,
10
11
  writeFileSync,
11
12
  } from 'node:fs';
12
- import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
13
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
13
14
  import { ensureArtifactsDir, getWorkspaceRoot } from './artifact-paths.js';
14
15
  import { canvasState, type CanvasNodeState } from './canvas-state.js';
15
16
  import { findOpenCanvasPosition } from './placement.js';
@@ -338,6 +339,33 @@ async function runProcess(
338
339
  return { stdout: stdout.trim(), stderr: stderr.trim() };
339
340
  }
340
341
 
342
+ /**
343
+ * Resolve the init/bundle script path. Without an override the trusted bundled
344
+ * resolver is used. An override (a test/debugging escape hatch exposed on every
345
+ * surface — CLI --init-script-path/--bundle-script-path, the MCP tool, the HTTP
346
+ * endpoint, and the SDK) is only honored when it resolves inside the active
347
+ * workspace root — otherwise it would let a caller exec an arbitrary host script
348
+ * via bash as the server user. This containment is the single chokepoint that
349
+ * makes forwarding the field safe across all of those surfaces.
350
+ */
351
+ function resolveTrustedScriptPath(override: string | undefined, kind: 'init' | 'bundle'): string {
352
+ if (!override) return resolveWebArtifactScriptPath(kind);
353
+ const resolved = resolve(override);
354
+ // Compare real (symlink-resolved) paths on both sides so a legitimately
355
+ // contained override under a symlinked root (e.g. macOS /var -> /private/var,
356
+ // or a workspace that itself lives beneath a symlink) is not wrongly rejected.
357
+ // The candidate is realpath'd only when it exists; a non-existent escape path
358
+ // still fails the containment check below (or the existsSync guard at the call
359
+ // site). This keeps the guard strict — genuine escapes are still rejected.
360
+ const realRoot = realpathSync(currentWorkspaceRoot());
361
+ const realCandidate = existsSync(resolved) ? realpathSync(resolved) : resolved;
362
+ const workspaceRel = relative(realRoot, realCandidate);
363
+ if (workspaceRel === '..' || workspaceRel.startsWith(`..${sep}`) || isAbsolute(workspaceRel)) {
364
+ throw new Error(`Web-artifact ${kind} script override must resolve inside the workspace: ${override}`);
365
+ }
366
+ return resolved;
367
+ }
368
+
341
369
  export function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string {
342
370
  const scriptFile = kind === 'init' ? 'init-artifact.sh' : 'bundle-artifact.sh';
343
371
  const candidates = [
@@ -459,10 +487,8 @@ export async function executeWebArtifactBuild(
459
487
  const slug = slugify(input.title);
460
488
  const projectPath = resolve(input.projectPath ?? join(artifactsDir, '.web-artifacts', slug));
461
489
  const outputPath = resolve(input.outputPath ?? join(artifactsDir, `${slug}.html`));
462
- const initScriptPath = resolve(input.initScriptPath ?? resolveWebArtifactScriptPath('init'));
463
- const bundleScriptPath = resolve(
464
- input.bundleScriptPath ?? resolveWebArtifactScriptPath('bundle'),
465
- );
490
+ const initScriptPath = resolveTrustedScriptPath(input.initScriptPath, 'init');
491
+ const bundleScriptPath = resolveTrustedScriptPath(input.bundleScriptPath, 'bundle');
466
492
  const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
467
493
 
468
494
  if (!existsSync(initScriptPath)) {