codex-plugin-doctor 1.10.0 → 1.12.1

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/README.md CHANGED
@@ -358,9 +358,9 @@ jobs:
358
358
  runs-on: ubuntu-latest
359
359
  steps:
360
360
  - uses: actions/checkout@v5
361
- - uses: Esquetta/CodexPluginDoctor@v1.10.0
361
+ - uses: Esquetta/CodexPluginDoctor@v1.12.1
362
362
  with:
363
- version: "1.10.0"
363
+ version: "1.12.1"
364
364
  path: .
365
365
  runtime: "true"
366
366
  policy: codex-publish
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildDoctorAttestation, renderDoctorAttestationJson, verifyDoctorAttestation } from "./attestation.js";
5
5
  import { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceJson, verifyDoctorReleaseEvidence } from "./release-evidence.js";
@@ -46,6 +46,25 @@ function relativeBundleFiles() {
46
46
  releaseEvidenceJson: "release-evidence.json"
47
47
  };
48
48
  }
49
+ function isPathInsideDirectory(candidatePath, directoryPath) {
50
+ const relativePath = path.relative(directoryPath, candidatePath);
51
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
52
+ }
53
+ async function resolveBundleArtifactPath(bundleDirectory, relativePath) {
54
+ const resolvedBundleDirectory = path.resolve(bundleDirectory);
55
+ const artifactPath = path.resolve(resolvedBundleDirectory, relativePath);
56
+ if (!isPathInsideDirectory(artifactPath, resolvedBundleDirectory)) {
57
+ throw new Error("Bundle artifact path resolves outside the bundle directory.");
58
+ }
59
+ const [canonicalBundleDirectory, canonicalArtifactPath] = await Promise.all([
60
+ realpath(resolvedBundleDirectory),
61
+ realpath(artifactPath)
62
+ ]);
63
+ if (!isPathInsideDirectory(canonicalArtifactPath, canonicalBundleDirectory)) {
64
+ throw new Error("Bundle artifact canonical path resolves outside the bundle directory.");
65
+ }
66
+ return canonicalArtifactPath;
67
+ }
49
68
  function bundleStatus(runtimePolicy, releaseEvidence) {
50
69
  if (runtimePolicy.status === "fail" || releaseEvidence.status === "fail") {
51
70
  return "fail";
@@ -167,7 +186,7 @@ export function renderDoctorReviewBundleJson(bundle) {
167
186
  return JSON.stringify(bundle.manifest, null, 2);
168
187
  }
169
188
  async function readBundleJsonFile(bundleDirectory, relativePath) {
170
- return readJsonFile(path.join(bundleDirectory, relativePath));
189
+ return readJsonFile(await resolveBundleArtifactPath(bundleDirectory, relativePath));
171
190
  }
172
191
  async function readBundleDiffSnapshot(bundleDirectory) {
173
192
  const manifestArtifact = await readBundleJsonFile(bundleDirectory, "manifest.json");
@@ -344,7 +363,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
344
363
  const files = manifest?.files ?? relativeBundleFiles();
345
364
  for (const [fileKey, relativePath] of Object.entries(files)) {
346
365
  try {
347
- const fileStat = await stat(path.join(resolvedBundleDirectory, relativePath));
366
+ const artifactPath = await resolveBundleArtifactPath(resolvedBundleDirectory, relativePath);
367
+ const fileStat = await stat(artifactPath);
348
368
  checks.push({
349
369
  id: `review_bundle.file.${fileKey}`,
350
370
  status: fileStat.isFile() ? "pass" : "fail",
@@ -404,7 +424,28 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
404
424
  continue;
405
425
  }
406
426
  try {
407
- const content = await readFile(path.join(resolvedBundleDirectory, expected.path));
427
+ const declaredPath = files[fileKey];
428
+ if (expected.path !== declaredPath) {
429
+ integrityStatus = "fail";
430
+ checks.push({
431
+ id: `review_bundle.integrity.${fileKey}.path`,
432
+ status: "fail",
433
+ message: `${expected.path} does not match the declared bundle file path.`
434
+ });
435
+ continue;
436
+ }
437
+ const resolvedIntegrityPath = path.resolve(resolvedBundleDirectory, expected.path);
438
+ if (!isPathInsideDirectory(resolvedIntegrityPath, resolvedBundleDirectory)) {
439
+ integrityStatus = "fail";
440
+ checks.push({
441
+ id: `review_bundle.integrity.${fileKey}.path`,
442
+ status: "fail",
443
+ message: `${expected.path} resolves outside the review bundle directory.`
444
+ });
445
+ continue;
446
+ }
447
+ const integrityPath = await resolveBundleArtifactPath(resolvedBundleDirectory, expected.path);
448
+ const content = await readFile(integrityPath);
408
449
  const digest = sha256(content);
409
450
  const matches = digest === expected.digest && content.byteLength === expected.bytes;
410
451
  if (!matches) {
@@ -472,7 +513,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
472
513
  });
473
514
  }
474
515
  try {
475
- attestation = await verifyDoctorAttestation(path.join(resolvedBundleDirectory, files.attestationJson), targetPath, { signingKey: options.signingKey });
516
+ const attestationPath = await resolveBundleArtifactPath(resolvedBundleDirectory, files.attestationJson);
517
+ attestation = await verifyDoctorAttestation(attestationPath, targetPath, { signingKey: options.signingKey });
476
518
  checks.push({
477
519
  id: "review_bundle.attestation",
478
520
  status: attestation.status,
@@ -489,7 +531,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
489
531
  });
490
532
  }
491
533
  try {
492
- releaseEvidence = await verifyDoctorReleaseEvidence(path.join(resolvedBundleDirectory, files.releaseEvidenceJson), {
534
+ const releaseEvidencePath = await resolveBundleArtifactPath(resolvedBundleDirectory, files.releaseEvidenceJson);
535
+ releaseEvidence = await verifyDoctorReleaseEvidence(releaseEvidencePath, {
493
536
  signingKey: options.signingKey,
494
537
  targetPath
495
538
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.10.0",
3
+ "version": "1.12.1",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",