summaryrefslogtreecommitdiff
path: root/Docs
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-24 23:14:23 -0500
committerChristian Cleberg <[email protected]>2026-07-24 23:29:01 -0500
commita52dee116d4066d1b59bd90b4ebc4def4e1597d6 (patch)
tree77f852ffea7f6940710a532190225673275b9279 /Docs
parent7001577e6798530c943ac4cf03258bc172927a67 (diff)
downloaddomain-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')
-rw-r--r--Docs/local-api.md113
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.