solid-pod-rs 0.4.0-alpha.15

Rust-native Solid Pod server library — LDP, WAC, WebID, Solid-OIDC, Solid Notifications, NIP-98. Framework-agnostic.
Documentation
# HTTP endpoint reference

solid-pod-rs is framework-agnostic: it does not ship an HTTP server.
This page describes the endpoint surface your integration should
expose, derived from the example in
[`examples/standalone.rs`](../../examples/standalone.rs) and the LDP
specification.

## Method matrix

| Path kind                    | GET | HEAD | PUT | POST | DELETE | PATCH |
|------------------------------|-----|------|-----|------|--------|-------|
| Pod root `/`                 | ✓ container || ✗ 405 || ✗ 405 | ✗ 405 |
| Container `/c/`              | ✓ container || ✗ 405 || ✓ if empty | ✗ 405 |
| Resource `/c/r`              | ✓ body ||| ✗ 405 || ✓ N3 or SPARQL |
| ACL sidecar `/c/r.acl`       ||| ✓ (acl:Control required) ||||
| Meta sidecar `/c/r.meta`     ||| server-managed ||||
| `.well-known/openid-configuration` |||||||
| `.notifications`             |||||||
| `.notifications/websocket`   |||| ✓ (subscribe) |||
| `.notifications/webhook`     |||| ✓ (subscribe) |||
| `/.well-known/solid`         | partial (OIDC discovery only) | | | | | |

## Response status codes

| Code | Use |
|---|---|
| 200 OK | `GET` / `HEAD` success |
| 201 Created | `PUT` (new resource) or `POST` (Slug → child) |
| 204 No Content | `DELETE`, successful `PATCH` |
| 400 Bad Request | Malformed body, invalid path, base64/hex/JSON decode error |
| 401 Unauthorized | Missing or invalid NIP-98 / OIDC token (send `WWW-Authenticate`) |
| 403 Forbidden | ACL evaluator denied |
| 404 Not Found | `PodError::NotFound` |
| 405 Method Not Allowed | See matrix |
| 409 Conflict | `PodError::AlreadyExists` — usually only for idempotent-create semantics |
| 412 Precondition Failed | N3 PATCH `where` clause missed, or `If-Match` mismatch |
| 415 Unsupported Media Type | Unknown `Content-Type` for resource kind or PATCH |
| 500 Internal Server Error | I/O, backend, or `PodError::Backend` |

See [reference/error-codes.md](error-codes.md) for the `PodError` →
status mapping.

## Response headers (per method / path kind)

### All non-error responses

- `Link` — see [reference/link-headers.md]link-headers.md.
- `WAC-Allow` — derived via `wac::wac_allow_header`. Shape
  `user="…", public="…"`.
- `ETag` — strong validator on resource bodies (SHA-256 hex).
- `Accept-Post` — on containers: `text/turtle, application/ld+json,
  application/n-triples` (`ldp::ACCEPT_POST`).
- `Content-Type` — passed through from `ResourceMeta.content_type` for
  resources; `application/ld+json` or the negotiated RDF format for
  containers.

### `401`

```
WWW-Authenticate: Nostr
WWW-Authenticate: DPoP algs="ES256 RS256"
```

## Request headers the server honours

| Header | Effect |
|---|---|
| `Authorization: Nostr <b64>` | NIP-98 authentication — `auth::nip98::verify`. |
| `Authorization: DPoP <token>` | Solid-OIDC access token (feature `oidc`). |
| `DPoP` | DPoP proof — `oidc::verify_dpop_proof`. |
| `Content-Type` | Stored verbatim on `PUT`; selects PATCH dialect (`text/n3` or `application/sparql-update`). |
| `Accept` | Drives RDF format for container GET — `ldp::negotiate_format`. |
| `Prefer` | Controls container representation — `ldp::PreferHeader::parse`. See [reference/prefer-headers.md]prefer-headers.md. |
| `Slug` | On `POST` to container: UTF-8 child name. Rejected if contains `/` or `..`. |
| `If-Match` / `If-None-Match` | P2 item — the storage layer returns canonical ETags; middleware enforces. |

## Path conventions

- Container paths end with `/`; resources do not.
- ACL sidecar for a resource `/c/r` lives at `/c/r.acl`.
- ACL sidecar for a container `/c/` lives at `/c/.acl`.
- Meta sidecar for `/c/r` lives at `/c/r.meta`.
- Pod root ACL lives at `/.acl`.
- Root path `/` is always a container.
- Paths are resolved case-sensitive.
- Backends MUST reject paths containing `..` or `\0`.

## Slug semantics on POST

```rust
pub fn resolve_slug(container: &str, slug: Option<&str>) -> String;
```

- If `slug` is `Some(s)` and `s` is non-empty, contains no `/`, and
  contains no `..`: append `s` to `container`.
- Otherwise: append a fresh UUID v4.

## Discovery endpoints

### `GET /.well-known/openid-configuration`

Feature `oidc` required. Build with `oidc::discovery_for(issuer)`.

### `GET /.notifications`

Returns the subscription-discovery JSON-LD document. Build with
`notifications::discovery_document(pod_base)`.

```json
{
  "@context": ["https://www.w3.org/ns/solid/notifications-context/v1"],
  "id":            "https://pod.example/.notifications",
  "channelTypes": [
    { "id": "WebSocketChannel2023", "endpoint": ".../websocket", "features": ["as:Create","as:Update","as:Delete"] },
    { "id": "WebhookChannel2023",   "endpoint": ".../webhook",   "features": ["as:Create","as:Update","as:Delete"] }
  ]
}
```

## Admin / provisioning endpoints

These endpoints are only present in the `solid-pod-rs-server` binary and
require the `git` feature (or the standalone binary build with admin routes
compiled in).  They are **not** part of the core library surface.

### `POST /_admin/provision/{pubkey}`

Creates a new pod for the given owner public key.

| Attribute | Value |
|---|---|
| Auth | PSK — `X-Pod-Admin-Key: <secret>` header. Requests without a valid key receive `403 Forbidden`. The key must match `SOLID_ADMIN_KEY` / `--admin-key`. |
| Feature gate | Compiled only when `--features git` is passed (or the default server build that includes it). |
| Path parameter | `pubkey` — hex-encoded Nostr/secp256k1 public key of the future pod owner. |
| Request body | None. |

**Response `200 OK`:**

```json
{ "podUrl": "https://pods.example.com/<pubkey>/", "ok": true }
```

**What it does:**

1. Creates the pod directory under the configured storage root.
2. Writes an owner-only `.acl` granting full control to the pubkey.
3. Runs `git init -b main` and sets `receive.denyCurrentBranch=updateInstead`
   so the pod directory is a bare-ish working-tree repo that can receive
   `git push` over HTTP via `/_git/{pubkey}/`.

**Error responses:**

| Code | Condition |
|---|---|
| `400 Bad Request` | `pubkey` is not valid hex or fails secp256k1 key validation. |
| `403 Forbidden` | Missing or incorrect `X-Pod-Admin-Key`. |
| `409 Conflict` | Pod directory already exists. |
| `500 Internal Server Error` | I/O error or `git init` failure. |

**Security note:** This endpoint is intended for the CF Workers ↔ agentbox
handshake only.  Bind the server to a non-public interface or protect it
with a firewall; the PSK is a defence-in-depth measure, not a public API.

## Git Control Panel endpoints

Present only when built with `--features git`.

### `OPTIONS /_git/{pubkey}/{tail}`

CORS preflight handler for the Git HTTP smart-protocol routes used by the
forum's VS Code-style Source Control panel.

| Attribute | Value |
|---|---|
| Auth | None — OPTIONS responses are unauthenticated by design. |
| Feature gate | `git` feature. |
| Path | `/_git/{pubkey}/{tail}` — matches any sub-path under a pubkey's git namespace. |
| Request body | None. |

**Response `204 No Content`** with the following headers:

```
Access-Control-Allow-Origin:  <origin> | *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Pod-Admin-Key
Access-Control-Max-Age:       86400
```

The `Access-Control-Allow-Origin` value is determined by the
`SOLID_ALLOWED_ORIGINS` / `--allowed-origins` list.  If the request
`Origin` is present in the allowlist it is echoed back verbatim;
otherwise `*` is returned when the allowlist is empty (dev default).
When the allowlist is non-empty and the origin is not on it, `*` is
**not** returned — the header is omitted so the browser blocks the
preflight.

## See also

- [reference/api.md]api.md — the Rust API backing each endpoint.
- [reference/link-headers.md]link-headers.md
- [reference/prefer-headers.md]prefer-headers.md
- [reference/content-types.md]content-types.md
- [reference/patch-semantics.md]patch-semantics.md