From 2f49b8cba26f3ae035638cf4be4da1539d9bd4fa Mon Sep 17 00:00:00 2001 From: Alexander Zaidelson Date: Sat, 25 Apr 2026 23:40:29 +0300 Subject: [PATCH] Replace verify subcommands with passthrough to secretvm-verify --- package-lock.json | 232 +++++++++++++++++++++++++++- package.json | 2 +- src/cli.ts | 85 ++-------- src/commands/index.ts | 4 +- src/commands/verify/passthrough.ts | 212 +++++++++++++++++++++++++ src/commands/verify/proofOfCloud.ts | 52 ------- src/commands/verify/quote.ts | 84 ---------- src/commands/verify/utils.ts | 174 --------------------- src/commands/verify/workload.ts | 105 ------------- src/types.d.ts | 21 +-- 10 files changed, 453 insertions(+), 518 deletions(-) create mode 100644 src/commands/verify/passthrough.ts delete mode 100644 src/commands/verify/proofOfCloud.ts delete mode 100644 src/commands/verify/quote.ts delete mode 100644 src/commands/verify/utils.ts delete mode 100644 src/commands/verify/workload.ts diff --git a/package-lock.json b/package-lock.json index ee23783..88e8c25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "js-yaml": "^4.1.0", "node-forge": "^1.3.1", "open": "^10.1.2", - "secretvm-verification-sdk": "^0.1.0", + "secretvm-verify": "^0.8.4", "tough-cookie": "^4.x.x" }, "bin": { @@ -38,6 +38,12 @@ "node": ">=16" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, "node_modules/@colors/colors": { "version": "1.5.0", "license": "MIT", @@ -79,6 +85,54 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@teekit/qvl": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@teekit/qvl/-/qvl-0.0.4.tgz", + "integrity": "sha512-vd0KJwqajGJwHsmdj/2hyeQjmz3obXshOqQSOuvbbpv3NBiilioAE1GciWNxKuKZNhAf4DN+BxXOimWddJbuEw==", + "dependencies": { + "@scure/base": "^2.0.0", + "asn1js": "^3.0.5", + "pkijs": "^3.0.18", + "restructure": "^3.0.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "dev": true, @@ -169,6 +223,12 @@ "node": ">=0.4.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, "node_modules/agent-base": { "version": "7.1.3", "license": "MIT", @@ -218,6 +278,20 @@ "version": "2.0.1", "license": "Python-2.0" }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -310,6 +384,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "license": "MIT", @@ -545,6 +628,49 @@ "node": ">=0.8.0" } }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/external-editor": { "version": "3.1.0", "license": "MIT", @@ -968,6 +1094,35 @@ "node": ">=0.10.0" } }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -989,6 +1144,24 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/querystringify": { "version": "2.2.0", "license": "MIT" @@ -1020,6 +1193,12 @@ "node": ">=8" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.0.0", "license": "MIT", @@ -1066,10 +1245,19 @@ "version": "2.1.2", "license": "MIT" }, - "node_modules/secretvm-verification-sdk": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/secretvm-verification-sdk/-/secretvm-verification-sdk-0.1.0.tgz", - "integrity": "sha512-wI/C5GHkZ8VzQxgQetrtgpyH3yb8HLAsGLxwzewFgFozX28NisDOgzT3FWu6iLtd3nQ1ABJpQrlhVvfDaPVDfQ==" + "node_modules/secretvm-verify": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/secretvm-verify/-/secretvm-verify-0.8.4.tgz", + "integrity": "sha512-t0JwJWHv+jbKWRA9NILHOdBh582K2cce237rIXdIHWJcqNXtpRqT45SEzGQYAB9wpMkKnfjlAG3J1hOL3K6Cbw==", + "license": "MIT", + "dependencies": { + "@teekit/qvl": "^0.0.4", + "asn1js": "^3.0.5", + "ethers": "^6.16.0" + }, + "bin": { + "secretvm-verify": "dist/cli.js" + } }, "node_modules/signal-exit": { "version": "3.0.7", @@ -1209,9 +1397,20 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "6.19.8", - "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -1257,6 +1456,27 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yn": { "version": "3.1.1", "dev": true, diff --git a/package.json b/package.json index 91f4019..6566617 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "js-yaml": "^4.1.0", "node-forge": "^1.3.1", "open": "^10.1.2", - "secretvm-verification-sdk": "^0.1.0", + "secretvm-verify": "^0.8.4", "tough-cookie": "^4.x.x" }, "devDependencies": { diff --git a/src/cli.ts b/src/cli.ts index f1c23f9..29f0f1c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,9 +15,7 @@ import { vmStatusCommand, editVmCommand, listTemplatesCommand, - verifyQuoteCommand, - verifyWorkloadCommand, - verifyProofOfCloudCommand, + verifyCommand, } from "./commands"; import { GlobalOptions } from "./types"; import pkg from "../package.json"; @@ -201,84 +199,25 @@ async function main() { }); program.addCommand(vmCommands); - const verifyCommands = new Command("verify").description( - "Verification helpers (quote, workload, proof-of-cloud)", - ); - verifyCommands - .command("quote") - .description("Verify a SecretVM quote") - .option("--quote ", "Quote string (hex for TDX, base64 for SEV)") - .option("--quote-file ", "Path to a quote file") - .option("--vm-id ", "Fetch the quote from a VM ID") - .option( - "--attestation-type ", - "Override attestation type detection", - ) - .option( - "--environment ", - "Portal environment for verification", - ) - .option("--base-url ", "Custom base URL for verification") - .action(async (cmdOptions) => { - await verifyQuoteCommand( - cmdOptions, - program.opts() as GlobalOptions, - ); - }); - verifyCommands - .command("workload") - .description("Verify a docker-compose workload against a quote") - .option("--quote ", "Quote string (hex for TDX, base64 for SEV)") - .option("--quote-file ", "Path to a quote file") - .option("--vm-id ", "Fetch the quote from a VM ID") - .option( - "-d, --docker-compose ", - "Path to docker-compose.yaml", - ) - .option( - "--docker-files ", - "Path to docker files (used to compute dockerFilesSha256)", - ) - .option("--docker-files-sha256 ", "Docker files SHA-256") - .option( - "--attestation-type ", - "Override attestation type detection", - ) - .option( - "--environment ", - "Portal environment for verification", - ) - .option("--base-url ", "Custom base URL for verification") - .action(async (cmdOptions) => { - await verifyWorkloadCommand( - cmdOptions, - program.opts() as GlobalOptions, - ); - }); - verifyCommands - .command("proof-of-cloud") - .alias("poc") - .description("Verify Proof of Cloud from a TDX quote (hex)") - .option("--quote ", "TDX quote hex string") - .option("--quote-file ", "Path to a quote file") - .option("--vm-id ", "Fetch the quote from a VM ID") + program + .command("verify") + .description("Verify attestation (passthrough to secretvm-verify)") .option( - "--environment ", - "Portal environment for verification", + "--vm-id ", + "Resolve VM ID to hostname via portal before forwarding", ) - .option("--base-url ", "Custom base URL for verification") + .allowUnknownOption(true) + .allowExcessArguments(true) + .helpOption(false) .action(async (cmdOptions) => { - await verifyProofOfCloudCommand( - cmdOptions, - program.opts() as GlobalOptions, - ); + await verifyCommand(cmdOptions, program.opts() as GlobalOptions); }); - program.addCommand(verifyCommands); await program.parseAsync(process.argv); } main().catch((err) => { - console.error("Unhandled error in main execution:", err); + const message = err instanceof Error ? err.message : String(err); + console.error(`Error: ${message}`); process.exit(1); }); diff --git a/src/commands/index.ts b/src/commands/index.ts index 66f59b5..3cbce5b 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -11,6 +11,4 @@ export { startVmCommand } from "./vm/start"; export { vmStatusCommand } from "./vm/status"; export { editVmCommand } from "./vm/edit"; export { listTemplatesCommand } from "./vm/templates"; -export { verifyQuoteCommand } from "./verify/quote"; -export { verifyWorkloadCommand } from "./verify/workload"; -export { verifyProofOfCloudCommand } from "./verify/proofOfCloud"; +export { verifyCommand } from "./verify/passthrough"; diff --git a/src/commands/verify/passthrough.ts b/src/commands/verify/passthrough.ts new file mode 100644 index 0000000..1afcef9 --- /dev/null +++ b/src/commands/verify/passthrough.ts @@ -0,0 +1,212 @@ +import fs from "fs"; +import path from "path"; +import { spawn } from "child_process"; +import { getApiClient } from "../../services/apiClient"; +import { API_ENDPOINTS } from "../../constants"; +import { + GlobalOptions, + VerifyCommandOptions, + VmDetailsApiResponse, +} from "../../types"; + +const VERB_FLAGS_THAT_ACCEPT_VM = new Set([ + "--cpu", + "--tdx", + "--sev", + "--gpu", + "--resolve-version", + "-rv", + "--verify-workload", + "-vw", + "--compose", +]); + +const VERB_FLAGS_THAT_SPECIFY_TARGET = new Set([ + "--secretvm", + "--vm", + "--check-agent", + "--agent", +]); + +export async function verifyCommand( + cmdOptions: VerifyCommandOptions, + globalOptions: GlobalOptions, +): Promise { + const verifyIdx = process.argv.lastIndexOf("verify"); + const rawArgs = verifyIdx >= 0 ? process.argv.slice(verifyIdx + 1) : []; + + // Strip --vm-id and its value from the forwarded args. + // Handles both `--vm-id ` and `--vm-id=` forms. + const passthrough: string[] = []; + for (let i = 0; i < rawArgs.length; i++) { + if (rawArgs[i] === "--vm-id") { + i++; // skip value + continue; + } + if (rawArgs[i].startsWith("--vm-id=")) { + continue; + } + passthrough.push(rawArgs[i]); + } + + const { binPath, version } = resolveUpstream(); + + // No args, or --help / -h: print our note, then forward help with + // the upstream CLI name rewritten to "secretvm-cli verify" so the + // shown examples match what the user actually types. + const isHelp = + passthrough.length === 0 || + passthrough.some((a) => a === "--help" || a === "-h"); + if (isHelp) { + console.log( + `The commands are passed to secretvm-verify SDK, using version ${version} of secretvm-verify.`, + ); + const helpArgs = passthrough.length === 0 ? ["--help"] : passthrough; + await spawnUpstreamForHelp(binPath, helpArgs); + return; + } + + // --vm-id translation. + if (cmdOptions.vmId) { + for (const arg of passthrough) { + if (VERB_FLAGS_THAT_SPECIFY_TARGET.has(arg)) { + throw new Error( + `Cannot combine --vm-id with ${arg} (both specify the target VM).`, + ); + } + } + const hostname = await resolveVmHostname( + cmdOptions.vmId, + globalOptions, + ); + const hasVerbFlag = passthrough.some((a) => + VERB_FLAGS_THAT_ACCEPT_VM.has(a), + ); + if (hasVerbFlag) { + passthrough.push("--vm", hostname); + } else { + passthrough.push("--secretvm", hostname); + } + } + + await spawnUpstream(binPath, passthrough); +} + +function resolveUpstream(): { binPath: string; version: string } { + const pkgDir = findPackageDir(__dirname, "secretvm-verify"); + if (!pkgDir) { + throw new Error( + "Failed to locate the secretvm-verify package. Reinstall with `npm install secretvm-verify`.", + ); + } + const pkgJsonPath = path.join(pkgDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")); + const binRelative = + typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["secretvm-verify"]; + if (!binRelative) { + throw new Error( + `secretvm-verify package.json at ${pkgJsonPath} does not declare a bin.secretvm-verify entry.`, + ); + } + const binPath = path.resolve(pkgDir, binRelative); + if (!fs.existsSync(binPath)) { + throw new Error( + `secretvm-verify binary not found at ${binPath}. Try reinstalling the package.`, + ); + } + const version = typeof pkg.version === "string" ? pkg.version : "unknown"; + return { binPath, version }; +} + +function findPackageDir(startDir: string, packageName: string): string | null { + // Walk up from startDir looking for node_modules//package.json. + // Mirrors Node's own resolution for CJS packages, but works regardless of + // the target package's "exports" field (which Node's resolver otherwise + // honors, blocking access when only ESM conditions are declared). + let dir = startDir; + while (true) { + const candidate = path.join(dir, "node_modules", packageName); + if (fs.existsSync(path.join(candidate, "package.json"))) { + return candidate; + } + const parent = path.dirname(dir); + if (parent === dir) { + return null; + } + dir = parent; + } +} + +function spawnUpstream(binPath: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [binPath, ...args], { + stdio: "inherit", + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === null) { + reject(new Error("secretvm-verify terminated by signal")); + return; + } + if (code !== 0) { + process.exit(code); + } + resolve(); + }); + }); +} + +// Same as spawnUpstream but captures stdout so we can rewrite the upstream CLI +// name to "secretvm-cli verify" before printing. Used only for help output; +// regular verification calls keep stdio:inherit so the user sees them live. +function spawnUpstreamForHelp( + binPath: string, + args: string[], +): Promise { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [binPath, ...args], { + stdio: ["inherit", "pipe", "inherit"], + }); + let stdout = ""; + child.stdout?.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === null) { + reject(new Error("secretvm-verify terminated by signal")); + return; + } + const rewritten = stdout.replace( + /secretvm-verify/g, + "secretvm-cli verify", + ); + process.stdout.write(rewritten); + if (code !== 0) { + process.exit(code); + } + resolve(); + }); + }); +} + +async function resolveVmHostname( + vmId: string, + globalOptions: GlobalOptions, +): Promise { + const trimmedVmId = vmId.trim(); + if (!trimmedVmId) { + throw new Error("VM ID is required to resolve the hostname."); + } + const apiClient = await getApiClient(globalOptions); + const response = await apiClient.get( + API_ENDPOINTS.VM.DETAILS(trimmedVmId), + ); + const vmDomain = response.data?.vmDomain; + if (!vmDomain || vmDomain.trim() === "") { + throw new Error( + `VM "${trimmedVmId}" does not have a domain assigned. Use --secretvm directly.`, + ); + } + return vmDomain.trim(); +} diff --git a/src/commands/verify/proofOfCloud.ts b/src/commands/verify/proofOfCloud.ts deleted file mode 100644 index bd0cb5a..0000000 --- a/src/commands/verify/proofOfCloud.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GlobalOptions, VerifyProofOfCloudCommandOptions } from "../../types"; -import { handleCommandExecution, successResponse } from "../../utils"; -import type { VerifyProofOfCloudResult } from "secretvm-verification-sdk"; -import { createVerificationSdk, resolveQuoteInput } from "./utils"; - -export async function verifyProofOfCloudCommand( - cmdOptions: VerifyProofOfCloudCommandOptions, - globalOptions: GlobalOptions, -): Promise { - await handleCommandExecution( - globalOptions, - async (): Promise => { - const quoteHex = await resolveQuoteInput( - { - quote: cmdOptions.quote, - quoteFile: cmdOptions.quoteFile, - vmId: cmdOptions.vmId, - }, - globalOptions, - ); - const sdk = await createVerificationSdk(globalOptions, { - baseUrl: cmdOptions.baseUrl, - environment: cmdOptions.environment, - }); - return await sdk.VerifyProofOfCloud({ quoteHex }); - }, - (result: VerifyProofOfCloudResult) => { - if (!globalOptions.interactive) { - successResponse(result); - return; - } - - if (result.error) { - console.error( - `❌ Proof of Cloud verification failed: ${result.error}`, - ); - return; - } - - const verified = result.proof_of_cloud === true; - console.log( - `✅ Proof of Cloud: ${verified ? "Verified" : "Not verified"}`, - ); - if (result.origin) { - console.log(` Origin: ${result.origin}`); - } - if (result.machine_id) { - console.log(` Machine ID: ${result.machine_id}`); - } - }, - ); -} diff --git a/src/commands/verify/quote.ts b/src/commands/verify/quote.ts deleted file mode 100644 index d57b183..0000000 --- a/src/commands/verify/quote.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { GlobalOptions, VerifyQuoteCommandOptions } from "../../types"; -import { handleCommandExecution, successResponse } from "../../utils"; -import type { VerifyQuoteResult } from "secretvm-verification-sdk"; -import { - createVerificationSdk, - parseAttestationType, - resolveQuoteInput, -} from "./utils"; - -export async function verifyQuoteCommand( - cmdOptions: VerifyQuoteCommandOptions, - globalOptions: GlobalOptions, -): Promise { - const attestationType = parseAttestationType(cmdOptions.attestationType); - - await handleCommandExecution( - globalOptions, - async (): Promise => { - const quote = await resolveQuoteInput( - { - quote: cmdOptions.quote, - quoteFile: cmdOptions.quoteFile, - vmId: cmdOptions.vmId, - }, - globalOptions, - ); - const sdk = await createVerificationSdk(globalOptions, { - baseUrl: cmdOptions.baseUrl, - environment: cmdOptions.environment, - }); - return await sdk.VerifyQuote({ quote, attestationType }); - }, - (result: VerifyQuoteResult) => { - if (!globalOptions.interactive) { - successResponse(result); - return; - } - - if (result.ok) { - console.log( - `✅ Quote verified (${result.attestationType.toUpperCase()})`, - ); - if (result.vmType) { - console.log(` VM Type: ${result.vmType}`); - } - if (result.artifactsVersion) { - console.log(` Artifacts Version: ${result.artifactsVersion}`); - } - if (result.artifactsLink) { - console.log(` Artifacts Link: ${result.artifactsLink}`); - } - if (typeof result.proof_of_cloud === "boolean") { - console.log( - ` Proof of Cloud: ${result.proof_of_cloud ? "Yes" : "No"}`, - ); - } - if (result.origin) { - console.log(` Origin: ${result.origin}`); - } - if (result.warnings && result.warnings.length > 0) { - console.log(" Warnings:"); - result.warnings.forEach((warning) => - console.log(` - ${warning}`), - ); - } - } else { - console.error( - `❌ Quote verification failed: ${result.error || "Unknown error"}`, - ); - if (result.attestationType) { - console.error( - ` Attestation Type: ${result.attestationType}`, - ); - } - if (result.warnings && result.warnings.length > 0) { - console.error(" Warnings:"); - result.warnings.forEach((warning) => - console.error(` - ${warning}`), - ); - } - } - }, - ); -} diff --git a/src/commands/verify/utils.ts b/src/commands/verify/utils.ts deleted file mode 100644 index e0f373c..0000000 --- a/src/commands/verify/utils.ts +++ /dev/null @@ -1,174 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { getApiClient } from "../../services/apiClient"; -import { API_ENDPOINTS } from "../../constants"; -import { GlobalOptions } from "../../types"; -import type { - AttestationTypeInput, - SdkConfig, - SdkEnvironment, -} from "secretvm-verification-sdk"; - -type SecretVmSdkModule = typeof import("secretvm-verification-sdk"); - -const dynamicImport = new Function( - "modulePath", - "return import(modulePath);", -); - -export async function loadSecretVmSdk(): Promise { - return dynamicImport( - "secretvm-verification-sdk", - ) as Promise; -} - -export function ensureFetchAvailable(): void { - if (typeof globalThis.fetch !== "function") { - throw new Error( - "Fetch API not available. Use Node 18+ or provide a global fetch implementation.", - ); - } -} - -export function ensureWorkloadRuntime( - hasDockerFiles: boolean, - hasDockerFilesSha256: boolean, -): void { - if (typeof globalThis.FormData === "undefined") { - throw new Error( - "FormData is not available in this runtime. Use Node 18+.", - ); - } - if (typeof globalThis.Blob === "undefined") { - throw new Error("Blob is not available in this runtime. Use Node 18+."); - } - if (!globalThis.crypto?.subtle && hasDockerFiles && !hasDockerFilesSha256) { - throw new Error( - "WebCrypto is not available to hash docker files. Use Node 18+ or provide --docker-files-sha256.", - ); - } -} - -export function resolveSdkConfig( - globalOptions: GlobalOptions, - options: { baseUrl?: string; environment?: string }, -): SdkConfig { - const config: SdkConfig = {}; - if (options.baseUrl) { - config.baseUrl = options.baseUrl; - } else if (options.environment) { - config.environment = normalizeEnvironment(options.environment); - } else if (process.env.SERVER_BASE_URL) { - config.baseUrl = process.env.SERVER_BASE_URL; - } - - if (globalOptions.apiKey) { - config.apiKey = globalOptions.apiKey; - } - - return config; -} - -export function normalizeEnvironment( - input?: string, -): SdkEnvironment | undefined { - if (!input) return undefined; - const normalized = input.toLowerCase(); - if (normalized === "production" || normalized === "preview") { - return normalized as SdkEnvironment; - } - throw new Error( - `Invalid environment "${input}". Expected "production" or "preview".`, - ); -} - -export function parseAttestationType( - input?: string, -): AttestationTypeInput | undefined { - if (!input) return undefined; - const normalized = input.toLowerCase(); - if (normalized === "auto" || normalized === "tdx" || normalized === "sev") { - return normalized as AttestationTypeInput; - } - throw new Error( - `Invalid attestation type "${input}". Expected "auto", "tdx", or "sev".`, - ); -} - -export async function createVerificationSdk( - globalOptions: GlobalOptions, - options: { baseUrl?: string; environment?: string }, -) { - ensureFetchAvailable(); - const { createSecretVmSdk } = await loadSecretVmSdk(); - const config = resolveSdkConfig(globalOptions, options); - return createSecretVmSdk(config); -} - -export async function resolveQuoteInput( - options: { quote?: string; quoteFile?: string; vmId?: string }, - globalOptions: GlobalOptions, -): Promise { - const sources = [ - options.quote ? "quote" : null, - options.quoteFile ? "quoteFile" : null, - options.vmId ? "vmId" : null, - ].filter(Boolean); - - if (sources.length === 0) { - throw new Error("Provide one of --quote, --quote-file, or --vm-id."); - } - if (sources.length > 1) { - throw new Error( - "Provide only one of --quote, --quote-file, or --vm-id.", - ); - } - - if (options.quote) { - return options.quote.trim(); - } - - if (options.quoteFile) { - return readTextFile(options.quoteFile, "Quote file"); - } - - return await fetchQuoteFromVm(options.vmId!, globalOptions); -} - -function readTextFile(filePath: string, label: string): string { - const trimmedPath = filePath.trim(); - if (!trimmedPath) { - throw new Error(`${label} path cannot be empty.`); - } - const absolutePath = path.resolve(trimmedPath); - try { - fs.accessSync(absolutePath, fs.constants.R_OK); - } catch (err) { - throw new Error( - `${label} "${trimmedPath}" does not exist or is not readable.`, - ); - } - return fs.readFileSync(absolutePath, "utf-8").trim(); -} - -async function fetchQuoteFromVm( - vmId: string, - globalOptions: GlobalOptions, -): Promise { - const trimmedVmId = vmId.trim(); - if (!trimmedVmId) { - throw new Error("VM ID is required to fetch the quote."); - } - const apiClient = await getApiClient(globalOptions); - const response = await apiClient.get( - API_ENDPOINTS.VM.CPU_ATTESTATION(trimmedVmId), - ); - const data = response.data; - if (typeof data === "string") { - return data.trim(); - } - if (data && typeof data.quote === "string") { - return data.quote.trim(); - } - throw new Error("Unexpected attestation response format."); -} diff --git a/src/commands/verify/workload.ts b/src/commands/verify/workload.ts deleted file mode 100644 index 64bace3..0000000 --- a/src/commands/verify/workload.ts +++ /dev/null @@ -1,105 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { GlobalOptions, VerifyWorkloadCommandOptions } from "../../types"; -import { handleCommandExecution, successResponse } from "../../utils"; -import type { VerifyWorkloadResult } from "secretvm-verification-sdk"; -import { - createVerificationSdk, - ensureWorkloadRuntime, - parseAttestationType, - resolveQuoteInput, -} from "./utils"; - -export async function verifyWorkloadCommand( - cmdOptions: VerifyWorkloadCommandOptions, - globalOptions: GlobalOptions, -): Promise { - if (!cmdOptions.dockerCompose || cmdOptions.dockerCompose.trim() === "") { - throw new Error("Missing required option: --docker-compose"); - } - - const dockerComposePath = path.resolve(cmdOptions.dockerCompose.trim()); - try { - fs.accessSync(dockerComposePath, fs.constants.R_OK); - } catch (err) { - throw new Error( - `Docker compose file "${cmdOptions.dockerCompose}" does not exist or is not readable.`, - ); - } - - const dockerComposeContent = fs.readFileSync(dockerComposePath, "utf-8"); - const dockerComposeFilename = path.basename(dockerComposePath); - - let dockerFiles: Uint8Array | undefined; - if (cmdOptions.dockerFiles && cmdOptions.dockerFiles.trim() !== "") { - const dockerFilesPath = path.resolve(cmdOptions.dockerFiles.trim()); - try { - fs.accessSync(dockerFilesPath, fs.constants.R_OK); - } catch (err) { - throw new Error( - `Docker files "${cmdOptions.dockerFiles}" does not exist or is not readable.`, - ); - } - dockerFiles = fs.readFileSync(dockerFilesPath); - } - - const dockerFilesSha256 = cmdOptions.dockerFilesSha256?.trim(); - const attestationType = parseAttestationType(cmdOptions.attestationType); - - await handleCommandExecution( - globalOptions, - async (): Promise => { - ensureWorkloadRuntime(!!dockerFiles, !!dockerFilesSha256); - const quote = await resolveQuoteInput( - { - quote: cmdOptions.quote, - quoteFile: cmdOptions.quoteFile, - vmId: cmdOptions.vmId, - }, - globalOptions, - ); - const sdk = await createVerificationSdk(globalOptions, { - baseUrl: cmdOptions.baseUrl, - environment: cmdOptions.environment, - }); - return await sdk.VerifyWorkload({ - quote, - dockerCompose: dockerComposeContent, - dockerComposeFilename, - dockerFiles, - dockerFilesSha256, - attestationType, - }); - }, - (result: VerifyWorkloadResult) => { - if (!globalOptions.interactive) { - successResponse(result); - return; - } - - if (result.ok) { - console.log( - `✅ Workload verified (${result.attestationType.toUpperCase()})`, - ); - if (result.artifactsLink) { - console.log(` Artifacts Link: ${result.artifactsLink}`); - } - if (result.workloadResult?.message) { - console.log(` Message: ${result.workloadResult.message}`); - } - } else { - console.error( - `❌ Workload verification failed: ${result.error || "Unknown error"}`, - ); - if (result.quoteResult && !result.quoteResult.ok) { - console.error( - ` Quote error: ${result.quoteResult.error || "Unknown error"}`, - ); - } - if (result.workloadResult?.message) { - console.error(` Message: ${result.workloadResult.message}`); - } - } - }, - ); -} diff --git a/src/types.d.ts b/src/types.d.ts index bbb1979..de74a39 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -163,27 +163,8 @@ export interface EditVmCommandOptions { dockerRegistry?: string; } -export interface VerifyQuoteCommandOptions { - quote?: string; - quoteFile?: string; +export interface VerifyCommandOptions { vmId?: string; - attestationType?: string; - environment?: string; - baseUrl?: string; -} - -export interface VerifyWorkloadCommandOptions extends VerifyQuoteCommandOptions { - dockerCompose?: string; - dockerFiles?: string; - dockerFilesSha256?: string; -} - -export interface VerifyProofOfCloudCommandOptions { - quote?: string; - quoteFile?: string; - vmId?: string; - environment?: string; - baseUrl?: string; } export interface DockerCompose {