DEV Community

VEILVM
VEILVM

Posted on

Pro API for JSVMP: call VEILVM's JS VM obfuscator from CI

I built VEILVM as a browser workspace that compiles JavaScript into a custom VM (JSVMP) — high-performance or dynamic mode — so small critical fragments are harder to reverse than with classic string/control-flow obfuscators.

Pro plans can now hit the same engines over HTTP, which is what you want for CI, batch jobs, or a private build step. API runs share the monthly quota with the web UI.

Quick facts (from the live docs)

  • POST https://vm.ldvmp.com/api/v1/obfuscate
  • Header: Authorization: Bearer veil_live_… (key shown once; up to 3 active keys per account)
  • Body: { "code": "<js>", "mode": "performance" | "dynamic" }
  • Max 2 MB source per request (UTF-8)
  • Success 200: { ok, output, inputBytes, outputBytes, ms, engine, engineVersion } plus X-Quota-Limit / X-Quota-Remaining
  • Plans: Try free 3 times a day, no sign-up; 20 runs a month after free signup; Pro $9/month or $79/year → 1,000 runs/month + API

Full reference: API documentation.

curl

curl -X POST https://vm.ldvmp.com/api/v1/obfuscate \
  -H "Authorization: Bearer $VEILVM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"console.log(\"hello\")","mode":"performance"}'
Enter fullscreen mode Exit fullscreen mode

Node.js 18+ with Retry-After on 503

Cold starts or engine timeouts return 503 with ENGINE_UNAVAILABLE or TIMEOUT. Those runs are not charged. Wait for the Retry-After header (seconds), then retry. 500 INTERNAL_ERROR is also refunded — retry later. 422 COMPILE_FAILED is refunded (bad source, not a transient fault).

// Node.js 18+ — keep the key in an env var, never in the repo
import { readFile, writeFile } from "node:fs/promises";

async function obfuscate(code, mode = "performance", { maxAttempts = 4 } = {}) {
  let attempt = 0;
  while (true) {
    attempt += 1;
    const response = await fetch("https://vm.ldvmp.com/api/v1/obfuscate", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VEILVM_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ code, mode }),
    });
    const data = await response.json();

    if (response.ok && data.ok) return data;

    const retriable =
      response.status === 503 ||
      response.status === 500 ||
      data.error === "ENGINE_UNAVAILABLE" ||
      data.error === "TIMEOUT" ||
      data.error === "INTERNAL_ERROR" ||
      data.error === "UNAVAILABLE" ||
      data.error === "QUOTA_UNAVAILABLE";

    if (!retriable || attempt >= maxAttempts) {
      throw new Error(`${response.status} ${data.error}: ${data.message}`);
    }

    const retryAfterHeader = response.headers.get("Retry-After");
    const waitSec = Number(retryAfterHeader);
    const delayMs =
      Number.isFinite(waitSec) && waitSec > 0
        ? waitSec * 1000
        : Math.min(8000, 1000 * 2 ** (attempt - 1));

    await new Promise((r) => setTimeout(r, delayMs));
  }
}

const code = await readFile("input.js", "utf8");
const data = await obfuscate(code, "dynamic");
await writeFile("output.js", data.output);
console.log(
  `${data.engine}: ${data.inputBytes} → ${data.outputBytes} bytes in ${data.ms} ms`
);
Enter fullscreen mode Exit fullscreen mode

Error cheat sheet

HTTP error Notes
401 INVALID_KEY Missing, malformed, revoked, or unknown
403 PLAN_REQUIRED Account is no longer Pro — keys die immediately on downgrade
422 COMPILE_FAILED Source would not compile — run refunded
429 QUOTA_EXCEEDED Body includes limit / used / remaining / resetAt
503 ENGINE_UNAVAILABLE / TIMEOUT Wait Retry-After seconds — run refunded
500 INTERNAL_ERROR Retry later — run refunded

Honest caveats

  • VM output is larger and slower; protect small high-value functions, not your whole bundle.
  • No client-side obfuscation is unbreakable.
  • Keep keys server-side; revoke from the Account page if one leaks.

If you try it, I’d love feedback on CI integration vs a future CLI. Links: workspace · pricing · docs.

Top comments (0)