From a8e40f7a8f46f204806655cd8ca3c306a9458494 Mon Sep 17 00:00:00 2001 From: Trezy Date: Thu, 30 Apr 2026 14:34:14 -0500 Subject: [PATCH] docs: add missing docs for Lua APIs --- .../docs/getting-started/configuration.md | 3 + .../guides/features/attestation-signing.md | 109 ++++++++++++++++++ .../docs/docs/guides/indexing/index-hooks.md | 3 + packages/docs/docs/guides/scripting.md | 48 +++++--- .../guides/scripting/signed-record-verify.md | 54 +++++++++ .../docs/guides/scripting/signed-record.md | 56 +++++++++ .../docs/docs/reference/lua/atproto-api.md | 92 ++++++++++++--- .../docs/docs/reference/lua/database-api.md | 20 +++- .../docs/docs/reference/lua/xrpc-lua-api.md | 76 ++++++++++++ packages/docs/sidebars.ts | 20 ++++ 10 files changed, 450 insertions(+), 31 deletions(-) create mode 100644 packages/docs/docs/guides/features/attestation-signing.md create mode 100644 packages/docs/docs/guides/scripting/signed-record-verify.md create mode 100644 packages/docs/docs/guides/scripting/signed-record.md create mode 100644 packages/docs/docs/reference/lua/xrpc-lua-api.md diff --git a/packages/docs/docs/getting-started/configuration.md b/packages/docs/docs/getting-started/configuration.md index 1df104d..34a809f 100644 --- a/packages/docs/docs/getting-started/configuration.md +++ b/packages/docs/docs/getting-started/configuration.md @@ -20,6 +20,9 @@ HappyView is configured via environment variables. A `.env` file in the project | `TOKEN_ENCRYPTION_KEY` | no | --- | Base64-encoded 32-byte key for encrypting stored OAuth tokens. **Strongly recommended in production** | | `DEFAULT_RATE_LIMIT_CAPACITY` | no | `100` | Default token bucket capacity used when registering a new API client | | `DEFAULT_RATE_LIMIT_REFILL_RATE` | no | `2.0` | Default token bucket refill rate (tokens/second) for new API clients | +| `ATTESTATION_PRIVATE_KEY` | no | auto-generated | Hex-encoded 32-byte secp256k1 private key for [attestation signing](../guides/features/attestation-signing.md). Auto-generated and persisted to database on first run | +| `ATTESTATION_KEY_ID` | no | `did:web:{host}#attestation` | Key identifier included in attestation signatures. Derived from `PUBLIC_URL` by default | +| `ATTESTATION_SIG_TYPE` | no | app-specific NSID | `$type` value used in attestation signature objects | | `RUST_LOG` | no | `happyview=debug,tower_http=debug` | Log filter (uses `tracing_subscriber::EnvFilter`) | | `APP_NAME` | no | --- | Application name shown on OAuth authorization screens. Overridden by database setting if set via admin API | | `LOGO_URI` | no | --- | URL to application logo for OAuth screens. Overridden by database setting or logo upload | diff --git a/packages/docs/docs/guides/features/attestation-signing.md b/packages/docs/docs/guides/features/attestation-signing.md new file mode 100644 index 0000000..3348c2f --- /dev/null +++ b/packages/docs/docs/guides/features/attestation-signing.md @@ -0,0 +1,109 @@ +# Attestation Signing + +HappyView can sign records with an ECDSA (secp256k1) keypair so their origin can be verified later. Lua scripts call `atproto.sign()` to attach an inline signature to a record and `atproto.verify_signature()` to check one. HappyView's implementation follows the [atproto attestation spec](https://tangled.org/strings/did:plc:cbkjy5n7bk3ax2wplmtjofq2/3m3fy2xuahc22). + +## How it works + +1. HappyView loads or generates a secp256k1 keypair on startup +2. `atproto.sign(record)` encodes the record to DAG-CBOR, computes its CID, and signs the CID with the private key +3. The signature is added to the record's `signatures` array as an inline object +4. `atproto.verify_signature(record, sig, repo_did)` recomputes the CID and verifies the signature + +The repo DID is included in the signed data — a signature for one user's record can't be replayed against another's. Any modification to the record invalidates the signature. + +## Setup + +Attestation signing is enabled by default — HappyView generates a keypair on first startup and persists it to the `instance_settings` database table. No configuration is required. + +To use an explicit key instead, set the `ATTESTATION_PRIVATE_KEY` environment variable: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ATTESTATION_PRIVATE_KEY` | no | auto-generated | Hex-encoded 32-byte secp256k1 private key | +| `ATTESTATION_KEY_ID` | no | `did:web:{host}#attestation` | Key identifier included in signatures. Derived from `PUBLIC_URL` by default | +| `ATTESTATION_SIG_TYPE` | no | app-specific NSID | The `$type` value used in signature objects | + +The key ID defaults to a `did:web` derived from your `PUBLIC_URL`. For example, `PUBLIC_URL=https://happyview.example.com` produces a key ID of `did:web:happyview.example.com#attestation`. + +### Priority order + +HappyView checks for signing configuration in this order: + +1. **Environment variables** — if `ATTESTATION_PRIVATE_KEY` is set, it's used +2. **Database** — if previously generated keys exist in `instance_settings`, they're loaded +3. **Auto-generation** — a new key is generated and persisted to the database + +If key loading fails for any reason, signing is disabled and `atproto.sign` / `atproto.verify_signature` will be `nil` in Lua scripts. + +## Using in Lua scripts + +Available in queries, procedures, and index hooks via the [atproto API](../../reference/lua/atproto-api.md). + +### Signing a record + +```lua +function handle() + local r = Record(collection, input) + r:save() + + local sig = atproto.sign({ text = input.text, createdAt = input.createdAt }) + return { uri = r._uri, cid = r._cid, signature = sig } +end +``` + +The returned signature object: + +```json +{ + "$type": "your.app.attestation", + "key": "did:web:happyview.example.com#attestation", + "signature": { + "$bytes": "base64-encoded-signature" + } +} +``` + +### Verifying a signature + +```lua +function handle() + local record = db.get(params.uri) + if not record then + return { error = "not found" } + end + + local sig = record.signatures and record.signatures[1] + if not sig then + return { record = record, verified = false } + end + + local valid = atproto.verify_signature(record, sig, record.did) + return { record = record, verified = valid } +end +``` + +### Checking availability + +Both functions are `nil` when no signer is configured: + +```lua +if atproto.sign then + record.signature = atproto.sign(record) +end +``` + +## Signature format + +Signatures are stored as objects in the record's `signatures` array: + +| Field | Type | Description | +| ----------- | ------ | ------------------------------------ | +| `$type` | string | Signature type NSID | +| `key` | string | Key identifier (DID with fragment) | +| `signature` | table | Contains `$bytes` (base64-encoded) | + +## Next steps + +- [atproto API reference](../../reference/lua/atproto-api.md#atprotosign) — `atproto.sign` and `atproto.verify_signature` parameter docs +- [Signed Record](../scripting/signed-record.md) — save a record with an attestation signature +- [Verify Signed Record](../scripting/signed-record-verify.md) — fetch a record and verify its signature diff --git a/packages/docs/docs/guides/indexing/index-hooks.md b/packages/docs/docs/guides/indexing/index-hooks.md index c00e3b9..a7f37d1 100644 --- a/packages/docs/docs/guides/indexing/index-hooks.md +++ b/packages/docs/docs/guides/indexing/index-hooks.md @@ -59,8 +59,11 @@ Index hooks have access to: - **[Database API](../../reference/lua/database-api.md)** — `db.query`, `db.get`, `db.search`, `db.backlinks`, `db.count`, `db.raw` - **[HTTP API](../../reference/lua/http-api.md)** — `http.get`, `http.post`, `http.put`, `http.patch`, `http.delete`, `http.head` +- **[XRPC Lua API](../../reference/lua/xrpc-lua-api.md)** — `xrpc.query`, `xrpc.procedure` +- **[atproto API](../../reference/lua/atproto-api.md)** — `atproto.resolve_service_endpoint`, `atproto.get_labels`, `atproto.get_labels_batch` - **[JSON API](../../reference/lua/json-api.md)** — `json.encode`, `json.decode` - **[Utility globals](../scripting.md#utility-globals)** — `log()`, `now()`, `TID()`, `toarray()` +- **[Script variables](../../reference/admin/script-variables.md)** — `env` table with key-value pairs configured in the dashboard ## Error handling and retries diff --git a/packages/docs/docs/guides/scripting.md b/packages/docs/docs/guides/scripting.md index 2ea70db..a8dc6fe 100644 --- a/packages/docs/docs/guides/scripting.md +++ b/packages/docs/docs/guides/scripting.md @@ -43,22 +43,25 @@ These globals are set automatically before `handle()` is called. ### Procedure globals -| Global | Type | Description | -| ------------ | ------ | ------------------------------------------------------- | -| `method` | string | The XRPC method name (e.g. `xyz.statusphere.setStatus`) | -| `input` | table | Parsed JSON request body | -| `caller_did` | string | DID of the authenticated user | -| `collection` | string | Target collection NSID | +| Global | Type | Description | +| -------------- | ------- | ------------------------------------------------------- | +| `method` | string | The XRPC method name (e.g. `xyz.statusphere.setStatus`) | +| `input` | table | Parsed JSON request body | +| `params` | table | Query string parameters | +| `caller_did` | string | DID of the authenticated user | +| `collection` | string | Target collection NSID | +| `delegate_did` | string? | DID of the delegated account, if using write delegation | +| `env` | table | Script variables configured in the dashboard | ### Query globals -| Global | Type | Description | -| ------------ | ------ | ------------------------------------------------ | -| `method` | string | The XRPC method name | -| `params` | table | Query string parameters (all values are strings) | -| `collection` | string | Target collection NSID | - -Queries are unauthenticated: there is no `caller_did` or `input`. +| Global | Type | Description | +| ------------ | ------- | ------------------------------------------------ | +| `method` | string | The XRPC method name | +| `params` | table | Query string parameters (all values are strings) | +| `collection` | string | Target collection NSID | +| `caller_did` | string? | DID of the authenticated user (nil if unauthenticated) | +| `env` | table | Script variables configured in the dashboard | ## Utility globals @@ -127,11 +130,24 @@ local resp = http.get("https://api.example.com/data") local data = json.decode(resp.body) ``` +## XRPC Lua API + +The `xrpc` table lets scripts call other XRPC endpoints — both local and proxied. Available in both queries and procedures. + +See the full [XRPC Lua API reference](../reference/lua/xrpc-lua-api.md) for `xrpc.query` and `xrpc.procedure`. + +Quick example: + +```lua +local resp = xrpc.query("xyz.statusphere.listStatuses", { limit = 5 }) +local data = json.decode(resp.body) +``` + ## atproto API -The `atproto` table provides atproto utility functions like DID resolution and label queries. +The `atproto` table provides atproto utility functions like DID resolution, label queries, and record signing. -See the full [atproto API reference](../reference/lua/atproto-api.md) for `atproto.resolve_service_endpoint`, `atproto.get_labels`, and `atproto.get_labels_batch`. +See the full [atproto API reference](../reference/lua/atproto-api.md) for `atproto.resolve_service_endpoint`, `atproto.get_labels`, `atproto.get_labels_batch`, `atproto.sign`, and `atproto.verify_signature`. ## JSON API @@ -181,6 +197,7 @@ See the example script references for complete, ready-to-use scripts: - [Paginated list](scripting/paginated-list.md) — list records with cursor-based pagination and DID filtering - [List or fetch](scripting/list-or-fetch.md) — combined single-record lookup and paginated listing - [Expanded query](scripting/expanded-query.md) — list statuses with user profiles in a single response +- [Verify signed record](scripting/signed-record-verify.md) — fetch a record and verify its attestation signature **Procedures:** - [Create a record](scripting/create-record.md) — simple write that saves input as a record @@ -190,6 +207,7 @@ See the example script references for complete, ready-to-use scripts: - [Sidecar records](scripting/sidecar-records.md) — create linked records across collections with a shared rkey - [Cascading delete](scripting/cascading-delete.md) — delete a record and all related records - [Complex mutations](scripting/complex-mutations.md) — load, transform, and save a record with multiple field changes +- [Signed record](scripting/signed-record.md) — save a record with an attestation signature **Index Hooks:** - [Algolia sync](scripting/algolia-sync.md) — push records to an Algolia search index on create/update/delete diff --git a/packages/docs/docs/guides/scripting/signed-record-verify.md b/packages/docs/docs/guides/scripting/signed-record-verify.md new file mode 100644 index 0000000..dc0dcd5 --- /dev/null +++ b/packages/docs/docs/guides/scripting/signed-record-verify.md @@ -0,0 +1,54 @@ +# Query: Verify Signed Record + +Fetch a record and verify its attestation signature. + +**Lexicon type:** query + +```lua +function handle() + local record = db.get(params.uri) + if not record then + return { error = "not found" } + end + + local verified = false + if atproto.verify_signature and record.signature then + verified = atproto.verify_signature( + { text = record.text, createdAt = record.createdAt }, + record.signature, + params.did + ) + end + + return { record = record, verified = verified } +end +``` + +## How it works + +1. Fetch the record by AT URI. +2. If a signature is present, rebuild the same field table that was signed and verify it with [`atproto.verify_signature()`](../../reference/lua/atproto-api.md#atprotoverify_signature). +3. Return `verified = true` if the signature is valid, `false` if it's missing, invalid, or the signer isn't configured. + +## Usage + +```sh +curl "http://127.0.0.1:3000/xrpc/xyz.example.getPost?uri=at://did:plc:abc/xyz.example.post/3abc123&did=did:plc:abc" +``` + +```json +{ + "record": { + "uri": "at://did:plc:abc/xyz.example.post/3abc123", + "text": "Hello world", + "createdAt": "2026-04-30T12:00:00Z" + }, + "verified": true +} +``` + +## Use case + +Pair this with the [Signed Record](signed-record.md) procedure to create a write-then-verify flow. The query re-derives the CID from the same fields that were originally signed, so any tampering between write and read is caught. + +See [Attestation Signing](../features/attestation-signing.md) for setup and configuration. diff --git a/packages/docs/docs/guides/scripting/signed-record.md b/packages/docs/docs/guides/scripting/signed-record.md new file mode 100644 index 0000000..448bc55 --- /dev/null +++ b/packages/docs/docs/guides/scripting/signed-record.md @@ -0,0 +1,56 @@ +# Procedure: Signed Record + +Save a record with an attestation signature attached. + +**Lexicon type:** procedure + +```lua +function handle() + local r = Record(collection, { + text = input.text, + createdAt = now(), + }) + r:save() + + local sig = nil + if atproto.sign then + sig = atproto.sign({ text = input.text, createdAt = r.createdAt }) + end + + return { uri = r._uri, cid = r._cid, signature = sig } +end +``` + +## How it works + +1. Create and save the record. +2. Sign the record fields with [`atproto.sign()`](../../reference/lua/atproto-api.md#atprotosign). The `nil` guard lets the script work without a signer configured. +3. Return the signature alongside the URI. + +## Usage + +```sh +curl -X POST http://127.0.0.1:3000/xrpc/xyz.example.createPost \ + -H "X-Client-Key: $CLIENT_KEY" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "text": "Hello world" }' +``` + +```json +{ + "uri": "at://did:plc:abc/xyz.example.post/3abc123", + "cid": "bafyrei...", + "signature": { + "$type": "your.app.attestation", + "key": "did:web:happyview.example.com#attestation", + "signature": { "$bytes": "..." } + } +} +``` + +## Use case + +Attestation signatures let clients verify that a record was processed by your HappyView instance — useful for contributions, moderation decisions, or cross-instance data where provenance matters. The signature covers both the record content and the author's DID, so it can't be replayed across users or tampered with. + +See [Attestation Signing](../features/attestation-signing.md) for setup and configuration, or [Verify Signed Record](signed-record-verify.md) for the read-side counterpart. diff --git a/packages/docs/docs/reference/lua/atproto-api.md b/packages/docs/docs/reference/lua/atproto-api.md index 271e965..9286b7f 100644 --- a/packages/docs/docs/reference/lua/atproto-api.md +++ b/packages/docs/docs/reference/lua/atproto-api.md @@ -10,9 +10,9 @@ local endpoint = atproto.resolve_service_endpoint(did) Resolves a DID to its atproto service endpoint URL by fetching the DID document. Supports both `did:plc:*` (via the PLC directory) and `did:web:*` (via `.well-known/did.json`). -| Parameter | Type | Description | -| --------- | ------ | ------------------------ | -| `did` | string | The DID to resolve | +| Parameter | Type | Description | +| --------- | ------ | ------------------ | +| `did` | string | The DID to resolve | **Returns:** The service endpoint URL as a string, or `nil` if resolution fails (DID not found, no PDS service in document, network error). @@ -49,18 +49,18 @@ local labels = atproto.get_labels(uri) Returns an array of labels for a single AT URI. Merges external labels (from subscribed labelers) with self-labels (from the record's `labels.values[]` field). -| Parameter | Type | Description | -| --------- | ------ | ------------------------------ | -| `uri` | string | AT URI of the record to query | +| Parameter | Type | Description | +| --------- | ------ | ----------------------------- | +| `uri` | string | AT URI of the record to query | Each label in the array is a table with: -| Field | Type | Description | -| ----- | ------ | ---------------------------------------- | -| `src` | string | DID of the labeler (or record author) | -| `uri` | string | AT URI this label applies to | -| `val` | string | Label value (e.g. "nsfw", "!hide") | -| `cts` | string | Timestamp when the label was created | +| Field | Type | Description | +| ----- | ------ | ------------------------------------- | +| `src` | string | DID of the labeler (or record author) | +| `uri` | string | AT URI this label applies to | +| `val` | string | Label value (e.g. "nsfw", "!hide") | +| `cts` | string | Timestamp when the label was created | Expired labels are automatically filtered out. Returns an empty array if no labels exist. @@ -72,9 +72,9 @@ local labels_by_uri = atproto.get_labels_batch(uris) Batch version of `get_labels`. Takes an array of AT URIs and returns a table keyed by URI, where each value is an array of labels. -| Parameter | Type | Description | -| --------- | ----- | ------------------------ | -| `uris` | table | Array of AT URI strings | +| Parameter | Type | Description | +| --------- | ----- | ----------------------- | +| `uris` | table | Array of AT URI strings | **Returns:** A table keyed by URI. Each value is an array of label tables (same shape as `get_labels`). URIs with no labels have an empty array. @@ -105,3 +105,65 @@ for _, uri in ipairs(uris) do end end ``` + +## atproto.sign + +```lua +local sig = atproto.sign(record) +``` + +Signs a record and returns the inline signature object. Only available when an attestation signer is configured — if no signer is configured, `atproto.sign` is `nil`. + +| Parameter | Type | Description | +| --------- | ----- | ----------------------- | +| `record` | table | The record data to sign | + +**Returns:** A signature table with: + +| Field | Type | Description | +| ----------- | ------ | --------------------------------------------------- | +| `key` | string | The signing key ID (e.g. `did:web:example#signing`) | +| `signature` | table | Contains `$bytes` with the signature | + +### Examples + +```lua +-- Sign a record before returning it +local record = { contributionType = "correction", changes = { name = "Test" } } +local sig = atproto.sign(record) +record.signature = sig +return record + +-- Check if signing is available +if atproto.sign then + local sig = atproto.sign(record) +end +``` + +## atproto.verify_signature + +```lua +local valid = atproto.verify_signature(record, signature, repo_did) +``` + +Verifies that an inline signature was produced by this HappyView instance. Only available when an attestation signer is configured — if no signer is configured, `atproto.verify_signature` is `nil`. + +| Parameter | Type | Description | +| ----------- | ------ | ------------------------------------------ | +| `record` | table | The record data | +| `signature` | table | The signature object from `atproto.sign()` | +| `repo_did` | string | The repo DID | + +**Returns:** `true` if the signature is valid, `false` otherwise. Returns `false` on failure rather than raising an error. + +### Examples + +```lua +-- Verify a signature roundtrip +local record = { contributionType = "correction", changes = { name = "Test" } } +local sig = atproto.sign(record) +local valid = atproto.verify_signature(record, sig, caller_did) +if not valid then + return { error = "signature verification failed" } +end +``` diff --git a/packages/docs/docs/reference/lua/database-api.md b/packages/docs/docs/reference/lua/database-api.md index f04667f..c6797c7 100644 --- a/packages/docs/docs/reference/lua/database-api.md +++ b/packages/docs/docs/reference/lua/database-api.md @@ -95,7 +95,7 @@ Parameters are passed as an array and bound to `$1`, `$2`, etc. Supported parame ### SQL dialect -Write SQL in **SQLite syntax** — HappyView translates it to Postgres at runtime if you're using Postgres. See [Database Setup](../../guides/database/database-setup.md) for details on what gets translated. If you need database-specific SQL that can't be translated, check `db.is_postgres()` at runtime. +Write SQL in **SQLite syntax** — HappyView translates it to Postgres at runtime if you're using Postgres. See [Database Setup](../../guides/database/database-setup.md) for details on what gets translated. If you need database-specific SQL that can't be translated, check `db.backend()` at runtime. ### Column type mapping @@ -108,3 +108,21 @@ Write SQL in **SQLite syntax** — HappyView translates it to Postgres at runtim | `TEXT` (JSON) | `JSON`, `JSONB` | table | | `TEXT` (ISO 8601) | `TIMESTAMPTZ` | string (ISO 8601) | | Other | Other | string (fallback) | + +## db.backend + +```lua +local backend = db.backend() +-- "sqlite" or "postgres" +``` + +Returns `"sqlite"` or `"postgres"`. Useful when you need database-specific SQL that can't be automatically translated. + +```lua +if db.backend() == "postgres" then + db.raw("SELECT * FROM records WHERE record @> $1::jsonb", { json.encode({ status = "active" }) }) +else + -- SQLite fallback + db.raw("SELECT * FROM records WHERE json_extract(record, '$.status') = $1", { "active" }) +end +``` diff --git a/packages/docs/docs/reference/lua/xrpc-lua-api.md b/packages/docs/docs/reference/lua/xrpc-lua-api.md new file mode 100644 index 0000000..f0a9a85 --- /dev/null +++ b/packages/docs/docs/reference/lua/xrpc-lua-api.md @@ -0,0 +1,76 @@ +# XRPC Lua API + +The `xrpc` table provides cross-endpoint XRPC calls. Available in queries, procedures, and [index hooks](../../guides/indexing/index-hooks.md). + +## xrpc.query + +```lua +local resp = xrpc.query("xyz.statusphere.listStatuses", { -- required: XRPC method name + limit = 10, -- optional: query parameters +}) +``` + +Calls an XRPC query. If the method matches a locally registered query lexicon, it runs locally. Otherwise, the request is proxied to the NSID's authority. + +**Returns:** A table with: + +| Field | Type | Description | +| -------- | ------- | -------------------- | +| `status` | integer | HTTP status code | +| `body` | string | Response body (JSON) | + +The body is a raw JSON string — use `json.decode(resp.body)` to parse it. + +### Examples + +```lua +-- Call a local query endpoint +local resp = xrpc.query("xyz.statusphere.listStatuses", { limit = 5 }) +local data = json.decode(resp.body) +for _, record in ipairs(data.records) do + log(record.uri) +end + +-- Call without parameters +local resp = xrpc.query("com.example.getConfig") + +-- Proxy to a remote XRPC endpoint +local resp = xrpc.query("app.bsky.feed.getAuthorFeed", { + actor = "did:plc:abc123", + limit = 10, +}) +``` + +## xrpc.procedure + +```lua +local resp = xrpc.procedure( + "xyz.statusphere.setStatus", -- required: XRPC method name + { status = "hello" }, -- required: request body + { someParam = "value" } -- optional: query parameters +) +``` + +Calls an XRPC procedure using the current request's `caller_did` for authentication. If the method matches a locally registered procedure lexicon, it runs locally. Otherwise, the request is proxied. + +Requires a `caller_did` — raises an error without one. + +**Returns:** A table with the same shape as `xrpc.query` responses (`status` and `body`). + +### Examples + +```lua +-- Call a local procedure +local resp = xrpc.procedure("xyz.statusphere.setStatus", { + status = "hello", + createdAt = now(), +}) + +if resp.status ~= 200 then + return { error = "failed: " .. resp.body } +end + +-- Parse the response +local result = json.decode(resp.body) +return { uri = result.uri } +``` diff --git a/packages/docs/sidebars.ts b/packages/docs/sidebars.ts index 60dd611..cdcfed2 100644 --- a/packages/docs/sidebars.ts +++ b/packages/docs/sidebars.ts @@ -88,6 +88,11 @@ const sidebars: SidebarsConfig = { id: "guides/features/api-clients", label: "API Clients", }, + { + type: "doc", + id: "guides/features/attestation-signing", + label: "Attestation Signing", + }, { type: "doc", id: "guides/features/labelers", @@ -194,6 +199,16 @@ const sidebars: SidebarsConfig = { id: "guides/scripting/complex-mutations", label: "Complex Mutations", }, + { + type: "doc", + id: "guides/scripting/signed-record", + label: "Signed Record", + }, + { + type: "doc", + id: "guides/scripting/signed-record-verify", + label: "Verify Signed Record", + }, { type: "doc", id: "guides/scripting/algolia-sync", @@ -393,6 +408,11 @@ const sidebars: SidebarsConfig = { id: "reference/lua/http-api", label: "HTTP API", }, + { + type: "doc", + id: "reference/lua/xrpc-lua-api", + label: "XRPC Lua API", + }, { type: "doc", id: "reference/lua/atproto-api", -- 2.51.2