Files
autoreg-skills-max/skills/account-validation/references/cloud-api-js-bundle-reverse-engineering.md

5.8 KiB

Cloud API Reverse-Engineering via JS Bundle

When a cloud provider's public API docs are incomplete or the API key scopes don't match what the console can do, download the SPA JavaScript bundle and extract the real API surface.

General Technique

  1. Download the JS bundle — the main entry chunk from the console/login page (e.g., /console/assets/index-*.js)
  2. Decode as UTF-8 — r.content.decode('utf-8', errors='ignore') — avoid r.text which can fail on binary chunks
  3. Grep for patterns:
    • baseURL / API_URL / apiUrl — the API client base URL
    • Se.get( / Se.post( / Se.put( / Se.patch( — actual API calls (name varies by bundler)
    • Authorization / Bearer / token — auth mechanism
    • interceptors.request.use — auth header injection
    • localStorage.getItem / sessionStorage.getItem — where tokens are stored
    • create({baseURL: — API client initialization
  4. Map the two-tier architecture — many providers use separate API surfaces:
    • Public API (/api/v1/...) — API key auth, limited endpoints
    • Console API (/api/console/v1/...) — session cookie auth, full admin access
  5. Test each endpoint with both auth mechanisms (API key Bearer vs session cookie)

Two-Tier Architecture

Layer Auth Base Path Endpoints
Public API API key (Bearer) /api/v1/ /account, /vms, /images
Console API Session cookie (React state) /api/console/v1/ /compute/vms, /compute/vms/{id}/access, /actions/vm/{action}, /orders, /billing/*, /network/*

Scope System

API keys have scopes that gate access to public API endpoints:

  • vm:read — can list VMs via GET /api/v1/vms
  • vm:write — can modify VMs (but NOT list them without vm:read)
  • account:read — can read account/org info via GET /api/v1/account
  • catalog:read — can read images/catalog via GET /api/v1/images

A key with vm:write but NOT vm:read gets 403 on GET /api/v1/vms. A key with vm:read but NOT account:read gets 403 on GET /api/v1/account.

Key Discovery from JS Bundle

import requests, re

r = requests.get("https://app.dgnlinks.com/console/assets/index-*.js", timeout=10)
js = r.content.decode('utf-8', errors='ignore')

# Find API client setup
for m in re.finditer(r'create\(\{baseURL:', js):
    ctx = js[m.start():m.start()+300]
    print(f"Client: {ctx}")

# Find all Se.get/Se.post calls
for m in re.finditer(r'Se\.(?:get|post|put|patch|delete)\s*\(\s*[`"\'/]([^`"\']+)[`"\']', js):
    print(f"Call: {m.group(1)}")

# Find auth interceptor
for m in re.finditer(r'interceptors\.request\.use', js):
    ctx = js[m.start():m.start()+500]
    print(f"Interceptor: {ctx}")

Console API Endpoints Discovered

From the JS bundle (index-BL-0-cnQ.js, 2.6 MB):

GET  /api/console/v1/me
GET  /api/console/v1/compute/vms
GET  /api/console/v1/compute/vms/{id}
GET  /api/console/v1/compute/vms/{id}/access
POST /api/console/v1/compute/vms/{id}/vnc/token
GET  /api/console/v1/compute/vms/{id}/metrics
GET  /api/console/v1/compute/vms/{id}/operations
POST /api/console/v1/actions/vm/{action}       # start, stop, reboot, delete
POST /api/console/v1/actions/vm/reinstall
POST /api/console/v1/actions/vm/auth/change     # change password/SSH keys
POST /api/console/v1/actions/vm/resize
POST /api/console/v1/actions/vm/delete/quote
POST /api/console/v1/actions/vm/tags
POST /api/console/v1/orders                     # create VM orders
GET  /api/console/v1/orders
GET  /api/console/v1/orders/{id}
POST /api/console/v1/orders/{id}/payments
POST /api/console/v1/orders/{id}/void
GET  /api/console/v1/billing/wallet
GET  /api/console/v1/catalog/images
GET  /api/console/v1/catalog/zones
GET  /api/console/v1/catalog/regions
GET  /api/console/v1/network/vpcs
GET  /api/console/v1/network/interfaces
GET  /api/console/v1/network/eips
GET  /api/console/v1/api-keys
GET  /api/console/v1/vm-templates
GET  /api/console/v1/sse-events?ticket={ticket}  # real-time SSE updates

Auth Mechanism

The console API uses React state for auth, not localStorage or cookies:

// From the interceptor
Se.interceptors.request.use(e => {
    const t = he.getState();  // React state store
    const n = AxiosHeaders.from(e.headers);
    const r = f_e(t, n.get("Authorization"), e.skipAuth);
    return r ? n.set("Authorization", r) : e.skipAuth && n.delete("Authorization"),
           e.headers = n, e;
});

The API key as Bearer token returns 401 invalid_or_expired_token on all /api/console/v1/ endpoints. The session token comes from the login form POST (email + password) which sets a session cookie, then the React app reads it into state.

VM Access Without vm:read

If you have vm:write but not vm:read:

  • Cannot list VMs via GET /api/v1/vms (403)
  • Cannot get VM details or access credentials
  • Cannot use console API without session cookie

Workaround: create a new API key with vm:read scope in the same org that has the VMs (via console → Profile → API Keys), OR log in via browser to get the session cookie for the console API.

Pitfalls

  • API key ≠ session token — the public API and console API use completely different auth. Don't expect API keys to work on console endpoints.
  • Scope mismatch is silent — a key with vm:read but no account:read can list VMs (200) but returns empty items: [] if the VMs are in a different org. The 403 only fires when accessing the /account endpoint directly.
  • HTML SPA on GET — GET /console/v1/compute/vms (without /api/ prefix) returns the React SPA HTML, not JSON. The actual API is at /api/console/v1/compute/vms.
  • JS bundle can be large — 2.6 MB minified. Use regex, not full parsing.
  • Bundler renames — function names (Se, ine, tne, cMe) are minified. Search for patterns, not specific names.