|
|
@@ -0,0 +1,135 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+
|
|
|
+import assert from "node:assert/strict";
|
|
|
+import { execFile } from "node:child_process";
|
|
|
+import { constants } from "node:fs";
|
|
|
+import { access, lstat, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
|
|
|
+import { tmpdir } from "node:os";
|
|
|
+import { isAbsolute, join, relative } from "node:path";
|
|
|
+import { promisify } from "node:util";
|
|
|
+
|
|
|
+const execFileAsync = promisify(execFile);
|
|
|
+const PACKAGE_NAME = "codex-app-server-bridge";
|
|
|
+const DEFAULT_CACHE = "/private/tmp/codex-app-server-bridge-npm-cache";
|
|
|
+const USAGE = "usage: npm run verify:git-package -- <git+https|git+ssh|git+file URL>#<vX.Y.Z|40-character-commit>";
|
|
|
+
|
|
|
+function selectedGitPackage(argv) {
|
|
|
+ if (argv.length !== 1) throw new Error(USAGE);
|
|
|
+ const packageSpec = argv[0];
|
|
|
+ const fragmentAt = packageSpec.lastIndexOf("#");
|
|
|
+ const repository = packageSpec.slice(0, fragmentAt);
|
|
|
+ const revision = packageSpec.slice(fragmentAt + 1);
|
|
|
+ const supportedRepository = /^(?:git\+https|git\+ssh|git\+file):\/\/.+\.git$/.test(repository);
|
|
|
+ const immutableRevision = /^(?:[0-9a-fA-F]{40}|v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.test(revision);
|
|
|
+ if (fragmentAt <= 0 || !supportedRepository || !immutableRevision) {
|
|
|
+ throw new Error("Git package must use a supported URL pinned to a full commit or release tag");
|
|
|
+ }
|
|
|
+ return { packageSpec, revision };
|
|
|
+}
|
|
|
+
|
|
|
+function isWithin(parent, child) {
|
|
|
+ const pathFromParent = relative(parent, child);
|
|
|
+ return pathFromParent === "" || (!pathFromParent.startsWith("..") && !isAbsolute(pathFromParent));
|
|
|
+}
|
|
|
+
|
|
|
+function commandExit(error) {
|
|
|
+ return typeof error === "object" && error !== null && "code" in error
|
|
|
+ ? String(error.code)
|
|
|
+ : "unknown";
|
|
|
+}
|
|
|
+
|
|
|
+async function installPackage(prefix, cache, packageSpec) {
|
|
|
+ try {
|
|
|
+ await execFileAsync("npm", [
|
|
|
+ "install",
|
|
|
+ "--global",
|
|
|
+ "--prefix",
|
|
|
+ prefix,
|
|
|
+ "--ignore-scripts",
|
|
|
+ "--cache",
|
|
|
+ cache,
|
|
|
+ packageSpec,
|
|
|
+ ], { maxBuffer: 10 * 1024 * 1024 });
|
|
|
+ } catch (error) {
|
|
|
+ throw new Error(`isolated npm installation failed with exit code ${commandExit(error)}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function installedPackage(prefix, cache) {
|
|
|
+ const packageRoot = join(prefix, "lib", "node_modules", PACKAGE_NAME);
|
|
|
+ const executable = join(prefix, "bin", PACKAGE_NAME);
|
|
|
+ await Promise.all([
|
|
|
+ access(executable, constants.X_OK),
|
|
|
+ access(join(packageRoot, "dist", "cli.js"), constants.R_OK),
|
|
|
+ access(join(packageRoot, "dist", "config.js"), constants.R_OK),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const packageMetadata = await lstat(packageRoot);
|
|
|
+ if (packageMetadata.isSymbolicLink()) {
|
|
|
+ throw new Error("installed package must not be a temporary link");
|
|
|
+ }
|
|
|
+
|
|
|
+ const [realPrefix, realCache, realPackage, realExecutable] = await Promise.all([
|
|
|
+ realpath(prefix),
|
|
|
+ realpath(cache),
|
|
|
+ realpath(packageRoot),
|
|
|
+ realpath(executable),
|
|
|
+ ]);
|
|
|
+ if (!isWithin(realPrefix, realPackage) || isWithin(realCache, realPackage)) {
|
|
|
+ throw new Error("installed package resolved outside the isolated prefix");
|
|
|
+ }
|
|
|
+ if (!isWithin(realPackage, realExecutable) || isWithin(realCache, realExecutable)) {
|
|
|
+ throw new Error("installed executable resolved outside the installed package");
|
|
|
+ }
|
|
|
+
|
|
|
+ const manifest = JSON.parse(await readFile(join(realPackage, "package.json"), "utf8"));
|
|
|
+ assert.equal(manifest.name, PACKAGE_NAME);
|
|
|
+ assert.deepEqual(manifest.bin, { [PACKAGE_NAME]: `bin/${PACKAGE_NAME}.js` });
|
|
|
+ return { executable, version: String(manifest.version) };
|
|
|
+}
|
|
|
+
|
|
|
+async function verifyExecutable(executable) {
|
|
|
+ try {
|
|
|
+ await execFileAsync(executable, ["--unsupported"], {
|
|
|
+ env: process.env,
|
|
|
+ maxBuffer: 1024 * 1024,
|
|
|
+ });
|
|
|
+ throw new Error("installed executable unexpectedly accepted an unsupported invocation");
|
|
|
+ } catch (error) {
|
|
|
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== 1) {
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ const stdout = "stdout" in error ? String(error.stdout) : "";
|
|
|
+ const stderr = "stderr" in error ? String(error.stderr) : "";
|
|
|
+ assert.equal(stdout, "");
|
|
|
+ assert.match(stderr, /Usage: codex-app-server-bridge app-server/);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function verify() {
|
|
|
+ const { packageSpec, revision } = selectedGitPackage(process.argv.slice(2));
|
|
|
+ const prefix = await mkdtemp(join(tmpdir(), "codex-app-server-bridge-git-package-"));
|
|
|
+ const cache = process.env.CODEX_APP_SERVER_BRIDGE_NPM_CACHE ?? DEFAULT_CACHE;
|
|
|
+
|
|
|
+ try {
|
|
|
+ await installPackage(prefix, cache, packageSpec);
|
|
|
+ const installed = await installedPackage(prefix, cache);
|
|
|
+ await verifyExecutable(installed.executable);
|
|
|
+ process.stdout.write(`${JSON.stringify({
|
|
|
+ ok: true,
|
|
|
+ package: `${PACKAGE_NAME}@${installed.version}`,
|
|
|
+ revision,
|
|
|
+ executable: `bin/${PACKAGE_NAME}`,
|
|
|
+ storage: "isolated-prefix",
|
|
|
+ invocation: "verified",
|
|
|
+ })}\n`);
|
|
|
+ } finally {
|
|
|
+ await rm(prefix, { recursive: true, force: true });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+verify().catch((error) => {
|
|
|
+ const message = error instanceof Error ? error.message : String(error);
|
|
|
+ process.stderr.write(`Git-package verification failed: ${message}\n`);
|
|
|
+ process.exitCode = 1;
|
|
|
+});
|