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
- Download the JS bundle — the main entry chunk from the console/login page (e.g.,
/console/assets/index-*.js) - Decode as UTF-8 —
r.content.decode('utf-8', errors='ignore')— avoidr.textwhich can fail on binary chunks - Grep for patterns:
baseURL/API_URL/apiUrl— the API client base URLSe.get(/Se.post(/Se.put(/Se.patch(— actual API calls (name varies by bundler)Authorization/Bearer/token— auth mechanisminterceptors.request.use— auth header injectionlocalStorage.getItem/sessionStorage.getItem— where tokens are storedcreate({baseURL:— API client initialization
- 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
- Public API (
- 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 viaGET /api/v1/vmsvm:write— can modify VMs (but NOT list them withoutvm:read)account:read— can read account/org info viaGET /api/v1/accountcatalog:read— can read images/catalog viaGET /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:readbut noaccount:readcan list VMs (200) but returns emptyitems: []if the VMs are in a different org. The 403 only fires when accessing the/accountendpoint 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.