diff --git a/DATTO_RMM_API_GUIDE.md b/DATTO_RMM_API_GUIDE.md new file mode 100644 index 0000000..56808c3 --- /dev/null +++ b/DATTO_RMM_API_GUIDE.md @@ -0,0 +1,280 @@ +# Datto RMM API Guide for openclaw + +Everything learned from building the Datto RMM integration in Pulse. Use this as a reference for any future work touching the Datto RMM API. + +--- + +## Authentication + +Datto RMM API v2 uses **OAuth2 with password grant type** — not a simple API key header. + +- **Auth endpoint**: `https://concord-api.centrastage.net/auth/oauth/token` +- **Base API URL**: `https://concord-api.centrastage.net/api/v2` +- **Client credentials**: always `public-client:public` (this is the public OAuth client — not your credentials) +- **Your credentials**: API Access Key = `username`, API Secret Key = `password` + +```http +POST https://concord-api.centrastage.net/auth/oauth/token +Authorization: Basic cHVibGljLWNsaWVudDpwdWJsaWM= (base64 of "public-client:public") +Content-Type: application/x-www-form-urlencoded + +grant_type=password&username=YOUR_API_KEY&password=YOUR_API_SECRET +``` + +Response returns `access_token` (bearer token). Tokens last ~1 hour; refresh proactively at 50 minutes. + +**Environment variables used in Pulse:** +``` +DATTO_RMM_API_URL=https://concord-api.centrastage.net +DATTO_RMM_API_KEY= +DATTO_RMM_API_SECRET= +DATTO_RMM_WEBHOOK_SECRET= +``` + +--- + +## Making API Calls + +All requests go to `https://concord-api.centrastage.net/api/v2{endpoint}` with: + +```http +Authorization: Bearer {access_token} +Content-Type: application/json +Accept: application/json +``` + +The response body is always JSON. Always parse it — the API never returns empty 200s (except for write operations where you should guard with `text ? JSON.parse(text) : {}`). + +--- + +## Key Endpoints + +### Sites +``` +GET /account/sites → { sites: [...], pageDetails: {...} } +GET /site/{siteUid}/devices → { devices: [...] } +``` + +Sites map to Autotask companies via `autotaskCompanyId` / `autotaskCompanyName` fields on the site object. This is the primary join key between the two systems. + +### Devices +``` +GET /account/devices → { devices: [...], pageDetails: {...} } +GET /devices/{deviceId} → { item: DattoRMMDevice } +GET /device/{deviceId}/auditdata → detailed hardware info (bios, processors, memory, disks) +``` + +Default page size is 250. Always paginate using `pageDetails.nextPageUrl` — follow it until null. + +### Alerts +``` +GET /account/alerts/open → { alerts: [...], pageDetails: {...} } +GET /account/alerts/resolved → { alerts: [...], pageDetails: {...} } +GET /alert/{alertUid} → single alert with full alertContext +``` + +Resolved alerts can be huge — cap page fetches (e.g. 4 pages) rather than fetching all. + +For PING alerts, `alertContext['@class'] === 'ping_ctx'` and `alertContext.instanceName` holds the ping target hostname. + +### Components & Quick Jobs +``` +GET /account/components → { components: [...] } (automation scripts/tasks) +PUT /device/{deviceUid}/quickjob → run a component on a device +GET /job/{jobUid}/results/{deviceUid} → job output/results +``` + +Quick job payload: +```json +{ + "jobName": "My Job Name", + "jobComponent": { + "componentUid": "abc-123-...", + "variables": [ + { "name": "VAR_NAME", "value": "value" } + ] + } +} +``` + +--- + +## Pagination Pattern + +The API uses `pageDetails.nextPageUrl` for cursor-based pagination. Always follow it: + +```typescript +let url: string | null = `https://concord-api.centrastage.net/api/v2/account/sites?pageSize=250`; +while (url) { + const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); + const body = await resp.json(); + items.push(...(body.sites || [])); + url = body.pageDetails?.nextPageUrl ?? null; +} +``` + +Some older endpoints use `?page=1&pageSize=250` query params — check which pattern each endpoint uses. The `fetchAllPages` helper in `datto-rmm-client.ts` handles the `nextPageUrl` style. + +--- + +## Data Model (PostgreSQL Tables) + +### `datto_rmm_sites` +| Column | Notes | +|--------|-------| +| `id` | Datto integer ID (PK) | +| `uid` | Datto GUID string (unique) | +| `name` | Site display name | +| `autotask_company_id` | FK → `companies(id)` — join key | +| `autotask_company_name` | Denormalized for display | +| `number_of_devices` | Online/offline counts from API | +| `portal_url` | Deep link to Datto portal | + +### `datto_rmm_devices` +| Column | Notes | +|--------|-------| +| `id` | Datto integer ID (PK) | +| `uid` | Datto GUID (unique, used for API calls and quick jobs) | +| `site_id` | FK → `datto_rmm_sites(id)` | +| `hostname` | Device hostname | +| `online` / `suspended` / `deleted` | Status flags | +| `device_type_category` | e.g. "Desktop", "Server", "Laptop" | +| `operating_system` | OS string | +| `antivirus_product` / `antivirus_status` | AV info | +| `patch_status` | Patch management summary | +| `patches_approved_pending` | Count of pending patches | +| `last_seen` | Timestamp (ms epoch from API, stored as TIMESTAMPTZ) | +| `web_remote_url` | Direct remote session URL | +| `udf` | JSONB — User Defined Fields 1–10 from API | + +### `datto_rmm_alerts` +| Column | Notes | +|--------|-------| +| `alert_uid` | PK (Datto GUID string) | +| `device_uid` / `device_name` | Source device | +| `site_uid` / `site_name` | Source site | +| `priority` | "Critical", "High", "Moderate", "Low", "Information" | +| `alert_context` | JSONB — varies by alert type, contains `@class` discriminator | +| `resolved` | Boolean | +| `resolved_on` | Timestamp | +| `muted` | Boolean | +| `ticket_number` | Linked Autotask ticket if any | +| `alert_category` | e.g. "Patch Management" | +| `alert_type` | e.g. "PING", "DISK", "CPU" | +| `alert_message_en` | Human-readable alert message | +| `device_udf1`–`device_udf29` | All 29 UDF fields from webhook payloads | +| `ping_target` | Resolved from API for PING alerts | +| `triggered` | Raw webhook "True"/"False" string | + +### `datto_rmm_webhook_logs` +Raw capture table — stores every incoming webhook payload verbatim for inspection before processing. + +--- + +## Webhooks + +Datto RMM can POST alert events to your endpoint when alerts fire or resolve. + +**Webhook receiver**: `POST /api/webhooks/datto-rmm` + +**Authentication**: Datto sends a shared secret in the `X-Datto-Webhook-Secret` header. Validate it against `DATTO_RMM_WEBHOOK_SECRET` env var. + +**Alert webhook payload shape** (flat JSON, not nested like the REST API): +```json +{ + "alert_uid": "abc-123-...", + "triggered": "True", // "True" = alert fired, "False" = resolved + "alert_type": "PING", + "alert_category": "Networking", + "alert_priority": "Critical", + "alert_message_en": "Ping monitor failed for ...", + "device_uid": "...", + "device_hostname": "SERVER01", + "device_ip": "10.0.0.1", + "device_os": "Windows Server 2019", + "device_description": "...", + "device_id": "12345", + "site_uid": "...", + "site_name": "ACME Corp", + "site_id": "678", + "platform": "Windows", + "last_user": "DOMAIN\\user", + "device_udf1": "...", + // ... device_udf2 through device_udf29 +} +``` + +Key gotcha: `triggered === "True"` means the alert is **active** (not resolved). `triggered === "False"` means it **resolved**. Map `triggered === "False"` → `resolved = true`. + +**Always return HTTP 200** even on errors — Datto will disable your webhook endpoint if it receives repeated non-200 responses. + +--- + +## Sync Architecture in Pulse + +The sync pipeline runs in order: **sites → devices → open_alerts → resolved_alerts** + +Sites must sync before devices (FK constraint). The sync service guards against orphaned FK refs by pre-fetching known IDs and setting FK fields to null when the parent doesn't exist yet. + +Timestamps from the Datto API come as **millisecond epoch integers**. Convert with `new Date(milliseconds)` before storing in PostgreSQL. + +The factory singleton (`datto-rmm-factory.ts`) is the standard way to get a client instance in API routes: +```typescript +import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory'; +const client = getDattoRMMClient(); +``` + +--- + +## Common Gotchas + +1. **Auth URL is different from API URL** — token is fetched from `concord-api.centrastage.net/auth/...`, API calls go to `concord-api.centrastage.net/api/v2/...` + +2. **Parse auth response carefully** — if auth fails, the server may return HTML (an error page) instead of JSON. Always try/catch the JSON.parse and log the raw text on failure. + +3. **`uid` vs `id`** — devices and sites have both an integer `id` and a GUID `uid`. The REST API uses `uid` in paths for most operations. Quick jobs require `deviceUid` (the GUID), not the integer ID. + +4. **UDFs in webhooks vs REST API** — The REST API returns UDFs as `udf: { udf1: "...", udf2: "..." }` (nested object, up to 10). Webhooks flatten them to `device_udf1` through `device_udf29` as top-level fields (29 total). + +5. **Alert context varies by type** — always check `alertContext['@class']` to know what fields are available. For PING alerts, fetch `/alert/{uid}` to get `instanceName` (the ping target) since it's not in the bulk alert list response. + +6. **Page size 250** is the effective maximum — don't request more. + +7. **Resolved alerts grow without bound** — never fetch all resolved alerts in production. Limit to recent pages (e.g. 4 pages = ~1000 most recent). + +8. **Site → Company mapping** — `site.autotaskCompanyId` is a string from the API even though it's an integer ID. Always `parseInt()` it and validate `> 0` and `!isNaN()` before using as a FK. + +--- + +## Quick Reference: Alert Priority Values + +- `Critical` +- `High` +- `Moderate` +- `Low` +- `Information` + +## Quick Reference: Common Alert Types + +- `PING` — ping monitor failure +- `DISK` — disk space/health +- `CPU` — CPU utilization +- `MEMORY` — RAM utilization +- `SERVICE` — Windows service down +- `EVENTLOG` — Windows event log match +- `PATCH` — patch management +- `ANTIVIRUS` — AV status + +--- + +## Pipeline Integration + +In Pulse, incoming Datto RMM webhooks can trigger the **pipeline engine** (fire-and-forget): + +```typescript +pipelineEngine.processTrigger('datto_rmm', payload).catch(err => + console.error('[DATTO-RMM-WEBHOOK] Pipeline processing error:', err) +); +``` + +The trigger type `'datto_rmm'` matches pipeline rules configured in the admin UI. Pipelines can take actions like creating Autotask tickets, sending notifications, etc.