# 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) ## DGNLinks Case Study (2026-08-12) ### 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 ```python 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: ```javascript // 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.