diff options
| author | Christian Cleberg <[email protected]> | 2026-07-24 23:14:23 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-24 23:29:01 -0500 |
| commit | a52dee116d4066d1b59bd90b4ebc4def4e1597d6 (patch) | |
| tree | 77f852ffea7f6940710a532190225673275b9279 /Docs/local-api.md | |
| parent | 7001577e6798530c943ac4cf03258bc172927a67 (diff) | |
| download | domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.tar.gz domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.tar.bz2 domain-dig-a52dee116d4066d1b59bd90b4ebc4def4e1597d6.zip | |
feat: stabilize and document the Local API v1 response contract (v5 step 3)
Second v5.0.0 roadmap item: make the Local API's public JSON contract explicit,
documented, and regression-locked, so external consumers (Shortcuts, scripts,
integrations) have a stable surface with a defined compatibility promise.
- LocalAPIContract: new single source of truth for the wire-format version
("v1") and the canonical JSON encoder (ISO-8601 dates, sorted keys). Both the
success and error paths in LocalAPIService now route through it, so the format
can't drift between them, and the ad-hoc per-call-site encoders are gone.
- The response envelope and every payload struct are promoted from `private` to
internal so the contract is a first-class, testable part of the module. The
transport/handler internals (request parser, HTTP response, secret store)
stay private.
- Docs/local-api.md documents the base URL/auth, the envelope, the encoding
conventions (notably: absent optionals are omitted, not null), every endpoint
and its payload fields, the error codes, and the semantic-version-style
compatibility policy (additive changes keep v1; renames/removals/type changes
bump the version). Linked from the README.
- LocalAPIContractTests: 16 structure/"golden" tests pinning the envelope shape,
each payload's field names, the enum encodings, and the ISO-8601 date format.
They assert structure, not values, so ordinary behavior changes don't churn
them but a renamed or dropped field fails CI. Full unit suite: 52 passing.
Diffstat (limited to 'Docs/local-api.md')
| -rw-r--r-- | Docs/local-api.md | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/Docs/local-api.md b/Docs/local-api.md new file mode 100644 index 0000000..a0cebdc --- /dev/null +++ b/Docs/local-api.md @@ -0,0 +1,113 @@ +# DomainDig Local API — `v1` + +The Local API exposes DomainDig's canonical report data to on-device automation +(Shortcuts, scripts, integrations). It is **off by default** and, when enabled, +binds only to loopback. + +- **Base URL:** `http://127.0.0.1:<port>` (default port `47821`, configurable in + Settings → Local API) +- **Binding:** loopback only (`acceptLocalOnly`); never reachable off-device +- **Content type:** every response is `application/json` +- **Version:** `v1` (reported in every response envelope) + +This document is the stable contract. The response shape is pinned by +`DomainDigTests/LocalAPIContractTests.swift`; `LocalAPIContract` (in +`LocalAPIContract.swift`) is the single source of truth for the version string +and the JSON encoder. + +## Authentication + +Every request requires the token shown in Settings → Local API, supplied either +way: + +``` +Authorization: Bearer <token> +``` +``` +X-API-Token: <token> +``` + +A missing or wrong token returns `401 unauthorized`. Settings → Local API has a +**Copy cURL Command** button that emits a ready-to-run authenticated request. + +## Response envelope + +Every response — success or error — is wrapped in the same envelope: + +```json +{ + "success": true, + "version": "v1", + "data": { "...": "payload, present on success" } +} +``` +```json +{ + "success": false, + "version": "v1", + "error": { "code": "not_found", "message": "The requested Local API route does not exist." } +} +``` + +- On success, `data` holds the endpoint payload and `error` is **omitted**. +- On failure, `error` holds a machine `code` plus a human `message`, and `data` + is **omitted**. + +### Encoding conventions + +- **Dates** are ISO-8601 UTC strings, e.g. `"2023-11-14T22:13:20Z"`. +- **Absent optional fields are omitted, not `null`.** Consumers must treat a + missing key as "not present." +- Object keys are emitted in sorted order (deterministic output; not + contractually meaningful — do not depend on key order). + +## Endpoints + +| Method | Path | Payload (`data`) fields | +|--------|------|-------------------------| +| GET | `/portfolio` | `summary` → `{ totalDomains, healthyCount, warningCount, criticalCount, changedLast24h, expiringSoonCount, unreachableCount }` | +| GET | `/domains` | `domains: [TrackedDomain]` | +| GET | `/domains/{domain}` | `domain`, `trackedDomain?` (`TrackedDomain`), `latestReport?` (`DomainReport`) | +| GET | `/domains/{domain}/history` | `domain`, `history: [HistoryEntry]` | +| GET | `/events` | `events: [{ timestamp, domain, summary, status, severity }]` | +| GET | `/monitoring` | `isEnabled`, `scope` (`"allTracked"` \| `"selectedOnly"`), `alertsEnabled`, `monitoredDomains: [{ domain, monitoringEnabled, lastMonitoredAt?, lastAlertAt?, certificateWarningLevel }]` | +| POST | `/inspect` | body `{ "domain": "example.com" }` → `report` (`DomainReport`) | +| POST | `/inspect/{domain}` | `report` (`DomainReport`) | +| POST | `/monitoring/{domain}/enable` | `domain`, `monitoringEnabled` | +| POST | `/monitoring/{domain}/disable` | `domain`, `monitoringEnabled` | + +`certificateWarningLevel` encodes as `"none"`, `"warning"`, or `"critical"`. + +`DomainReport` is the app's canonical report model (the same shape the JSON +export produces); see `DomainReportBuilder.swift` for its fields. It is a large +object and is treated as an additive contract: new fields may appear without a +version bump. + +## Error codes + +| HTTP | `code` | When | +|------|--------|------| +| 400 | `bad_request` | The HTTP request line/path could not be parsed | +| 400 | `invalid_body` | `POST /inspect` body was not `{ "domain": "…" }` | +| 400 | `invalid_domain` | A path/body domain was empty or invalid | +| 401 | `unauthorized` | Missing or incorrect token | +| 404 | `not_found` | No such route | +| 404 | `domain_not_found` | No local data / tracked domain for the given name | +| 500 | `encoding_failed` | The response could not be encoded | +| 500 | `internal_error` | The request handler failed unexpectedly | + +## Compatibility policy + +The `version` field follows a semantic-version-style promise: + +- **Backward-compatible changes keep `version` at `v1`.** Adding a new endpoint, + or adding a new field to an existing payload, is non-breaking. **Consumers + must ignore unknown fields.** +- **Breaking changes bump `version`.** Renaming or removing a field, changing a + field's type, or changing the meaning/units of an existing field requires a new + version, an update to this document, and an update to + `LocalAPIContractTests.swift`. + +There are currently no deprecated fields or endpoints. When a field is +deprecated, it will be listed here with the version in which it becomes eligible +for removal, and will remain present for at least one subsequent version. |
