autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user