From 08f21fb1a627d149074a5eb6f7d043e4ea509300 Mon Sep 17 00:00:00 2001 From: Trezy Date: Wed, 1 Jul 2026 09:22:11 -0500 Subject: [PATCH] docs: add missing endpoint groups Signed-off-by: Trezy --- .../docs/api-reference/admin/admin-api.md | 36 ++ .../docs/api-reference/admin/dead-letters.md | 203 +++++++ .../docs/api-reference/admin/feature-flags.md | 89 +++ .../docs/api-reference/admin/meta.json | 8 +- .../docs/api-reference/admin/permissions.md | 102 ++++ .../api-reference/admin/service-entries.md | 520 ++++++++++++++++++ .../api-reference/admin/service-identity.md | 160 ++++++ .../admin/verification-methods.md | 216 ++++++++ 8 files changed, 1333 insertions(+), 1 deletion(-) create mode 100644 packages/docs/content/docs/api-reference/admin/dead-letters.md create mode 100644 packages/docs/content/docs/api-reference/admin/feature-flags.md create mode 100644 packages/docs/content/docs/api-reference/admin/permissions.md create mode 100644 packages/docs/content/docs/api-reference/admin/service-entries.md create mode 100644 packages/docs/content/docs/api-reference/admin/service-identity.md create mode 100644 packages/docs/content/docs/api-reference/admin/verification-methods.md diff --git a/packages/docs/content/docs/api-reference/admin/admin-api.md b/packages/docs/content/docs/api-reference/admin/admin-api.md index 5370d89..4794315 100644 --- a/packages/docs/content/docs/api-reference/admin/admin-api.md +++ b/packages/docs/content/docs/api-reference/admin/admin-api.md @@ -58,6 +58,12 @@ AUTH="Authorization: Bearer $TOKEN" | [Script Variables](script-variables.md) | Encrypted key/value pairs for Lua scripts | | [API Clients](api-clients.md) | Register and manage third-party XRPC clients | | [Plugins](plugins.md) | Install, configure, and manage WASM plugins | +| [Dead Letters](dead-letters.md) | List, inspect, dismiss, retry, and reindex dead-lettered events | +| [Service Identity](service-identity.md) | Get and update service identity configuration | +| [Service Entries](service-entries.md) | Manage service entries and their XRPC bindings | +| [Verification Methods](verification-methods.md) | Create, list, and delete DID verification methods | +| [Feature Flags](feature-flags.md) | List feature flag status | +| [Permissions](permissions.md) | List available permission definitions | ## Permissions @@ -133,3 +139,33 @@ Each admin API endpoint requires a specific permission. See the [Permissions gui | `GET /admin/api-clients/{id}` | `api-clients:view` | | `PUT /admin/api-clients/{id}` | `api-clients:edit` | | `DELETE /admin/api-clients/{id}` | `api-clients:delete` | +| `GET /admin/dead-letters` | `dead-letters:read` | +| `GET /admin/dead-letters/count` | `dead-letters:read` | +| `GET /admin/dead-letters/{id}` | `dead-letters:read` | +| `POST /admin/dead-letters/{id}/dismiss` | `dead-letters:manage` | +| `POST /admin/dead-letters/{id}/retry` | `dead-letters:manage` | +| `POST /admin/dead-letters/{id}/reindex` | `dead-letters:manage` | +| `POST /admin/dead-letters/bulk/dismiss` | `dead-letters:manage` | +| `POST /admin/dead-letters/bulk/retry` | `dead-letters:manage` | +| `POST /admin/dead-letters/bulk/reindex` | `dead-letters:manage` | +| `GET /admin/service-identity` | `settings:manage` | +| `PUT /admin/service-identity` | `settings:manage` | +| `GET /admin/service-entries` | `settings:manage` | +| `POST /admin/service-entries` | `settings:manage` | +| `PUT /admin/service-entries/{id}` | `settings:manage` | +| `DELETE /admin/service-entries/{id}` | `settings:manage` | +| `GET /admin/service-entries/{id}/xrpcs` | `settings:manage` | +| `POST /admin/service-entries/{id}/xrpcs` | `settings:manage` | +| `DELETE /admin/service-entries/{id}/xrpcs`| `settings:manage` | +| `POST /admin/service-entries/sync-plc` | `settings:manage` | +| `POST /admin/service-entries/sync-plc/request` | `settings:manage` | +| `POST /admin/service-entries/sync-plc/submit` | `settings:manage` | +| `GET /admin/lexicons/{id}/services` | `settings:manage` | +| `GET /admin/verification-methods` | `settings:manage` | +| `POST /admin/verification-methods` | `settings:manage` | +| `DELETE /admin/verification-methods/{fragment_id}` | `settings:manage` | +| `GET /admin/feature-flags` | `settings:manage` | +| `GET /admin/network-lexicons/resolve/{nsid}` | `lexicons:read` | +| `GET /admin/permissions` | `users:read` | +| `GET /admin/settings/xrpc-proxy` | `settings:manage` | +| `PUT /admin/settings/xrpc-proxy` | `settings:manage` | diff --git a/packages/docs/content/docs/api-reference/admin/dead-letters.md b/packages/docs/content/docs/api-reference/admin/dead-letters.md new file mode 100644 index 0000000..febc323 --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/dead-letters.md @@ -0,0 +1,203 @@ +--- +title: "Dead Letters" +--- + +Events that failed all retry attempts are stored as dead letters for inspection and manual resolution. Dead letters come from two sources: legacy index hooks (`happyview_dead_letter_hooks`) and trigger-keyed scripts (`happyview_dead_letter_scripts`). Both tables are surfaced through a single unified API. + +Read endpoints require `dead-letters:read`. Action endpoints (dismiss, retry, reindex) require `dead-letters:manage`. + +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## List dead letters + +``` +GET /admin/dead-letters +``` + +Paginated list of dead letters from both tables, merged and sorted by `created_at` descending. + +| Param | Type | Required | Description | +| ------------ | ------ | -------- | ----------------------------------------------------- | +| `collection` | string | no | Filter by collection NSID | +| `resolved` | string | no | `"true"`, `"false"` (default), or omit for all | +| `cursor` | string | no | Pagination cursor from a previous response | +| `limit` | number | no | Max results per page (default 50, max 100) | + +```sh tab="cURL" tab-group="language" +curl "http://127.0.0.1:3000/admin/dead-letters?limit=10" -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +{ + "dead_letters": [ + { + "id": "42", + "lexicon_id": "xyz.statusphere.status", + "uri": "at://did:plc:abc/xyz.statusphere.status/3k...", + "did": "did:plc:abc", + "collection": "xyz.statusphere.status", + "rkey": "3k...", + "action": "create", + "error": "script error: attempt to index nil value", + "attempts": 4, + "created_at": "2026-06-01T12:00:00Z" + } + ], + "cursor": "2026-06-01T11:00:00Z" +} +``` + +Resolved dead letters include a `resolved_at` timestamp. `cursor` is omitted when there are no more results. + +## Count dead letters + +``` +GET /admin/dead-letters/count +``` + +Returns the total count of dead letters across both tables. + +| Param | Type | Required | Description | +| ------------ | ------ | -------- | ---------------------------------------------- | +| `collection` | string | no | Filter by collection NSID | +| `resolved` | string | no | `"true"`, `"false"` (default), or omit for all | + +```sh tab="cURL" tab-group="language" +curl "http://127.0.0.1:3000/admin/dead-letters/count" -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +{ + "count": 7 +} +``` + +## Get dead letter detail + +``` +GET /admin/dead-letters/{id} +``` + +Returns the full dead letter including the record body. + +```sh tab="cURL" tab-group="language" +curl "http://127.0.0.1:3000/admin/dead-letters/42" -H "$AUTH" +``` + +**Response**: `200 OK` + +The response includes all fields from the list view plus a `record` field containing the original record data (if available). Returns `404` if the dead letter is not found. + +## Dismiss a dead letter + +``` +POST /admin/dead-letters/{id}/dismiss +``` + +Marks a dead letter as resolved without retrying it. + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/42/dismiss" -H "$AUTH" +``` + +**Response**: `200 OK` — `{"ok": true}` + +Returns `404` if the dead letter is not found. + +## Retry a dead letter + +``` +POST /admin/dead-letters/{id}/retry +``` + +Re-runs the dead letter's script with the original event payload. On success the dead letter is marked resolved. On failure the error and attempt count are updated. + +Label-arrival dead letters cannot be retried — the upstream label event is gone. These return `400 Bad Request`. + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/42/retry" -H "$AUTH" +``` + +**Response**: `200 OK` — `{"ok": true}` + +Returns `404` if no matching script binding is found, or if the dead letter is not found or already resolved. + +## Reindex a dead letter + +``` +POST /admin/dead-letters/{id}/reindex +``` + +Fetches the record fresh from the author's PDS and re-runs the full record indexing pipeline. On success the dead letter is marked resolved. Label-arrival dead letters cannot be reindexed and return `400 Bad Request`. + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/42/reindex" -H "$AUTH" +``` + +**Response**: `200 OK` — `{"ok": true}` + +## Bulk dismiss + +``` +POST /admin/dead-letters/bulk/dismiss +``` + +Dismiss multiple dead letters at once. + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | ----------------------------------------------------- | +| `ids` | string[] | no | List of dead letter IDs to dismiss | +| `all` | boolean | no | Set to `true` to dismiss all unresolved dead letters | +| `collection` | string | no | When `all` is true, limit to this collection | + +One of `ids` or `all: true` is required. + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/bulk/dismiss" \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{"ids": ["42", "43", "44"]}' +``` + +**Response**: `200 OK` — `{"ok": true}` + +## Bulk retry + +``` +POST /admin/dead-letters/bulk/retry +``` + +Retry multiple dead letters. Accepts the same input as [bulk dismiss](#bulk-dismiss). + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/bulk/retry" \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{"all": true, "collection": "xyz.statusphere.status"}' +``` + +**Response**: `200 OK` — `{"ok": true}` + +## Bulk reindex + +``` +POST /admin/dead-letters/bulk/reindex +``` + +Reindex multiple dead letters. Accepts the same input as [bulk dismiss](#bulk-dismiss). + +```sh tab="cURL" tab-group="language" +curl -X POST "http://127.0.0.1:3000/admin/dead-letters/bulk/reindex" \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{"all": true}' +``` + +**Response**: `200 OK` — `{"ok": true}` diff --git a/packages/docs/content/docs/api-reference/admin/feature-flags.md b/packages/docs/content/docs/api-reference/admin/feature-flags.md new file mode 100644 index 0000000..914a6f5 --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/feature-flags.md @@ -0,0 +1,89 @@ +--- +title: "Feature Flags" +--- + +```ts tab="TypeScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```js tab="JavaScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```rust tab="Rust" tab-group="language" +let token = "hv_..."; // your API key +``` +```go tab="Go" tab-group="language" +token := "hv_..." // your API key +``` +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## List feature flags + +``` +GET /admin/feature-flags +``` + +Returns all feature flags and their current status. Flags are backed by the `happyview_instance_settings` table — a flag is enabled when its key is set to `"true"`. + +```ts tab="TypeScript" tab-group="language" +interface FeatureFlag { + key: string; + name: string; + description: string; + enabled: boolean; +} + +const response = await fetch("http://127.0.0.1:3000/admin/feature-flags", { + headers, +}); +const data: FeatureFlag[] = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/feature-flags", { + headers, +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let client = reqwest::Client::new(); +let response = client + .get("http://127.0.0.1:3000/admin/feature-flags") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/feature-flags", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/feature-flags -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +[ + { + "key": "feature.spaces_enabled", + "name": "Permissioned Spaces", + "description": "Collaborative data spaces with granular permissions, membership, and invites.", + "enabled": false + } +] +``` + +| Field | Type | Description | +| ------------- | ------- | -------------------------------------------- | +| `key` | string | Settings key for the flag | +| `name` | string | Human-readable name | +| `description` | string | What the flag controls | +| `enabled` | boolean | Whether the flag is currently enabled | + +To toggle a flag, use `PUT /admin/settings/{key}` with the value `"true"` or `"false"`. See [Instance Settings](settings.md) for details. diff --git a/packages/docs/content/docs/api-reference/admin/meta.json b/packages/docs/content/docs/api-reference/admin/meta.json index f1e0c09..79305b5 100644 --- a/packages/docs/content/docs/api-reference/admin/meta.json +++ b/packages/docs/content/docs/api-reference/admin/meta.json @@ -16,6 +16,12 @@ "domains", "script-variables", "api-clients", - "plugins" + "plugins", + "dead-letters", + "service-identity", + "service-entries", + "verification-methods", + "feature-flags", + "permissions" ] } diff --git a/packages/docs/content/docs/api-reference/admin/permissions.md b/packages/docs/content/docs/api-reference/admin/permissions.md new file mode 100644 index 0000000..f2bf6de --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/permissions.md @@ -0,0 +1,102 @@ +--- +title: "Permissions" +--- + +```ts tab="TypeScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```js tab="JavaScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```rust tab="Rust" tab-group="language" +let token = "hv_..."; // your API key +``` +```go tab="Go" tab-group="language" +token := "hv_..." // your API key +``` +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## List permissions + +``` +GET /admin/permissions +``` + +Returns all available permission definitions and permission templates. When the Spaces feature flag is disabled, spaces-related permissions and template entries are excluded. + +```ts tab="TypeScript" tab-group="language" +interface PermissionDef { + key: string; + name: string; + description: string; + category: string; +} + +interface PermissionTemplate { + key: string; + label: string; + permissions: string[]; +} + +interface PermissionsResponse { + permissions: PermissionDef[]; + templates: PermissionTemplate[]; +} + +const response = await fetch("http://127.0.0.1:3000/admin/permissions", { + headers, +}); +const data: PermissionsResponse = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/permissions", { + headers, +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let client = reqwest::Client::new(); +let response = client + .get("http://127.0.0.1:3000/admin/permissions") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/permissions", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/permissions -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +{ + "permissions": [ + { + "key": "lexicons:create", + "name": "Create Lexicons", + "description": "Upload lexicon schemas", + "category": "Lexicons" + } + ], + "templates": [ + { + "key": "viewer", + "label": "Viewer", + "permissions": ["lexicons:read", "records:read", "stats:read", "events:read"] + } + ] +} +``` + +Templates are predefined permission bundles used when creating or updating users. See the [Permissions guide](../../guides/permissions.md) for the full list. diff --git a/packages/docs/content/docs/api-reference/admin/service-entries.md b/packages/docs/content/docs/api-reference/admin/service-entries.md new file mode 100644 index 0000000..eefebc7 --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/service-entries.md @@ -0,0 +1,520 @@ +--- +title: "Service Entries" +--- + +Manage the service entries published in the instance's DID document. Each entry declares an XRPC service type (e.g. `AtprotoLabeler`, `BskyFeedGenerator`) and controls which lexicons route to HappyView's endpoint. All endpoints require the `settings:manage` permission. + +```ts tab="TypeScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```js tab="JavaScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` +```rust tab="Rust" tab-group="language" +let token = "hv_..."; // your API key +``` +```go tab="Go" tab-group="language" +token := "hv_..." // your API key +``` +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## List service entries + +``` +GET /admin/service-entries +``` + +```ts tab="TypeScript" tab-group="language" +interface ServiceEntry { + id: number; + fragment_id: string; + service_type: string; + access_mode: string; + created_at: string; + updated_at: string; +} + +const response = await fetch("http://127.0.0.1:3000/admin/service-entries", { + headers, +}); +const data: ServiceEntry[] = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-entries", { + headers, +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let client = reqwest::Client::new(); +let response = client + .get("http://127.0.0.1:3000/admin/service-entries") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/service-entries", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/service-entries -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +[ + { + "id": 1, + "fragment_id": "#happyview", + "service_type": "AtprotoLabeler", + "access_mode": "all", + "created_at": "2026-05-09T12:00:00Z", + "updated_at": "2026-05-09T12:00:00Z" + } +] +``` + +## Create a service entry + +``` +POST /admin/service-entries +``` + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-entries", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fragment_id: "#feed", + service_type: "BskyFeedGenerator", + }), +}); +const data: ServiceEntry = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-entries", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fragment_id: "#feed", + service_type: "BskyFeedGenerator", + }), +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let response = client + .post("http://127.0.0.1:3000/admin/service-entries") + .bearer_auth(token) + .json(&serde_json::json!({ + "fragment_id": "#feed", + "service_type": "BskyFeedGenerator" + })) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"fragment_id": "#feed", "service_type": "BskyFeedGenerator"}`) +req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/service-entries", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/service-entries \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "fragment_id": "#feed", "service_type": "BskyFeedGenerator" }' +``` + +| Field | Type | Required | Description | +| -------------- | ------ | -------- | ----------------------------------------------------------- | +| `fragment_id` | string | yes | DID document fragment (e.g. `#feed`, `#happyview`) | +| `service_type` | string | yes | AT Protocol service type (e.g. `BskyFeedGenerator`) | + +The entry is created with `access_mode` set to `all`. The endpoint URL is derived from `PUBLIC_URL`. + +**Response**: `201 Created` + +## Update a service entry + +``` +PUT /admin/service-entries/{id} +``` + +```ts tab="TypeScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1", { + method: "PUT", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + service_type: "AtprotoLabeler", + access_mode: "allowlist", + }), +}); +``` +```js tab="JavaScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1", { + method: "PUT", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + service_type: "AtprotoLabeler", + access_mode: "allowlist", + }), +}); +``` +```rust tab="Rust" tab-group="language" +client + .put("http://127.0.0.1:3000/admin/service-entries/1") + .bearer_auth(token) + .json(&serde_json::json!({ + "service_type": "AtprotoLabeler", + "access_mode": "allowlist" + })) + .send() + .await?; +``` +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"service_type": "AtprotoLabeler", "access_mode": "allowlist"}`) +req, _ := http.NewRequest("PUT", "http://127.0.0.1:3000/admin/service-entries/1", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl -X PUT http://127.0.0.1:3000/admin/service-entries/1 \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "service_type": "AtprotoLabeler", "access_mode": "allowlist" }' +``` + +| Field | Type | Required | Description | +| -------------- | ------ | -------- | -------------------------------------------------- | +| `fragment_id` | string | no | Updated DID document fragment | +| `service_type` | string | no | Updated service type | +| `access_mode` | string | no | `all`, `allowlist`, or `disabled` | + +All fields are optional. Only provided fields are updated. + +**Response**: `204 No Content` + +## Delete a service entry + +``` +DELETE /admin/service-entries/{id} +``` + +```ts tab="TypeScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1", { + method: "DELETE", + headers, +}); +``` +```js tab="JavaScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1", { + method: "DELETE", + headers, +}); +``` +```rust tab="Rust" tab-group="language" +client + .delete("http://127.0.0.1:3000/admin/service-entries/1") + .bearer_auth(token) + .send() + .await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("DELETE", "http://127.0.0.1:3000/admin/service-entries/1", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl -X DELETE http://127.0.0.1:3000/admin/service-entries/1 \ + -H "$AUTH" +``` + +Returns `404 Not Found` if the entry doesn't exist. + +**Response**: `204 No Content` + +## List XRPCs for a service entry + +``` +GET /admin/service-entries/{id}/xrpcs +``` + +Returns the lexicon IDs (NSIDs) bound to this service entry. + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + headers, +}); +const data: string[] = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + headers, +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let response = client + .get("http://127.0.0.1:3000/admin/service-entries/1/xrpcs") + .bearer_auth(token) + .send() + .await?; +let data: Vec = response.json().await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/service-entries/1/xrpcs", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/service-entries/1/xrpcs -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +["app.bsky.feed.getFeedSkeleton", "app.bsky.feed.describeFeedGenerator"] +``` + +## Add XRPCs to a service entry + +``` +POST /admin/service-entries/{id}/xrpcs +``` + +```ts tab="TypeScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lexicon_ids: ["app.bsky.feed.getFeedSkeleton"], + }), +}); +``` +```js tab="JavaScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lexicon_ids: ["app.bsky.feed.getFeedSkeleton"], + }), +}); +``` +```rust tab="Rust" tab-group="language" +client + .post("http://127.0.0.1:3000/admin/service-entries/1/xrpcs") + .bearer_auth(token) + .json(&serde_json::json!({ + "lexicon_ids": ["app.bsky.feed.getFeedSkeleton"] + })) + .send() + .await?; +``` +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"lexicon_ids": ["app.bsky.feed.getFeedSkeleton"]}`) +req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/service-entries/1/xrpcs", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/service-entries/1/xrpcs \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "lexicon_ids": ["app.bsky.feed.getFeedSkeleton"] }' +``` + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | ------------------------------------- | +| `lexicon_ids` | string[] | yes | Lexicon NSIDs to bind to this entry | + +**Response**: `204 No Content` + +## Remove XRPCs from a service entry + +``` +DELETE /admin/service-entries/{id}/xrpcs +``` + +```ts tab="TypeScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + method: "DELETE", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lexicon_ids: ["app.bsky.feed.getFeedSkeleton"], + }), +}); +``` +```js tab="JavaScript" tab-group="language" +await fetch("http://127.0.0.1:3000/admin/service-entries/1/xrpcs", { + method: "DELETE", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lexicon_ids: ["app.bsky.feed.getFeedSkeleton"], + }), +}); +``` +```rust tab="Rust" tab-group="language" +client + .delete("http://127.0.0.1:3000/admin/service-entries/1/xrpcs") + .bearer_auth(token) + .json(&serde_json::json!({ + "lexicon_ids": ["app.bsky.feed.getFeedSkeleton"] + })) + .send() + .await?; +``` +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"lexicon_ids": ["app.bsky.feed.getFeedSkeleton"]}`) +req, _ := http.NewRequest("DELETE", "http://127.0.0.1:3000/admin/service-entries/1/xrpcs", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl -X DELETE http://127.0.0.1:3000/admin/service-entries/1/xrpcs \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "lexicon_ids": ["app.bsky.feed.getFeedSkeleton"] }' +``` + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | ---------------------------------------- | +| `lexicon_ids` | string[] | yes | Lexicon NSIDs to unbind from this entry | + +**Response**: `204 No Content` + +## List services for a lexicon + +``` +GET /admin/lexicons/{id}/services +``` + +Returns service entries that grant access to the specified lexicon. + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services", { + headers, +}); +const data: ServiceEntry[] = await response.json(); +``` +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services", { + headers, +}); +const data = await response.json(); +``` +```rust tab="Rust" tab-group="language" +let response = client + .get("http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/lexicons/app.bsky.feed.getFeedSkeleton/services -H "$AUTH" +``` + +**Response**: `200 OK` — returns an array of `ServiceEntry` objects. + +## Sync to PLC directory + +These endpoints publish your service entries to the PLC directory so they appear in the DID document. The sync method depends on the [service identity mode](../../getting-started/service-identity.md). + +### Direct sync (did:plc mode) + +``` +POST /admin/service-entries/sync-plc +``` + +Signs and submits a PLC update operation using the stored rotation key. Only available when the identity mode is `did_plc`. + +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc -H "$AUTH" +``` + +Returns `400 Bad Request` if the identity mode is not `did_plc` or no DID is configured. + +**Response**: `204 No Content` + +### Request PLC token (attach_account mode) + +``` +POST /admin/service-entries/sync-plc/request +``` + +Requests a PLC operation signature token via the attached account's PDS. This sends an email confirmation code to the account holder. Only available when the identity mode is `attach_account`. + +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc/request -H "$AUTH" +``` + +Returns `400 Bad Request` if the identity mode is not `attach_account`. + +**Response**: `204 No Content` + +### Submit PLC token (attach_account mode) + +``` +POST /admin/service-entries/sync-plc/submit +``` + +Submits the email confirmation token to sign and publish the PLC operation via the attached account's PDS. + +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/service-entries/sync-plc/submit \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "token": "123456" }' +``` + +| Field | Type | Required | Description | +| ------- | ------ | -------- | ------------------------------------ | +| `token` | string | yes | Email confirmation code from the PDS | + +Returns `400 Bad Request` if the identity mode is not `attach_account`. + +**Response**: `204 No Content` diff --git a/packages/docs/content/docs/api-reference/admin/service-identity.md b/packages/docs/content/docs/api-reference/admin/service-identity.md new file mode 100644 index 0000000..0e1656e --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/service-identity.md @@ -0,0 +1,160 @@ +--- +title: "Service Identity" +--- + +Manage the service identity configuration: the DID and identity mode that HappyView uses to identify itself on the AT Protocol network. All endpoints require the `settings:manage` permission. + +```ts tab="TypeScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` + +```js tab="JavaScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` + +```rust tab="Rust" tab-group="language" +let token = "hv_..."; // your API key +``` + +```go tab="Go" tab-group="language" +token := "hv_..." // your API key +``` + +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## Get service identity + +``` +GET /admin/service-identity +``` + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-identity", { + headers, +}); +const data = await response.json(); +``` + +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-identity", { + headers, +}); +const data = await response.json(); +``` + +```rust tab="Rust" tab-group="language" +let response = client + .get("http://127.0.0.1:3000/admin/service-identity") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` + +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/service-identity", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` + +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/service-identity -H "$AUTH" +``` + +Returns the current service identity configuration, or `null` if no identity has been configured. + +**Response:** + +| Field | Type | Description | +| ---------------------- | ----------- | ------------------------------------------------- | +| `mode` | string | Identity mode (see below) | +| `did` | string/null | The service DID | +| `signing_key_enc` | string/null | Encrypted signing key (present for `did_plc`) | +| `attached_account_did` | string/null | Linked account DID (present for `attach_account`) | +| `setup_complete` | boolean | Whether identity setup has been completed | +| `created_at` | string | ISO 8601 timestamp | +| `updated_at` | string | ISO 8601 timestamp | + +### Identity modes + +| Mode | Description | +| ---------------- | ---------------------------------------------------- | +| `did_web` | HappyView derives a `did:web` from its public URL | +| `did_plc` | HappyView manages its own `did:plc` identity | +| `attach_account` | HappyView uses an existing AT Protocol account's DID | +| `not_exposed` | No service identity is exposed on the network | + +## Update service identity + +``` +PUT /admin/service-identity +``` + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-identity", { + method: "PUT", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + mode: "did_web", + }), +}); +``` + +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/service-identity", { + method: "PUT", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + mode: "did_web", + }), +}); +``` + +```rust tab="Rust" tab-group="language" +let response = client + .put("http://127.0.0.1:3000/admin/service-identity") + .bearer_auth(token) + .json(&serde_json::json!({ + "mode": "did_web" + })) + .send() + .await?; +``` + +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"mode": "did_web"}`) +req, _ := http.NewRequest("PUT", "http://127.0.0.1:3000/admin/service-identity", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` + +```sh tab="cURL" tab-group="language" +curl -X PUT http://127.0.0.1:3000/admin/service-identity \ + -H "$AUTH" \ + -H 'Content-Type: application/json' \ + -d '{"mode": "did_web"}' +``` + +**Input:** + +| Field | Type | Required | Description | +| ---------------------- | ------ | -------- | -------------------------------------------------------- | +| `mode` | string | Yes | `did_web`, `did_plc`, `attach_account`, or `not_exposed` | +| `did` | string | No | Service DID (required for `did_plc`) | +| `signing_key_enc` | string | No | Encrypted signing key (for `did_plc`) | +| `rotation_key_enc` | string | No | Encrypted rotation key (for `did_plc`) | +| `attached_account_did` | string | No | Account DID to attach (for `attach_account`) | + +**Response**: `204 No Content` diff --git a/packages/docs/content/docs/api-reference/admin/verification-methods.md b/packages/docs/content/docs/api-reference/admin/verification-methods.md new file mode 100644 index 0000000..123e8a6 --- /dev/null +++ b/packages/docs/content/docs/api-reference/admin/verification-methods.md @@ -0,0 +1,216 @@ +--- +title: "Verification Methods" +--- + +Manage DID verification methods (P-256 keypairs) used for attestation signing and PLC operations. + +```ts tab="TypeScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` + +```js tab="JavaScript" tab-group="language" +const TOKEN = "hv_..."; // your API key +const headers = { Authorization: `Bearer ${TOKEN}` }; +``` + +```rust tab="Rust" tab-group="language" +let token = "hv_..."; // your API key +``` + +```go tab="Go" tab-group="language" +token := "hv_..." // your API key +``` + +```sh tab="cURL" tab-group="language" +# All examples assume $TOKEN is an API key (hv_...) +AUTH="Authorization: Bearer $TOKEN" +``` + +## List verification methods + +``` +GET /admin/verification-methods +``` + +Requires `settings:manage` permission. + +```ts tab="TypeScript" tab-group="language" +interface VerificationMethod { + id: string; + fragment_id: string; + key_type: string; + public_key_multibase: string; + created_at: string; +} + +const response = await fetch("http://127.0.0.1:3000/admin/verification-methods", { + headers, +}); +const data: VerificationMethod[] = await response.json(); +``` + +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/verification-methods", { + headers, +}); +const data = await response.json(); +``` + +```rust tab="Rust" tab-group="language" +let response = client + .get("http://127.0.0.1:3000/admin/verification-methods") + .bearer_auth(token) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` + +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/verification-methods", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` + +```sh tab="cURL" tab-group="language" +curl http://127.0.0.1:3000/admin/verification-methods -H "$AUTH" +``` + +**Response**: `200 OK` + +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "fragment_id": "#attestation", + "key_type": "Multikey", + "public_key_multibase": "zDnae...", + "created_at": "2026-06-27T00:00:00Z" + } +] +``` + +## Create a verification method + +``` +POST /admin/verification-methods +``` + +Requires `settings:manage` permission. Generates a new P-256 keypair. The private key is encrypted at rest with AES-256-GCM using `TOKEN_ENCRYPTION_KEY`. + +```ts tab="TypeScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/verification-methods", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fragment_id: "#attestation", + }), +}); +const data: VerificationMethod = await response.json(); +``` + +```js tab="JavaScript" tab-group="language" +const response = await fetch("http://127.0.0.1:3000/admin/verification-methods", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fragment_id: "#attestation", + }), +}); +const data = await response.json(); +``` + +```rust tab="Rust" tab-group="language" +let response = client + .post("http://127.0.0.1:3000/admin/verification-methods") + .bearer_auth(token) + .json(&serde_json::json!({ + "fragment_id": "#attestation" + })) + .send() + .await?; +let data: serde_json::Value = response.json().await?; +``` + +```go tab="Go" tab-group="language" +body := bytes.NewBufferString(`{"fragment_id": "#attestation"}`) +req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/admin/verification-methods", body) +req.Header.Set("Authorization", "Bearer "+token) +req.Header.Set("Content-Type", "application/json") +resp, err := http.DefaultClient.Do(req) +``` + +```sh tab="cURL" tab-group="language" +curl -X POST http://127.0.0.1:3000/admin/verification-methods \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{"fragment_id": "#attestation"}' +``` + +| Field | Type | Required | Description | +| ------------- | ------ | -------- | --------------------------------------------------------------------------------- | +| `fragment_id` | string | yes | DID document fragment identifier, must start with `#` followed by alphanumerics or underscores | + +**Response**: `201 Created` + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "fragment_id": "#attestation", + "key_type": "Multikey", + "public_key_multibase": "zDnae...", + "created_at": "2026-06-27T00:00:00Z" +} +``` + +## Delete a verification method + +``` +DELETE /admin/verification-methods/{fragment_id} +``` + +Requires `settings:manage` permission. + +```ts tab="TypeScript" tab-group="language" +const response = await fetch( + "http://127.0.0.1:3000/admin/verification-methods/attestation", + { method: "DELETE", headers }, +); +``` + +```js tab="JavaScript" tab-group="language" +const response = await fetch( + "http://127.0.0.1:3000/admin/verification-methods/attestation", + { method: "DELETE", headers }, +); +``` + +```rust tab="Rust" tab-group="language" +client + .delete("http://127.0.0.1:3000/admin/verification-methods/attestation") + .bearer_auth(token) + .send() + .await?; +``` + +```go tab="Go" tab-group="language" +req, _ := http.NewRequest("DELETE", + "http://127.0.0.1:3000/admin/verification-methods/attestation", nil) +req.Header.Set("Authorization", "Bearer "+token) +resp, err := http.DefaultClient.Do(req) +``` + +```sh tab="cURL" tab-group="language" +curl -X DELETE http://127.0.0.1:3000/admin/verification-methods/attestation \ + -H "$AUTH" +``` + +The `fragment_id` path parameter is the identifier without the leading `#` (e.g. `attestation` for `#attestation`). + +**Response**: `204 No Content`. Returns `404 Not Found` if no method with that fragment ID exists. -- 2.51.2