#!/usr/bin/env -S node --import tsx
// Openclaw Npm Postpublish Verify script supports OpenClaw repository automation.

import { createPublicKey, verify as verifySignature } from "node:crypto";
import {
  existsSync,
  lstatSync,
  mkdtempSync,
  opendirSync,
  readdirSync,
  readFileSync,
  realpathSync,
  rmSync,
} from "node:fs";
import { builtinModules } from "node:module";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import {
  dirname,
  isAbsolute,
  join,
  posix as pathPosix,
  relative,
  win32 as pathWin32,
} from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
import { readBoundedResponseText } from "./lib/bounded-response.ts";
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
import { runNpmVerifyCommand } from "./lib/npm-verify-exec.ts";
import {
  collectRuntimeDependencySpecs,
  packageNameFromSpecifier,
} from "./lib/plugin-package-dependencies.mjs";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";

type InstalledPackageJson = {
  version?: string;
  dependencies?: Record<string, string>;
  optionalDependencies?: Record<string, string>;
};

type InstalledBundledExtensionPackageJson = {
  dependencies?: Record<string, string>;
  optionalDependencies?: Record<string, string>;
};

type InstalledBundledExtensionManifestRecord = {
  id: string;
  manifest: InstalledBundledExtensionPackageJson;
  path: string;
};

const MAX_BUNDLED_EXTENSION_MANIFEST_BYTES = 1024 * 1024;
const LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER =
  "Failed to load legacy context engine runtime.";
const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.filter(
  (relativePath) => listBundledPluginPackArtifacts().includes(relativePath),
);
const NODE_BUILTIN_MODULES = new Set(builtinModules.map((name) => name.replace(/^node:/u, "")));
const MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 6 * 1024 * 1024;
// Keep the dependency scan bounded while allowing headroom for generated root chunks.
const MAX_INSTALLED_ROOT_DIST_JS_FILES = 10_000;
const ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE = /\.(?:c|m)?js$/u;
const OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS = new Set([
  // Optional A2UI markdown renderer. The Canvas host bundle catches the missing
  // package and falls back when the optional renderer is unavailable.
  "@a2ui/markdown-it",
  "@discordjs/opus",
  "@lancedb/lancedb",
  // Feishu/Lark remains a bundled plugin package. Root dist can retain orphaned
  // lazy chunks from the plugin build even though dist/extensions/feishu is
  // externalized from the root package scan.
  "@larksuiteoapi/node-sdk",
  // Discord remains an official external plugin. The root package can retain
  // orphaned lazy chunks from the plugin build, but the plugin owns prism-media.
  "prism-media",
  "@matrix-org/matrix-sdk-crypto-nodejs",
  "link-preview-js",
  "matrix-js-sdk",
  // Public plugin SDK contract helpers are intentionally test-only entrypoints.
  // Consumers importing them run under their own Vitest dev dependency.
  "vitest",
]);
const require = createRequire(import.meta.url);
const acorn = require("acorn") as typeof import("acorn");

type DistJavaScriptFileListResult =
  | { files: string[]; limitExceeded: false }
  | { files: string[]; limit: number; limitExceeded: true };

export type PublishedInstallScenario = {
  name: string;
  installSpecs: string[];
  expectedVersion: string;
};

export type OpenClawNpmPostpublishVerifyArgs =
  | {
      help: false;
      version: string;
    }
  | {
      help: true;
      version: "";
    };

export function openClawNpmPostpublishVerifyUsage(): string {
  return "Usage: node --import tsx scripts/openclaw-npm-postpublish-verify.ts <version>";
}

export function parseOpenClawNpmPostpublishVerifyArgs(
  argv: readonly string[],
): OpenClawNpmPostpublishVerifyArgs {
  const args = argv[0] === "--" ? argv.slice(1) : argv;
  const version = args[0]?.trim() ?? "";
  if (version === "--help" || version === "-h") {
    return { help: true, version: "" };
  }
  if (!version) {
    throw new Error(openClawNpmPostpublishVerifyUsage());
  }
  if (version.startsWith("-")) {
    throw new Error(`Unknown openclaw npm postpublish verifier option: ${version}`);
  }
  const extraArg = args[1]?.trim();
  if (extraArg) {
    throw new Error(`Unexpected openclaw npm postpublish verifier argument: ${extraArg}`);
  }
  return { help: false, version };
}

export function buildPublishedInstallScenarios(version: string): PublishedInstallScenario[] {
  const parsed = parseReleaseVersion(version);
  if (parsed === null) {
    throw new Error(`Unsupported release version "${version}".`);
  }

  const exactSpec = `openclaw@${version}`;
  const scenarios: PublishedInstallScenario[] = [
    {
      name: "fresh-exact",
      installSpecs: [exactSpec],
      expectedVersion: version,
    },
  ];

  if (parsed.channel === "stable" && parsed.correctionNumber !== undefined) {
    scenarios.push({
      name: "upgrade-from-base-stable",
      installSpecs: [`openclaw@${parsed.baseVersion}`, exactSpec],
      expectedVersion: version,
    });
  }

  return scenarios;
}

type NpmRegistryKey = {
  key: string;
  keyid: string;
};

type NpmRegistrySignature = {
  keyid: string;
  sig: string;
};

type NpmRegistryAttestation = {
  bundle?: {
    dsseEnvelope?: {
      payload?: string;
    };
  };
  predicateType?: string;
};

type NpmProvenanceVerificationPolicy = {
  certificateIdentityURI: string;
  certificateIssuer: string;
};

type VerifyNpmProvenanceBundle = (
  bundle: unknown,
  policy: NpmProvenanceVerificationPolicy,
) => Promise<void>;

type NpmProvenanceStatement = {
  predicate?: {
    buildDefinition?: {
      externalParameters?: {
        workflow?: {
          path?: string;
          ref?: string;
          repository?: string;
        };
      };
    };
    runDetails?: {
      builder?: {
        id?: string;
      };
    };
  };
  subject?: Array<{
    digest?: Record<string, string>;
    name?: string;
  }>;
};

const NPM_PROVENANCE_PREDICATE_TYPE = "https://slsa.dev/provenance/v1";
const NPM_PROVENANCE_REPOSITORY = "https://github.com/openclaw/openclaw";
const NPM_PROVENANCE_WORKFLOW_PATH = ".github/workflows/openclaw-npm-release.yml";
const NPM_PROVENANCE_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com";
const NPM_PROVENANCE_BUILDER_ID = "https://github.com/actions/runner/github-hosted";
const NPM_REGISTRY_REQUEST_TIMEOUT_MS = 30_000;
const NPM_REGISTRY_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
const NPM_REGISTRY_PROVENANCE_ATTEMPTS = 30;
const NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS = 10_000;

type FetchRegistryJsonOptions = {
  fetchImpl?: typeof fetch;
  maxBodyBytes?: number;
  timeoutMs?: number;
};

export function verifyNpmRegistrySignatures(params: {
  integrity: string;
  keys: NpmRegistryKey[];
  packageName: string;
  signatures: NpmRegistrySignature[];
  version: string;
}): void {
  if (!params.integrity.startsWith("sha512-")) {
    throw new Error(`npm registry integrity is missing a sha512 digest for ${params.packageName}.`);
  }
  if (params.signatures.length === 0) {
    throw new Error(
      `npm registry returned no signatures for ${params.packageName}@${params.version}.`,
    );
  }

  const payload = `${params.packageName}@${params.version}:${params.integrity}`;
  for (const signature of params.signatures) {
    const key = params.keys.find((candidate) => candidate.keyid === signature.keyid);
    if (!key) {
      continue;
    }
    const publicKey = createPublicKey({
      key: Buffer.from(key.key, "base64"),
      format: "der",
      type: "spki",
    });
    if (
      verifySignature(
        "sha256",
        Buffer.from(payload, "utf8"),
        publicKey,
        Buffer.from(signature.sig, "base64"),
      )
    ) {
      return;
    }
  }

  throw new Error(
    `npm registry signatures did not verify for ${params.packageName}@${params.version}.`,
  );
}

function resolveNpmProvenanceVerificationPolicy(
  statement: NpmProvenanceStatement,
  version: string,
): NpmProvenanceVerificationPolicy {
  const parsedVersion = parseReleaseVersion(version);
  if (parsedVersion === null) {
    throw new Error(`Unsupported release version "${version}".`);
  }
  const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
  const workflowRef = workflow?.ref;
  const expectedReleaseRef = `refs/heads/release/${parsedVersion.baseVersion}`;
  const isTrustedRef =
    workflowRef === "refs/heads/main" ||
    workflowRef === expectedReleaseRef ||
    (parsedVersion.channel === "alpha" &&
      /^refs\/heads\/tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u.test(
        workflowRef ?? "",
      ));

  if (
    workflow?.repository !== NPM_PROVENANCE_REPOSITORY ||
    workflow?.path !== NPM_PROVENANCE_WORKFLOW_PATH ||
    !isTrustedRef ||
    statement.predicate?.runDetails?.builder?.id !== NPM_PROVENANCE_BUILDER_ID
  ) {
    throw new Error(
      `npm provenance attestation does not bind ${version} to the trusted OpenClaw GitHub release workflow.`,
    );
  }

  return {
    certificateIssuer: NPM_PROVENANCE_CERTIFICATE_ISSUER,
    certificateIdentityURI: `${NPM_PROVENANCE_REPOSITORY}/${NPM_PROVENANCE_WORKFLOW_PATH}@${workflowRef}`,
  };
}

async function verifySigstoreNpmProvenanceBundle(
  bundle: unknown,
  policy: NpmProvenanceVerificationPolicy,
): Promise<void> {
  const sigstore = require("sigstore") as { verify: VerifyNpmProvenanceBundle };
  await sigstore.verify(bundle, policy);
}

export async function verifyNpmProvenanceAttestation(params: {
  attestations: NpmRegistryAttestation[];
  integrity: string;
  packageName: string;
  verifyBundle?: VerifyNpmProvenanceBundle;
  version: string;
}): Promise<void> {
  const expectedSubject = `pkg:npm/${params.packageName}@${params.version}`;
  const expectedSha512 = Buffer.from(params.integrity.slice("sha512-".length), "base64").toString(
    "hex",
  );
  const verifyBundle = params.verifyBundle ?? verifySigstoreNpmProvenanceBundle;
  let verificationError: unknown;
  let policyError: unknown;

  for (const attestation of params.attestations) {
    if (attestation.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE) {
      continue;
    }
    const payload = attestation.bundle?.dsseEnvelope?.payload;
    if (!payload) {
      continue;
    }
    try {
      const statement = JSON.parse(
        Buffer.from(payload, "base64").toString("utf8"),
      ) as NpmProvenanceStatement;
      if (
        statement.subject?.some(
          (subject) =>
            subject.name === expectedSubject && subject.digest?.sha512 === expectedSha512,
        )
      ) {
        let policy: NpmProvenanceVerificationPolicy;
        try {
          policy = resolveNpmProvenanceVerificationPolicy(statement, params.version);
        } catch (error) {
          policyError = error;
          continue;
        }
        try {
          await verifyBundle(attestation.bundle, policy);
          return;
        } catch (error) {
          verificationError = error;
        }
      }
    } catch {
      // Try the remaining attestations before reporting the missing match.
    }
  }

  if (verificationError) {
    throw new Error(
      `npm provenance attestation failed Sigstore verification for ${params.packageName}@${params.version}: ${formatErrorMessage(verificationError)}`,
    );
  }

  if (policyError instanceof Error) {
    throw policyError;
  }
  if (policyError) {
    throw new Error(
      `npm provenance attestation policy evaluation failed for ${params.packageName}@${params.version}: ${formatErrorMessage(policyError)}`,
    );
  }

  throw new Error(
    `npm provenance attestation does not match ${params.packageName}@${params.version} and its registry integrity.`,
  );
}

export function collectInstalledPackageErrors(params: {
  expectedVersion: string;
  installedVersion: string;
  packageRoot: string;
}): string[] {
  const errors: string[] = [];
  const installedVersion = normalizeInstalledBinaryVersion(params.installedVersion);

  if (installedVersion !== params.expectedVersion) {
    errors.push(
      `installed package version mismatch: expected ${params.expectedVersion}, found ${params.installedVersion || "<missing>"}.`,
    );
  }

  for (const relativePath of collectInstalledBundledRuntimeSidecarPaths(params.packageRoot)) {
    if (!existsSync(join(params.packageRoot, relativePath))) {
      errors.push(`installed package is missing required bundled runtime sidecar: ${relativePath}`);
    }
  }

  errors.push(...collectInstalledBundledExtensionManifestErrors(params.packageRoot));
  errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot));
  errors.push(...collectInstalledPluginSdkZodArtifactErrors(params.packageRoot));
  errors.push(...collectInstalledPluginSdkDeclarationErrors(params.packageRoot));
  errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));

  return errors;
}

function collectInstalledBundledExtensionIds(packageRoot: string): Set<string> {
  const extensionsDir = join(packageRoot, "dist", "extensions");
  if (!existsSync(extensionsDir)) {
    return new Set();
  }
  const ids = new Set<string>();
  for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
    if (!entry.isDirectory()) {
      continue;
    }
    if (existsSync(join(extensionsDir, entry.name, "package.json"))) {
      ids.add(entry.name);
    }
  }
  return ids;
}

export function collectInstalledBundledRuntimeSidecarPaths(packageRoot: string): string[] {
  const installedExtensionIds = collectInstalledBundledExtensionIds(packageRoot);
  return PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS.filter((relativePath) => {
    const match = /^dist\/extensions\/([^/]+)\//u.exec(relativePath);
    return match !== null && installedExtensionIds.has(match[1]);
  });
}

export function collectInstalledBundledExtensionManifestErrors(packageRoot: string): string[] {
  return readBundledExtensionPackageJsons(packageRoot).errors;
}

export function normalizeInstalledBinaryVersion(output: string): string {
  const trimmed = output.trim();
  const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-(?:alpha|beta)\.\d+)?\b/u.exec(trimmed);
  return versionMatch?.[0] ?? trimmed;
}

function listDistJavaScriptFiles(
  packageRoot: string,
  opts: {
    maxFiles?: number;
    skipRelativePath?: (relativePath: string) => boolean;
  } = {},
): DistJavaScriptFileListResult {
  const distDir = join(packageRoot, "dist");
  if (!existsSync(distDir)) {
    return { files: [], limitExceeded: false };
  }

  const pending = [distDir];
  const files: string[] = [];
  while (pending.length > 0) {
    const currentDir = pending.pop();
    if (!currentDir) {
      continue;
    }
    const dir = opendirSync(currentDir);
    try {
      while (true) {
        const entry = dir.readSync();
        if (!entry) {
          break;
        }

        const entryPath = join(currentDir, entry.name);
        const relativePath = relative(distDir, entryPath).replaceAll("\\", "/");
        if (opts.skipRelativePath?.(relativePath)) {
          continue;
        }
        if (entry.isDirectory()) {
          pending.push(entryPath);
          continue;
        }
        if (entry.isFile() && ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE.test(entry.name)) {
          files.push(entryPath);
          if (opts.maxFiles !== undefined && files.length > opts.maxFiles) {
            return {
              files,
              limit: opts.maxFiles,
              limitExceeded: true,
            };
          }
        }
      }
    } finally {
      dir.closeSync();
    }
  }

  return { files, limitExceeded: false };
}

function formatInstalledDistFileScanLimitError(scope: string, limit: number): string {
  return `installed package ${scope} contains more than ${limit} JavaScript files; refusing to scan unbounded package contents.`;
}

export function collectInstalledContextEngineRuntimeErrors(packageRoot: string): string[] {
  const errors: string[] = [];
  const distFiles = listDistJavaScriptFiles(packageRoot, {
    maxFiles: MAX_INSTALLED_ROOT_DIST_JS_FILES,
  });
  if (distFiles.limitExceeded) {
    return [formatInstalledDistFileScanLimitError("dist", distFiles.limit)];
  }

  for (const filePath of distFiles.files) {
    const contents = readFileSync(filePath, "utf8");
    if (contents.includes(LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER)) {
      errors.push(
        "installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.",
      );
      break;
    }
  }
  return errors;
}

function resolveInstalledDistRelativeImport(params: {
  distRoot: string;
  importerPath: string;
  specifier: string;
}): string | null {
  if (!params.specifier.startsWith(".")) {
    return null;
  }

  const candidatePath = join(dirname(params.importerPath), params.specifier);
  const candidatePaths = [
    candidatePath,
    `${candidatePath}.js`,
    `${candidatePath}.mjs`,
    `${candidatePath}.cjs`,
    join(candidatePath, "index.js"),
    join(candidatePath, "index.mjs"),
    join(candidatePath, "index.cjs"),
  ];

  for (const resolvedPath of candidatePaths) {
    const relativePath = relative(params.distRoot, resolvedPath);
    if (
      relativePath.length === 0 ||
      relativePath.startsWith("..") ||
      isAbsolute(relativePath) ||
      !existsSync(resolvedPath)
    ) {
      continue;
    }
    return resolvedPath;
  }

  return null;
}

export function collectInstalledPluginSdkZodArtifactErrors(packageRoot: string): string[] {
  const distRoot = join(packageRoot, "dist");
  const entryRelativePath = "dist/plugin-sdk/zod.js";
  const entryPath = join(packageRoot, entryRelativePath);
  const pending = [entryPath];
  const visited = new Set<string>();

  while (pending.length > 0) {
    const filePath = pending.pop();
    if (!filePath || visited.has(filePath)) {
      continue;
    }
    visited.add(filePath);

    if (!existsSync(filePath)) {
      return [`installed package is missing required plugin SDK artifact: ${entryRelativePath}`];
    }

    const relativePath = relative(packageRoot, filePath).replaceAll("\\", "/");
    const fileStat = lstatSync(filePath);
    if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
      return [
        `installed package plugin SDK artifact '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
      ];
    }

    const source = readFileSync(filePath, "utf8");
    const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
    if (!parsedSpecifiers.ok) {
      return [
        `installed package plugin SDK artifact '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
      ];
    }

    for (const specifier of parsedSpecifiers.specifiers) {
      if (specifier === "zod" || specifier.startsWith("zod/")) {
        return [
          `installed package plugin SDK zod artifact must be self-contained but ${relativePath} imports ${specifier}.`,
        ];
      }

      const resolvedPath = resolveInstalledDistRelativeImport({
        distRoot,
        importerPath: filePath,
        specifier,
      });
      if (resolvedPath) {
        pending.push(resolvedPath);
      }
    }
  }

  return [];
}

export function collectInstalledPluginSdkDeclarationErrors(packageRoot: string): string[] {
  const pluginSdkDistRoot = join(packageRoot, "dist", "plugin-sdk");
  const errors: string[] = [];
  const forbiddenPrivateWorkspaceSpecifiers = ["@openclaw/llm-core"];

  if (!existsSync(pluginSdkDistRoot)) {
    return [];
  }

  for (const entry of readdirSync(pluginSdkDistRoot, { withFileTypes: true })) {
    if (!entry.isFile() || !entry.name.endsWith(".d.ts")) {
      continue;
    }

    const relativePath = `dist/plugin-sdk/${entry.name}`;
    const content = readFileSync(join(pluginSdkDistRoot, entry.name), "utf8");
    for (const specifier of forbiddenPrivateWorkspaceSpecifiers) {
      if (content.includes(`"${specifier}`) || content.includes(`'${specifier}`)) {
        errors.push(
          `installed package plugin SDK declaration '${relativePath}' references private workspace package ${specifier}.`,
        );
      }
    }
  }

  return errors;
}

function listInstalledRootDistJavaScriptFiles(packageRoot: string): DistJavaScriptFileListResult {
  return listDistJavaScriptFiles(packageRoot, {
    maxFiles: MAX_INSTALLED_ROOT_DIST_JS_FILES,
    skipRelativePath: (relativePath) =>
      relativePath === "extensions" || relativePath.startsWith("extensions/"),
  });
}

type ParsedImportSpecifiersResult =
  | { ok: true; specifiers: Set<string> }
  | { ok: false; error: string };

function extractLiteralSpecifier(node: unknown): string | null {
  if (!node || typeof node !== "object") {
    return null;
  }
  const candidate = node as { type?: string; value?: unknown };
  if (candidate.type === "Literal" && typeof candidate.value === "string") {
    return candidate.value;
  }
  return null;
}

function extractJavaScriptImportSpecifiers(source: string): ParsedImportSpecifiersResult {
  const specifiers = new Set<string>();
  let program: unknown;
  try {
    program = acorn.parse(source, {
      allowHashBang: true,
      ecmaVersion: "latest",
      sourceType: "module",
    });
  } catch (error) {
    return { ok: false, error: formatErrorMessage(error) };
  }

  const visited = new Set<unknown>();
  const pending: unknown[] = [program];
  while (pending.length > 0) {
    const current = pending.pop();
    if (!current || typeof current !== "object" || visited.has(current)) {
      continue;
    }
    visited.add(current);
    const node = current as Record<string, unknown>;
    const nodeType = typeof node.type === "string" ? node.type : null;

    if (nodeType === "ImportDeclaration") {
      const specifier = extractLiteralSpecifier(node.source);
      if (specifier) {
        specifiers.add(specifier);
      }
    } else if (nodeType === "ExportAllDeclaration" || nodeType === "ExportNamedDeclaration") {
      const specifier = extractLiteralSpecifier(node.source);
      if (specifier) {
        specifiers.add(specifier);
      }
    } else if (nodeType === "ImportExpression") {
      const specifier = extractLiteralSpecifier(node.source);
      if (specifier) {
        specifiers.add(specifier);
      }
    } else if (nodeType === "CallExpression") {
      const callee = node.callee as { type?: string; name?: string } | undefined;
      const args = Array.isArray(node.arguments) ? node.arguments : [];
      if (callee?.type === "Identifier" && callee.name === "require" && args.length === 1) {
        const specifier = extractLiteralSpecifier(args[0]);
        if (specifier) {
          specifiers.add(specifier);
        }
      }
    }

    for (const value of Object.values(node)) {
      if (Array.isArray(value)) {
        pending.push(...value);
      } else if (value && typeof value === "object") {
        pending.push(value);
      }
    }
  }

  return { ok: true, specifiers };
}

export function collectInstalledRootDependencyManifestErrors(packageRoot: string): string[] {
  const packageJsonPath = join(packageRoot, "package.json");
  if (!existsSync(packageJsonPath)) {
    return ["installed package is missing package.json."];
  }
  const packageJsonStat = lstatSync(packageJsonPath);
  if (!packageJsonStat.isFile() || packageJsonStat.size > MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES) {
    return [
      `installed package.json is invalid or exceeds ${MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES} bytes.`,
    ];
  }
  let rootPackageJson: InstalledPackageJson;
  try {
    rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
  } catch (error) {
    return [`installed package.json could not be parsed: ${formatErrorMessage(error)}.`];
  }
  const declaredRuntimeDeps = new Set([
    ...Object.keys(rootPackageJson.dependencies ?? {}),
    ...Object.keys(rootPackageJson.optionalDependencies ?? {}),
  ]);
  const distFiles = listInstalledRootDistJavaScriptFiles(packageRoot);
  if (distFiles.limitExceeded) {
    return [formatInstalledDistFileScanLimitError("root dist", distFiles.limit)];
  }
  const missingImporters = new Map<string, Set<string>>();
  const bundledExtensionRuntimeDependencyOwners =
    collectBundledExtensionRuntimeDependencyOwners(packageRoot);

  for (const filePath of distFiles.files) {
    const fileStat = lstatSync(filePath);
    if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
      const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
      return [
        `installed package root dist file '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
      ];
    }
    const source = readFileSync(filePath, "utf8");
    const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
    const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
    if (!parsedSpecifiers.ok) {
      return [
        `installed package root dist file '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
      ];
    }
    for (const specifier of parsedSpecifiers.specifiers) {
      const dependencyName = packageNameFromSpecifier(specifier);
      if (
        !dependencyName ||
        NODE_BUILTIN_MODULES.has(dependencyName) ||
        OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS.has(dependencyName) ||
        declaredRuntimeDeps.has(dependencyName) ||
        isBundledExtensionOwnedRuntimeImport({
          dependencyName,
          ownersByDependency: bundledExtensionRuntimeDependencyOwners,
          source,
        })
      ) {
        continue;
      }
      const importers = missingImporters.get(dependencyName) ?? new Set<string>();
      importers.add(relativePath);
      missingImporters.set(dependencyName, importers);
    }
  }

  return [...missingImporters.entries()]
    .map(([dependencyName, importers]) => {
      const importerList = [...importers].toSorted((left, right) => left.localeCompare(right));
      return `installed package root is missing declared runtime dependency '${dependencyName}' for dist importers: ${importerList.join(", ")}. Add it to package.json dependencies/optionalDependencies.`;
    })
    .toSorted((left, right) => left.localeCompare(right));
}

function collectBundledExtensionRuntimeDependencyOwners(
  packageRoot: string,
): Map<string, Set<string>> {
  const ownersByDependency = new Map<string, Set<string>>();
  const { manifests } = readBundledExtensionPackageJsons(packageRoot);
  for (const { id, manifest } of manifests) {
    for (const dependencyName of collectRuntimeDependencySpecs(manifest).keys()) {
      const owners = ownersByDependency.get(dependencyName) ?? new Set<string>();
      owners.add(id);
      ownersByDependency.set(dependencyName, owners);
    }
  }
  return ownersByDependency;
}

function isBundledExtensionOwnedRuntimeImport(params: {
  dependencyName: string;
  ownersByDependency: Map<string, Set<string>>;
  source: string;
}): boolean {
  const owners = params.ownersByDependency.get(params.dependencyName);
  if (!owners) {
    return false;
  }
  return [...owners].some((pluginId) =>
    params.source.includes(`//#region extensions/${pluginId}/`),
  );
}

export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
  return platform === "win32"
    ? pathWin32.join(prefixDir, "openclaw.cmd")
    : pathPosix.join(prefixDir, "bin", "openclaw");
}

export function resolveInstalledBinaryCommandInvocation(
  prefixDir: string,
  args: string[],
  params: { comSpec?: string; platform?: NodeJS.Platform } = {},
): {
  args: string[];
  command: string;
  windowsVerbatimArguments?: boolean;
} {
  const platform = params.platform ?? process.platform;
  const binaryPath = resolveInstalledBinaryPath(prefixDir, platform);
  if (platform === "win32") {
    return {
      command: params.comSpec ?? resolveWindowsCmdExePath(),
      args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, args)],
      windowsVerbatimArguments: true,
    };
  }

  return {
    command: binaryPath,
    args,
  };
}

function collectExpectedBundledExtensionPackageIds(): ReadonlySet<string> {
  const ids = new Set<string>();
  for (const relativePath of listBundledPluginPackArtifacts()) {
    const match = /^dist\/extensions\/([^/]+)\/package\.json$/u.exec(relativePath);
    if (match) {
      ids.add(match[1]);
    }
  }
  return ids;
}

function readBundledExtensionPackageJsons(packageRoot: string): {
  manifests: InstalledBundledExtensionManifestRecord[];
  errors: string[];
} {
  const extensionsDir = join(packageRoot, "dist", "extensions");
  if (!existsSync(extensionsDir)) {
    return { manifests: [], errors: [] };
  }

  const manifests: InstalledBundledExtensionManifestRecord[] = [];
  const errors: string[] = [];
  const expectedPackageIds = collectExpectedBundledExtensionPackageIds();

  for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
    if (!entry.isDirectory()) {
      continue;
    }

    const extensionDirPath = join(extensionsDir, entry.name);
    const packageJsonPath = join(extensionsDir, entry.name, "package.json");
    if (!existsSync(packageJsonPath)) {
      if (expectedPackageIds.has(entry.name)) {
        errors.push(`installed bundled extension manifest missing: ${packageJsonPath}.`);
      }
      continue;
    }

    try {
      const packageJsonStats = lstatSync(packageJsonPath);
      if (!packageJsonStats.isFile()) {
        throw new Error("manifest must be a regular file");
      }
      if (packageJsonStats.size > MAX_BUNDLED_EXTENSION_MANIFEST_BYTES) {
        throw new Error(`manifest exceeds ${MAX_BUNDLED_EXTENSION_MANIFEST_BYTES} bytes`);
      }

      const realExtensionDirPath = realpathSync(extensionDirPath);
      const realPackageJsonPath = realpathSync(packageJsonPath);
      const relativeManifestPath = relative(realExtensionDirPath, realPackageJsonPath);
      if (
        relativeManifestPath.length === 0 ||
        relativeManifestPath.startsWith("..") ||
        isAbsolute(relativeManifestPath)
      ) {
        throw new Error("manifest resolves outside the bundled extension directory");
      }

      manifests.push({
        id: entry.name,
        manifest: JSON.parse(
          readFileSync(realPackageJsonPath, "utf8"),
        ) as InstalledBundledExtensionPackageJson,
        path: realPackageJsonPath,
      });
    } catch (error) {
      errors.push(
        `installed bundled extension manifest invalid: failed to parse ${packageJsonPath}: ${formatErrorMessage(error)}.`,
      );
    }
  }

  return { manifests, errors };
}

function npmExec(args: string[], cwd: string): string {
  const invocation = resolveNpmCommandInvocation({
    npmArgs: args,
    npmExecPath: process.env.npm_execpath,
    nodeExecPath: process.execPath,
    platform: process.platform,
  });

  return runNpmVerifyCommand(invocation, cwd);
}

function resolveGlobalRoot(prefixDir: string, cwd: string): string {
  return npmExec(["root", "-g", "--prefix", prefixDir], cwd);
}

export function buildPublishedInstallCommandArgs(prefixDir: string, spec: string): string[] {
  return ["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"];
}

function installSpec(prefixDir: string, spec: string, cwd: string): void {
  npmExec(buildPublishedInstallCommandArgs(prefixDir, spec), cwd);
}

export async function fetchRegistryJson(
  url: string,
  options: FetchRegistryJsonOptions = {},
): Promise<unknown> {
  const timeoutMs = options.timeoutMs ?? NPM_REGISTRY_REQUEST_TIMEOUT_MS;
  const maxBodyBytes = options.maxBodyBytes ?? NPM_REGISTRY_RESPONSE_BODY_MAX_BYTES;
  const controller = new AbortController();
  const timeoutError = Object.assign(
    new Error(`npm registry request timed out after ${timeoutMs}ms: ${url}`),
    { code: "ETIMEDOUT" },
  );
  let timeout: ReturnType<typeof setTimeout> | undefined;
  const timeoutPromise = new Promise<never>((_resolve, reject) => {
    timeout = setTimeout(() => {
      controller.abort(timeoutError);
      reject(timeoutError);
    }, timeoutMs);
    timeout.unref?.();
  });
  try {
    const response = await Promise.race([
      (options.fetchImpl ?? fetch)(url, {
        headers: {
          Accept: "application/json",
        },
        redirect: "error",
        signal: controller.signal,
      }),
      timeoutPromise,
    ]);
    if (!response.ok) {
      throw new Error(`npm registry request failed (${response.status}): ${url}`);
    }
    return JSON.parse(
      await readBoundedResponseText(response, `npm registry ${url}`, maxBodyBytes, {
        signal: controller.signal,
        timeoutPromise,
      }),
    );
  } finally {
    clearTimeout(timeout);
  }
}

function isRetryableRegistryProvenanceError(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  return (
    /npm registry request failed \((?:404|408|425|429|5\d\d)\)/u.test(message) ||
    message.includes("npm registry metadata is incomplete") ||
    message.includes("npm registry provenance metadata is incomplete") ||
    /aborted|fetch failed|network|timeout|timed out/u.test(message)
  );
}

export async function retryNpmRegistryProvenanceRead<T>(
  read: () => Promise<T>,
  options: {
    attempts?: number;
    delay?: (delayMs: number) => Promise<void>;
  } = {},
): Promise<T> {
  const attempts = options.attempts ?? NPM_REGISTRY_PROVENANCE_ATTEMPTS;
  const delay =
    options.delay ??
    ((delayMs: number) =>
      new Promise<void>((resolveDelay) => {
        setTimeout(resolveDelay, delayMs);
      }));
  let lastError: unknown;

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    try {
      return await read();
    } catch (error) {
      lastError = error;
      if (!isRetryableRegistryProvenanceError(error) || attempt === attempts) {
        throw error;
      }
      await delay(Math.min(attempt * 1000, NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS));
    }
  }

  throw lastError;
}

async function verifyPublishedRegistryProvenanceOnce(version: string): Promise<void> {
  const registry = new URL(process.env.npm_config_registry ?? "https://registry.npmjs.org");
  if (registry.protocol !== "https:") {
    throw new Error(`npm registry must use HTTPS: ${registry}`);
  }
  if (!registry.pathname.endsWith("/")) {
    registry.pathname = `${registry.pathname}/`;
  }
  const packageName = "openclaw";
  const packageDocument = (await fetchRegistryJson(
    new URL(
      `${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
      registry,
    ).toString(),
  )) as {
    dist?: {
      attestations?: {
        provenance?: {
          predicateType?: string;
        };
        url?: string;
      };
      integrity?: string;
      signatures?: NpmRegistrySignature[];
    };
  };
  const keysDocument = (await fetchRegistryJson(new URL("-/npm/v1/keys", registry).toString())) as {
    keys?: NpmRegistryKey[];
  };
  const integrity = packageDocument.dist?.integrity;
  const signatures = packageDocument.dist?.signatures;
  const provenance = packageDocument.dist?.attestations?.provenance;
  const attestationUrl = packageDocument.dist?.attestations?.url;
  if (!integrity || !signatures || !keysDocument.keys) {
    throw new Error(`npm registry metadata is incomplete for ${packageName}@${version}.`);
  }
  if (
    provenance?.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE ||
    typeof attestationUrl !== "string" ||
    attestationUrl.length === 0
  ) {
    throw new Error(
      `npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
    );
  }
  const parsedAttestationUrl = new URL(attestationUrl);
  const attestationPathPrefix = new URL("-/npm/v1/attestations/", registry).pathname;
  if (
    parsedAttestationUrl.protocol !== "https:" ||
    parsedAttestationUrl.origin !== registry.origin ||
    !parsedAttestationUrl.pathname.startsWith(attestationPathPrefix)
  ) {
    throw new Error(
      `npm registry returned an untrusted provenance attestation URL for ${packageName}@${version}.`,
    );
  }

  verifyNpmRegistrySignatures({
    packageName,
    version,
    integrity,
    signatures,
    keys: keysDocument.keys,
  });
  const attestationDocument = (await fetchRegistryJson(parsedAttestationUrl.toString())) as {
    attestations?: NpmRegistryAttestation[];
  };
  const attestations = attestationDocument.attestations ?? [];
  if (attestations.length === 0) {
    throw new Error(
      `npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
    );
  }
  await verifyNpmProvenanceAttestation({
    packageName,
    version,
    integrity,
    attestations,
  });
  console.log(
    `openclaw-npm-postpublish-verify: registry signature and provenance attestation verified (${version})`,
  );
}

async function verifyPublishedRegistryProvenance(version: string): Promise<void> {
  await retryNpmRegistryProvenanceRead(() => verifyPublishedRegistryProvenanceOnce(version));
}

function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
  const invocation = resolveInstalledBinaryCommandInvocation(prefixDir, ["--version"]);
  return runNpmVerifyCommand(invocation, cwd);
}

function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
  const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
  const prefixDir = join(workingDir, "prefix");

  try {
    for (const spec of scenario.installSpecs) {
      installSpec(prefixDir, spec, workingDir);
    }

    const globalRoot = resolveGlobalRoot(prefixDir, workingDir);
    const packageRoot = join(globalRoot, "openclaw");
    const pkg = JSON.parse(
      readFileSync(join(packageRoot, "package.json"), "utf8"),
    ) as InstalledPackageJson;
    const errors = collectInstalledPackageErrors({
      expectedVersion: scenario.expectedVersion,
      installedVersion: pkg.version?.trim() ?? "",
      packageRoot,
    });
    const installedBinaryVersion = readInstalledBinaryVersion(prefixDir, workingDir);

    if (normalizeInstalledBinaryVersion(installedBinaryVersion) !== scenario.expectedVersion) {
      errors.push(
        `installed openclaw binary version mismatch: expected ${scenario.expectedVersion}, found ${installedBinaryVersion || "<missing>"}.`,
      );
    }

    if (errors.length === 0) {
      runInstalledWorkspaceBootstrapSmoke({ packageRoot });
    }

    if (errors.length > 0) {
      throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`);
    }

    console.log(`openclaw-npm-postpublish-verify: ${scenario.name} OK (${version})`);
  } finally {
    rmSync(workingDir, { force: true, recursive: true });
  }
}

async function main(argv = process.argv.slice(2)): Promise<void> {
  const args = parseOpenClawNpmPostpublishVerifyArgs(argv);
  if (args.help) {
    console.log(openClawNpmPostpublishVerifyUsage());
    return;
  }

  const { version } = args;
  const scenarios = buildPublishedInstallScenarios(version);
  await verifyPublishedRegistryProvenance(version);
  for (const scenario of scenarios) {
    verifyScenario(version, scenario);
  }

  console.log(
    `openclaw-npm-postpublish-verify: verified published npm install paths for ${version}.`,
  );
}

const entrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entrypoint !== null && import.meta.url === entrypoint) {
  try {
    await main();
  } catch (error) {
    console.error(`openclaw-npm-postpublish-verify: ${formatErrorMessage(error)}`);
    process.exitCode = 1;
  }
}
