From d733e2d34001f132b4c617173bdb1264aca804f9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 9 Sep 2026 07:36:00 -0500 Subject: [PATCH] =?UTF-8?q?spec:=20hoisted=20error=20responses,=20terse=20?= =?UTF-8?q?descriptions,=20curl=20samples=20=E2=80=94=20release=20v0.12.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-sync from platform #3284 (tight OAS: hoisted error responses, real home-space examples, llms.txt) and #3285 (terse description pass, Scalar curl code samples). Consumer-visible: - No success shape, request body, parameter, security, or schema definition changed. Same 41 operations. - New `ErrorEnvelope` schema; 12 named error responses under `components.responses`; every operation gains a `default` response. - The inline error unions no longer carry the leaked `defined: true|false` discriminator. Error bodies remain `code`, `status`, `message`, `data` with const-narrowed codes. - Snapshot shrinks ~36% from description trimming. Test fixture for the 429 path updated to the documented RateLimited envelope (which now requires `data`). Minor bump per the version rule: additive type surface, pre-1.0. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Rpq64ASTsKJ3xc2DVoQMjh --- openapi.json | 19776 +++++++++++------------------------------ package.json | 2 +- src/generated/api.ts | 8719 ++++++------------ tests/client.spec.ts | 7 +- 4 files changed, 7874 insertions(+), 20630 deletions(-) diff --git a/openapi.json b/openapi.json index 3582096..fca3a82 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "info": { "title": "The DFOS API", "version": "1.0.0-beta", - "description": "The external HTTP API for Dark Forest OS — space profiles and public feeds,\nposts and pages, topics, releases and products, calendar events, and public user\nprofiles.\n\nEvery endpoint that reads data is a `GET`, and there are **three route classes**\nplus a small enumerated set of WRITES (see \"Writing\" below).\n\n**Anonymous** is the default and most of the surface. Anonymous responses are\nviewer-independent — the anonymous audience is the same for everyone, so there is\nexactly one correct projection of any resource.\n\n**Gated** routes need a proof and describe one person. `GET /profile` serves the\nprofile of the user who granted access, `GET /memberships` and\n`GET /group-memberships` serve the spaces and groups they belong to (with\n`GET /membership/{space}` and `GET /group-membership/{group}` as one-request\nchecks), `GET /credential` describes the presented credential itself,\n`GET /feed` is their cross-space post feed, and\n`GET /spaces/{space}/posts/{postId}/comments` is a post's comment thread as they\nsee it. These are the deliberate carve-out from viewer-independence: their bodies\nare a specific person's data, selected by the credential presented, and they are\n`no-store` like every other data response.\n\n**Optional-auth** is the third class, and today it is the two space post routes —\n`GET /spaces/{space}/posts` and `GET /spaces/{space}/posts/{postId}`. They\nserve the anonymous projection to everybody, and to a caller whose credential\ncovers that space with `read:posts`, the projection the granting user sees:\ntheir whole feed for that space, full bodies and attachments where they genuinely\nread a post, and a `viewer` block. Their `security` lists an EMPTY requirement\nobject first, which is how OpenAPI spells \"authentication is optional\" — a client\nmay call them with nothing.\n\n**On an optional-auth route a grant only ever ADDS, and the four outcomes are\ndistinct.** No `Authorization` header serves the anonymous projection. A valid\nproof whose grant covers the space serves the member one. A valid proof whose\ngrant does NOT cover the space serves **exactly the anonymous projection, byte\nfor byte** — you hold a good credential that says nothing about this space, which\nis the same authority as no credential; nothing in the response distinguishes the\ntwo, and losing access to a space therefore looks like silence rather than an\nerror. A proof that is malformed, expired, revoked or forged is still a\n`401`/`403`: a broken credential is never quietly downgraded, so a client must\nnot read an anonymous-looking body as evidence that its authentication is fine.\n\n**Authenticating to the credential-gated routes.** Access is proof-of-possession, not a\nbearer token. The caller presents a durable **credential** the user issued (via\nSign In With DFOS) alongside a fresh **request proof** — a short-lived JWS,\nsigned by the key the credential was issued to, binding this exact method, host,\npath, and body:\n\n```\nAuthorization: DFOS \nX-Credential: \n```\n\nNeither works alone: a captured credential is inert without the audience key, and\na captured proof authorizes nothing but the single request it already described,\nfor the seconds its freshness window allows. A proof-layer failure is `401`\nwith a `WWW-Authenticate: DFOS` challenge (sign a correct proof and retry); a\ncredential-layer failure is `403` (the grant does not cover this — retrying\nwill not help). The full byte contract is\n[DFOS API-AUTH](https://protocol.dfos.com/api-auth).\n\n**Branch on the status and the error body, never on the challenge header.** The\n`401` always carries the machine-readable typed error body, and that body is the\ncontract. The header beside it is best-effort: the origin runs behind a Lambda\nFunction URL, which rewrites `WWW-Authenticate` to\n`x-amzn-remapped-www-authenticate` on the way out, and the canonical host puts\nthe standard spelling back at the CDN edge. A caller that reaches the origin by\nany other route can therefore see the remapped name instead, and a client keyed\non the header alone would read that as no challenge at all.\n\n**Reading your own data.** Every route that accepts a credential except\n`GET /credential` — the five own-data routes `GET /profile`,\n`GET /memberships`, `GET /membership/{space}`, `GET /group-memberships` and\n`GET /group-membership/{group}`, plus `GET /feed`, the post comments route, and\nthe optional-auth post routes — also accepts a bare **identity proof**:\n`Authorization: DFOS ` with no `X-Credential`, signed by one\nof your OWN identity keys. It authenticates the signing DID and nothing more, and\non these routes that opens exactly that DID's own data — with no space\nrestriction, because there is no third party for a grant to attenuate. This\ndocument marks those operations with the identity alternative, so a client\nholding its own key needs no credential for them, while a third-party app acting\nfor a user still presents the delegated pair. Presenting a credential alongside\nan identity proof is malformed (`401`): the two headers assert different claims\nand this API will not pick one.\n\n**Writing.** A small enumerated set of routes WRITE, as the granting user, in the\nspaces a grant covers: posts (`write:posts`), comments (`write:comments`), and\nupvotes on either (`write:upvotes`). They accept the same two authenticated\nprofiles as the gated reads — a bare identity proof, or the delegated pair — and\nadd three obligations no read has:\n\n- **A per-request `jti`.** Every write's proof MUST carry the registered `jti`\n member, and a write without one is a `401`. A proof is accepted for its whole\n freshness window, so replaying one on a read merely re-reads while replaying\n one on a write would execute it twice; the `jti` is what makes that\n impossible. `generateJti()` in `@metalabel/dfos-client` mints one, and\n `createApiAuthFetch` attaches one to every non-`GET` request by default.\n- **A `409` that means \"this already happened\".** Presenting a `jti` that was\n already spent inside the window is refused with `409`, and the status is the\n instruction: the earlier attempt may have SUCCEEDED, so re-read state and\n reconcile rather than retrying. A genuine retry must carry a NEW `jti`;\n resending the identical bytes will answer `409` until the window lapses.\n- **One body media type, uncompressed, with the method you signed.** A request\n body must be `application/json` (optionally `; charset=utf-8`) — exactly one\n media type is served, so the octets a proof binds have exactly one parse. A\n `Content-Encoding` other than `identity` is refused `415` (the proof commits\n to raw octets and this API will not reverse an encoding before checking that),\n and `X-HTTP-Method-Override` or a `?_method=` parameter is refused `400` (the\n proof binds the method).\n\n**What the write tier deliberately cannot do.** It reaches the granting user's\nOWN posts and comments and nothing else — another member's content is `403` even\nwhen that user is an admin of the space, because moderation is not on this\nsurface. And the fields that would let an app speak FOR a space are absent from\nthe request schemas rather than rejected by them: there is no announce, no\nbroadcast, no pin, no backdated `publishedAt`, no view-access override, no media\nattachment, and no moderation flag. Request bodies are closed, so naming a field\nthis API does not serve is a `400`.\n\n**Where a grant reaches.** A credential's `read:posts` is granted either across\nall the user's spaces — including ones they join later — or on a named set of\nthem. The API never reports which: a route the grant does not reach behaves as\ndescribed above for its class (the anonymous projection on an optional-auth\nroute, the ordinary not-found on a gated one). Read your own coverage from\n`GET /credential`, which is what it is for.\n\n**Adding a signing key (KEY-PROOF).** The one ANONYMOUS write on this API, and\nthe only one that is not part of the write tier above:\n`POST /key-proof/present` carries a signed envelope proving possession\nof a public key, against a ceremony the key's owner minted in the DFOS app.\n`GET /key-proof/status` is the poll that follows it. Both are anonymous — the\ntyp-scoped JWS envelope is self-authenticating, and the client presenting it is\na CLI that has no session yet by construction.\n\n**Presenting does not add the key.** It verifies the envelope and stores it; the\nidentity's owner then sees the key's fingerprint in their DFOS settings and\neither adopts it — which is what appends the operation — or rejects it. Poll\n`status` to find out which. A code carried to the wrong terminal therefore adds\nnothing to anybody's chain.\n\nThe discovery step is a well-known **outside the `/v1` prefix**, so it is not an\noperation in this document:\n\n```\nGET https://{this host}/.well-known/dfos-key-proof?code={the 8-character code}\n→ 200 {\n \"present\": \"…\", \"nonce\": \"…\", \"audience\": \"…\", \"purpose\": \"did:dfos:key-add\",\n \"adopts\": { \"did\": \"…\", \"handle\": \"…\", \"displayName\": \"…\" },\n \"roleSet\": \"auth,assert | controller\", \"prevCID\": \"…\", \"expiresAt\": \"…\", \"relay\": \"…\"\n }\n→ 404 { \"error\": \"unknown or expired code\" }\n```\n\n`present` is the endpoint to POST the envelope to — follow it verbatim, do not\nassemble it. `nonce`, `audience`, `did` (from `adopts`), `roleSet` and\n`prevCID` are five of the envelope's seven signed members, and they are supplied\nrather than chosen: the envelope binds the key to ONE introduction, on one chain,\nat one chain head, in ONE ROLE SET, so it is worthless anywhere else and there is\nno such thing as a proof held in reserve. `roleSet` in particular is not a\nconstant — `auth,assert` is an ordinary signing key and `controller` is a key\nregistered to carry the identity beyond this platform — so sign the set you were\nhanded, show it to your human, and never assume one. `adopts` also carries the identity's handle and display\nname because **a tool MUST show its human the identity and role set before it\nsigns** — a bare DID is not something a person recognizes. `relay` is the origin\nof the relay serving this deployment's identity index, so a client with no peer\nconfigured can still run its one-key-one-DID pre-flight on first use; treat it as\noptional and advisory (the platform re-checks authoritatively at adoption).\nUnknown and expired codes answer identically, so a code-guesser never learns it\nfound a real ceremony that had merely lapsed. Everything else about the flow is\non the operations themselves.\n\nData responses are `no-store` (only the generated spec is CDN-cached), so\nreferenced media in a public post comes back as time-limited SIGNED URLs inline —\nnever persist those, re-fetch the post for fresh ones (see `urlExpiresAt`). A\nnon-public or missing space returns a 404 that is deliberately indistinguishable\nfrom any other — the API never reveals whether a space it won't serve actually\nexists.\n\n**Forward-compatibility — clients MUST:**\n- **Tolerate unknown fields.** New response fields are added without a version\n bump; ignore fields you don't recognize.\n- **Tolerate unknown enum values.** Enums (e.g. `joinMode`, `siteMode`) can grow\n new members; treat an unrecognized value as an opaque string, not an error.\n- **Treat cursors as opaque tokens.** Pass `nextCursor`/`previousCursor` back\n verbatim; never parse or construct a cursor.\n- **Treat subdomains as mutable aliases.** A space's subdomain can change or be\n reassigned; the space `id` and protocol `did` are the canonical, stable\n identifiers to store.", + "description": "The HTTP API for Dark Forest OS at `https://api.dfos.com/v1/` — spaces, posts,\npages, events, products, and profiles. Guides: https://docs.dfos.com/api.\n\n**Anonymous** is the default and most of the surface; those responses are\nviewer-independent.\n\n**Gated** routes need a proof and describe one person: the granting user's\nprofile, memberships, credential, feed, and a post's comments.\n\n**Optional-auth** routes — `GET /spaces/{space}/posts` and\n`GET /spaces/{space}/posts/{postId}` — serve the anonymous projection to\neveryone, and the granting user's view to a credential covering that space with\n`read:posts`. A grant only ever adds: an uncovered grant returns the anonymous\nbody byte for byte, and a bad proof is still a `401`/`403`.\n\n**Authenticating.** Access is proof-of-possession: the credential the user\nissued through Sign In With DFOS, plus a fresh request proof signed by the key\nit was issued to, binding this exact method, host, path, and body. See\n[DFOS API-AUTH](https://protocol.dfos.com/integrations#api-authentication).\n\n```\nAuthorization: DFOS \nX-Credential: \n```\n\nTo reach your own data a bare identity proof is enough: `Authorization: DFOS\n` with no `X-Credential`, signed by one of your own keys.\nPresenting both is malformed (`401`).\n\n`401` means the proof failed, so sign a correct one; `403` means the grant does\nnot cover this. `WWW-Authenticate` is best-effort — infrastructure between your\nclient and this API can rename or drop it — so branch on the status and the\nerror body.\n\n**Writing.** Posts (`write:posts`), comments (`write:comments`), and upvotes on\neither (`write:upvotes`) write as the granting user, in the spaces a grant\ncovers:\n\n- Every write proof carries a `jti`; without one, `401`.\n- A spent `jti` is `409`: the first attempt may have succeeded, so re-read state\n and retry with a new `jti`.\n- Bodies are `application/json` and uncompressed: another media type or a\n `Content-Encoding` other than `identity` is `415`, and\n `X-HTTP-Method-Override` or `?_method=` is `400`.\n- Own content only, text only — no announce, broadcast, pin, backdate,\n view-access override, media, or moderation. Bodies are closed: an unknown\n member is a `400`.\n\n**Adding a signing key.** `POST /key-proof/present` presents a signed envelope\nproving possession of a public key, against a ceremony minted in the DFOS app;\n`GET /key-proof/status` polls it. Presenting does not add the key: the owner\nadopts or rejects it in DFOS settings. Discovery is a well-known outside `/v1`:\n\n```\nGET https://{this host}/.well-known/dfos-key-proof?code={the 8-character code}\n→ 200 {\n \"present\": \"…\", \"nonce\": \"…\", \"audience\": \"…\", \"purpose\": \"…\",\n \"roleSet\": \"…\", \"prevCID\": \"…\", \"expiresAt\": \"…\", \"relay\": \"…\",\n \"adopts\": { \"did\": \"…\", \"handle\": \"…\", \"displayName\": \"…\" }\n }\n→ 404 { \"error\": \"unknown or expired code\" }\n```\n\nPOST to `present` verbatim. `nonce`, `audience`, `did`, `prevCID`\nand `roleSet` (`auth,assert` or `controller`) are supplied, not chosen — sign\nthe set you were handed, and show your human the identity and role set first.\n`relay` is the origin of the relay serving this deployment's identity index —\nadvisory only; the platform re-checks authoritatively at adoption. Unknown and\nexpired codes answer identically.\n\n- Tolerate unknown fields and unknown enum values.\n- Cursors are opaque: pass `nextCursor`/`previousCursor` back verbatim.\n- A space's `id` and protocol `did` are canonical; its subdomain is a mutable\n alias.\n- A non-public space and a missing one return the same `404`.\n- Data responses are `no-store`. Signed media URLs are ephemeral: re-fetch\n rather than persist (see `urlExpiresAt`).", "contact": { "name": "DFOS", "url": "https://docs.dfos.com/api" @@ -15,28 +15,28 @@ "type": "http", "scheme": "dfos", "x-dfos-typ": "did:dfos:identity-proof", - "description": "An identity proof: a short-lived JWS signed by one of the caller's OWN identity keys, binding this exact method, host, path, and body. Carried as `Authorization: DFOS ` with NO `X-Credential` — presenting a credential alongside one on this surface is malformed, because the two headers would assert two different claims at once. It authenticates the signer's DID and nothing more; on this surface that opens the signer's own data, and only their own data. See https://protocol.dfos.com/api-auth#the-identity-proof." + "description": "An identity proof: a short-lived JWS signed by one of the caller's own identity keys, binding this exact method, host, path, and body, and carried as `Authorization: DFOS ` with no `X-Credential` (presenting both is malformed). It authenticates the signer's DID and opens only that DID's own data — see https://protocol.dfos.com/integrations#the-identity-proof." }, "dfosRequestProof": { "type": "http", "scheme": "dfos", "x-dfos-typ": "did:dfos:request-proof", "x-dfos-actions": { - "read:profile": "Read the granting user's own profile — handle, display name, bio, avatar, and join date. The account email address is excluded; that is `read:email`.", + "read:profile": "Read the granting user's profile — handle, display name, bio, avatar, and join date, but not their email address.", "read:email": "Read the granting user's account email address.", - "read:memberships": "Read the granting user's own memberships — the spaces and groups the account belongs to, and its roles in them, private and unlisted spaces included.", - "read:posts": "Read posts and their comments as the granting user sees them, in the spaces this grant covers — including private spaces they belong to and content they have paid for. Comment visibility follows the post, so there is no separate token for it.", - "write:upvotes": "Upvote and un-upvote posts and comments AS the granting user, in the spaces this grant covers. It writes their name onto a signal other members can see.", - "write:comments": "Write, edit, and delete the granting user's OWN comments, in the spaces this grant covers. It never reaches anyone else's comments and never moderates.", - "write:posts": "Write, edit, and delete the granting user's OWN posts, in the spaces this grant covers. Text only: it never announces, broadcasts, pins, backdates, or moderates, and it never touches anyone else's posts." + "read:memberships": "Read the spaces and groups the granting user belongs to, and their roles in them, private and unlisted included.", + "read:posts": "Read posts and their comments as the granting user sees them, in the spaces this grant covers, including private spaces they belong to and content they have paid for.", + "write:upvotes": "Upvote and un-upvote posts and comments as the granting user, in the spaces this grant covers — their name on a signal other members can see.", + "write:comments": "Write, edit, and delete the granting user's own comments, in the spaces this grant covers — never anyone else's, and never moderation.", + "write:posts": "Write, edit, and delete the granting user's own posts, in the spaces this grant covers — text only, never announcing, broadcasting, pinning, backdating, or moderating, and never anyone else's." }, - "description": "A request proof: a short-lived JWS signed by the key the credential was issued to, binding this exact method, host, path, and body. Carried as `Authorization: DFOS `. Deliberately NOT `Bearer` — nothing here is a bearer token, and naming it one invites the logging, caching, and forwarding this artifact exists to make useless. See https://protocol.dfos.com/api-auth." + "description": "A request proof: a short-lived JWS signed by the key the credential was issued to, binding this exact method, host, path, and body, and carried as `Authorization: DFOS `. Not a bearer token — see https://protocol.dfos.com/integrations#api-authentication." }, "dfosCredential": { "type": "apiKey", "in": "header", "name": "X-Credential", - "description": "The DFOS credential the user issued to your application, embedding its full delegation chain. A durable grant — it names what you may do and until when, and is revocable by the user at any time. It is not a secret in the bearer-token sense: without the audience key it authorizes nothing." + "description": "The DFOS credential the user issued to your application, embedding its full delegation chain. It names what you may do and until when, is revocable by the user at any time, and authorizes nothing without the audience key." } }, "schemas": { @@ -47,23 +47,23 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] }, "after": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "before": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] } }, @@ -72,9 +72,9 @@ "ProtocolDid": { "type": "string", "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", - "description": "Protocol DID — `did:dfos:` + 31 chain-derived chars (40 total). The ONLY DID universe the public wire speaks; never the internal 22-char OLTP identifier.", + "description": "Protocol DID — `did:dfos:` plus 31 chain-derived characters (40 total). The only DID form this API speaks; a DID of any other shape will not resolve.", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] }, "PublicMediaOutput": { @@ -82,18 +82,15 @@ "properties": { "id": { "type": "string", - "description": "Media object id — the value an `attachment://` inline body token references. Use it to associate a body token with its entry in `bodyMedia` (required when a body carries more than one inline media item). Media ids already appear verbatim in the body markdown, so this exposes nothing new.", - "examples": [ - "media_6encc4akrze2ah9kntzd9t" - ] + "description": "Media object id — what an `attachment://` inline body token references. Use it to match a token to its `bodyMedia` entry." }, "filename": { "type": "string", - "description": "Original uploaded filename of the media object" + "description": "Original uploaded filename" }, "url": { "type": "string", - "description": "Resolved URL for the media object. A permanent, unsigned imgix CDN URL for public images; a time-limited SIGNED URL for private media (audio/video/files). When signed, `urlExpiresAt` is present — never persist a signed URL, re-fetch the post for a fresh one." + "description": "Resolved URL for the media object. Permanent for public images; a time-limited signed URL for private media, which carries `urlExpiresAt`." }, "contentType": { "type": "string", @@ -103,7 +100,7 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Size of the media object in bytes (absent until upload is finalized)" + "description": "Size in bytes, absent until the upload is finalized" }, "width": { "type": "integer", @@ -123,7 +120,7 @@ }, "alt": { "type": "string", - "description": "Uploader-authored caption / alt text, when present" + "description": "Uploader-authored caption or alt text" }, "durationMs": { "type": "integer", @@ -133,16 +130,16 @@ }, "posterUrl": { "type": "string", - "description": "CDN URL of an extracted poster frame / cover art (audio/video only)" + "description": "CDN URL of an extracted poster frame or cover art (audio/video only)" }, "playbackUrl": { "type": "string", - "description": "Streamable MP4 rendition URL (audio/video only). A permanent CDN URL for public media; a time-limited SIGNED URL for private media (in which case `urlExpiresAt` is present)." + "description": "Streamable MP4 rendition URL (audio/video only). Permanent for public media; a time-limited signed URL for private media." }, "urlExpiresAt": { "type": "string", "format": "date-time", - "description": "When the SIGNED `url` / `playbackUrl` expire (ISO 8601 UTC). PRESENT iff those URLs are time-limited signed URLs (private media); ABSENT means they are permanent (public images). Never persist a signed URL — re-fetch the post to obtain fresh ones." + "description": "When the signed `url` and `playbackUrl` expire (ISO 8601 UTC). Present only for private media. Signed URLs are ephemeral — re-fetch rather than persisting them." }, "waveformPeaks": { "type": "array", @@ -162,7 +159,19 @@ "contentType" ], "additionalProperties": false, - "description": "A resolved media object" + "description": "A resolved media object", + "examples": [ + { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + } + ] }, "PublicAuthorOutput": { "type": "object", @@ -180,14 +189,22 @@ }, "avatarUrl": { "type": "string", - "description": "Resolved public CDN URL for the author avatar, when present" + "description": "Public CDN URL for the author avatar" } }, "required": [ "did" ], "additionalProperties": false, - "description": "Public author identity" + "description": "Public author identity", + "examples": [ + { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + } + ] }, "SpaceOutput": { "type": "object", @@ -201,7 +218,7 @@ }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical." + "description": "Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -234,7 +251,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" }, "headerUrl": { "anyOf": [ @@ -245,7 +262,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space header/wallpaper image, or null" + "description": "Public CDN URL for the space header image, or null" }, "verifiedDomain": { "anyOf": [ @@ -256,7 +273,7 @@ "type": "null" } ], - "description": "An external domain this space has proven control of via origin binding, or null. NOT the same thing as `domain`, which is the space's DFOS subdomain. The claim is a `DfosOrigin` entry on the space's identity chain, and the domain independently publishes the space's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the space claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction." + "description": "A domain this space has proven control of through origin binding, or null. Distinct from `domain`, the DFOS subdomain." }, "links": { "type": "array", @@ -279,11 +296,11 @@ "type": "string", "minLength": 31, "maxLength": 31, - "description": "The space profile's protocol content-chain id" + "description": "The space profile's content-chain id" }, "headOpCid": { "type": "string", - "description": "The space profile content chain's current head operation CID" + "description": "The profile content chain's current head operation CID" } }, "required": [ @@ -291,14 +308,14 @@ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for the space profile content chain. Present iff the space has a non-empty profile chain; absent when the profile is empty or its chain has not been created." + "description": "Proof-plane handles for the space profile content chain, absent when there is none." } }, "required": [ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for this space's identity and optional profile content chain. Present iff the identity chain exists; absent for legacy identities without a chain. Combine with `GET /protocol` to fetch and verify the chains from the relay." + "description": "Proof-plane handles for this space's identity chain and optional profile chain. Absent for legacy identities without one; resolve them via `GET /protocol`." }, "joinMode": { "$ref": "#/components/schemas/PublicSpaceJoinMode" @@ -313,38 +330,38 @@ }, "subscribeEnabled": { "type": "boolean", - "description": "Whether the public email-subscribe form is enabled for this space" + "description": "Whether the public email-subscribe form is enabled" }, "memberCountSummary": { "type": "string", - "description": "Worded member-count summary (e.g. \"a few dozen members\"). Public surfaces deliberately avoid exact counts." + "description": "Worded member-count summary (e.g. \"a few dozen members\"), never exact." }, "privatePostCountSummary": { "type": "string", - "description": "Worded summary of how many posts sit BEHIND the door — live posts this API cannot serve anonymously (e.g. \"a few dozen private posts\", \"no private posts\"). The exact complement of the space's public posts, which the posts listing enumerates precisely as `totalCount`. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent." + "description": "Worded summary of the posts this API does not serve anonymously (e.g. \"a few dozen private posts\"). Approximate scale, not an exact count; render it, never parse it." }, "chatMessageCountSummary": { "type": "string", - "description": "Worded summary of how many chat messages the space's channels hold (e.g. \"hundreds of chat messages\", \"no chat messages\"). Covers every channel bound to the space, including its private ones. Direct and group messages are never space-bound and never counted. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent." + "description": "Worded summary of chat messages across the space's channels, private ones included; direct and group messages never count. Approximate scale, not an exact count; render it, never parse it." }, "eventCountSummary": { "type": "string", - "description": "Worded summary of how many published events the space has scheduled (e.g. \"a few events\", \"no events\"). A recurring series counts once. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent." + "description": "Worded summary of the space's published events; a recurring series counts once. Approximate scale, not an exact count; render it, never parse it." }, "mediaUploadCountSummary": { "type": "string", - "description": "Worded summary of how many media files have been uploaded to the space (e.g. \"about a hundred media uploads\", \"no media uploads\"). Covers ALL of the space's media, including images it serves publicly — the avatar and header on this response are themselves uploads and are counted. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent." + "description": "Worded summary of the space's media uploads, publicly served ones like the avatar included. Approximate scale, not an exact count; render it, never parse it." }, "category": { "type": "string", - "description": "The normalized discovery category the space filed itself under. ABSENT (not null) when the space has not filed one, matching the `protocol` block convention. The same token the discovery listing emits and its `category` filter accepts." + "description": "The discovery category the space filed itself under, absent when it has filed none. The same token the `category` filter accepts." }, "applicationQuestions": { "type": "array", "items": { "$ref": "#/components/schemas/PublicApplicationQuestionOutput" }, - "description": "The questions an applicant answers when joining, present ONLY for a public application-mode space (`joinMode: \"application\"`) that has configured at least one question. Absent for every other space. Viewer-independent — the same list for all callers." + "description": "The questions an applicant answers when joining. Present only for an application-mode space that configured at least one." }, "createdAt": { "type": "string", @@ -369,7 +386,44 @@ "createdAt" ], "additionalProperties": false, - "description": "A public space profile and site configuration" + "description": "A public space profile and site configuration", + "examples": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "headerUrl": "https://dfos.imgix.net/media/public/dv6kef3nahvh4anecth2zt-bbgg.png", + "verifiedDomain": "dfos.com", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "protocol": { + "headOpCid": "bafyreib4dybomqzwea3ottaeflaapuj4vlci6zpuelb54l3o3lqm4zntli", + "profile": { + "contentId": "e7nr6ta3ddhvnvdz6aav99anf2tna3r", + "headOpCid": "bafyreiejihfwzybgtg5b3pgahumbzpqkzp6h4m3etedo6x27xbenjhpvbu" + } + }, + "joinMode": "open", + "siteMode": "posts", + "subscribeEnabled": true, + "memberCountSummary": "thousands of members", + "privatePostCountSummary": "hundreds of private posts", + "chatMessageCountSummary": "thousands of chat messages", + "eventCountSummary": "about a dozen events", + "mediaUploadCountSummary": "thousands of media uploads", + "createdAt": "2025-12-12T02:20:55.777Z" + } + ] }, "PublicApplicationQuestionOutput": { "type": "object", @@ -380,7 +434,7 @@ }, "isRequired": { "type": "boolean", - "description": "Whether answering this question is required to apply" + "description": "Whether an answer is required to apply" } }, "required": [ @@ -388,7 +442,13 @@ "isRequired" ], "additionalProperties": false, - "description": "A public space application question" + "description": "A public space application question", + "examples": [ + { + "question": "What are you working on, and what brings you here?", + "isRequired": true + } + ] }, "PublicSpaceCtaOutput": { "type": "object", @@ -424,22 +484,22 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias." + "description": "Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias." }, "joinMode": { "$ref": "#/components/schemas/PublicSpaceJoinMode" }, "subscribeEnabled": { "type": "boolean", - "description": "Whether the public email-subscribe form is enabled for this space" + "description": "Whether the public email-subscribe form is enabled" }, "memberCountSummary": { "type": "string", - "description": "Worded member-count summary (e.g. \"a few dozen members\"). Public surfaces deliberately avoid exact counts." + "description": "Worded member-count summary (e.g. \"a few dozen members\"), never exact." } }, "required": [ @@ -452,7 +512,18 @@ "memberCountSummary" ], "additionalProperties": false, - "description": "Compact space context for a post response (header + join/sign-in CTA)" + "description": "Compact space context for a post response (header + join/sign-in CTA)", + "examples": [ + { + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "domain": "home", + "joinMode": "open", + "subscribeEnabled": true, + "memberCountSummary": "thousands of members" + } + ] }, "PublicSpaceJoinMode": { "enum": [ @@ -476,7 +547,7 @@ }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical." + "description": "Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -509,7 +580,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" }, "verifiedDomain": { "anyOf": [ @@ -520,7 +591,7 @@ "type": "null" } ], - "description": "An external domain this space has proven control of via origin binding, or null. NOT the same thing as `domain`, which is the space's DFOS subdomain. The claim is a `DfosOrigin` entry on the space's identity chain, and the domain independently publishes the space's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the space claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction." + "description": "A domain this space has proven control of through origin binding, or null. Distinct from `domain`, the DFOS subdomain." }, "links": { "type": "array", @@ -531,11 +602,11 @@ }, "memberCountSummary": { "type": "string", - "description": "Worded member-count summary (e.g. \"a few dozen members\"). Public surfaces deliberately avoid exact counts." + "description": "Worded member-count summary (e.g. \"a few dozen members\"), never exact." }, "category": { "type": "string", - "description": "The normalized discovery category the space filed itself under. ABSENT (not null) when the space has not filed one, matching the `protocol` block convention. A storage token, not display copy — pass it back verbatim as the `category` filter." + "description": "The discovery category the space filed itself under, absent when it has filed none. Pass it back verbatim as the `category` filter." } }, "required": [ @@ -550,7 +621,28 @@ "memberCountSummary" ], "additionalProperties": false, - "description": "A public space in the discovery listing" + "description": "A public space in the discovery listing", + "examples": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "verifiedDomain": "dfos.com", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "memberCountSummary": "thousands of members" + } + ] }, "SpaceDiscoveryPageOutput": { "type": "object", @@ -571,7 +663,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -582,7 +674,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -593,7 +685,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -601,24 +693,57 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public spaces" + "description": "A cursor-paginated page of public spaces", + "examples": [ + { + "items": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "verifiedDomain": "dfos.com", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "memberCountSummary": "thousands of members" + }, + { + "id": "space_z94a849d9kdftfvv3n9hn7", + "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + "domain": "rakowwwski", + "displayName": "POPULAR", + "description": "Popular is a space we share our creative process and research practices. We get to know our work to get to know ourselves.", + "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + "verifiedDomain": null, + "links": [], + "memberCountSummary": "about a dozen members" + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null, + "totalCount": null + } + ] }, "PublicPostListItemOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Post ID (use as `{postId}` on the single-post route)", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] + "description": "Post ID. Use as `{postId}` on the single-post route." }, "slug": { "type": "string", - "description": "Server-generated URL slug for the post", - "examples": [ - "building-a-more-generous-internet" - ] + "description": "URL slug for the post" }, "format": { "$ref": "#/components/schemas/PublicPostFormat" @@ -632,10 +757,7 @@ "type": "null" } ], - "description": "Post title", - "examples": [ - "Building a more generous internet" - ] + "description": "Post title" }, "displayTitle": { "anyOf": [ @@ -646,10 +768,7 @@ "type": "null" } ], - "description": "Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content.", - "examples": [ - "Building a more generous internet" - ] + "description": "The title, else a short markdown-stripped excerpt of the above-fold body, else null." }, "excerpt": { "anyOf": [ @@ -660,10 +779,7 @@ "type": "null" } ], - "description": "Truncated plain-text preview of the post body (markdown-stripped)", - "examples": [ - "A field guide to shared infrastructure for creative communities." - ] + "description": "Plain-text preview of the body, markdown-stripped" }, "author": { "anyOf": [ @@ -684,17 +800,17 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of upvotes on the post" + "description": "Number of upvotes" }, "commentCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of comments on the post" + "description": "Number of comments" }, "isPinned": { "type": "boolean", - "description": "Whether the space has pinned this post. The feed is ordered purely by recency (newest first); a client may use this flag to surface pinned posts itself." + "description": "Whether the space has pinned this post. The feed is ordered by recency alone." }, "publishedAt": { "type": "string", @@ -708,7 +824,7 @@ }, "canonicalUri": { "type": "string", - "description": "Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path. Same value as on the single-post response." + "description": "Canonical public web permalink for the post" }, "protocol": { "type": "object", @@ -729,7 +845,7 @@ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay." + "description": "Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only." }, "viewer": { "$ref": "#/components/schemas/PublicPostViewerOutput" @@ -750,7 +866,43 @@ "updatedAt" ], "additionalProperties": false, - "description": "A post in a space public feed" + "description": "A post in a space public feed", + "examples": [ + { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "upvoteCount": 14, + "commentCount": 6, + "isPinned": false, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z", + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + } + } + ] }, "PublicPostFormat": { "enum": [ @@ -758,7 +910,7 @@ "long-post" ], "type": "string", - "description": "Post format: `short-post` (a short, typically untitled note) or `long-post` (a titled article). Open enum — treat an unrecognized value as a generic post.", + "description": "`short-post` (untitled note) or `long-post` (titled article). Open enum — render an unknown value as a generic post.", "examples": [ "long-post" ] @@ -772,10 +924,7 @@ }, "slug": { "type": "string", - "description": "Server-generated URL slug for the post", - "examples": [ - "building-a-more-generous-internet" - ] + "description": "URL slug for the post" }, "format": { "$ref": "#/components/schemas/PublicPostFormat" @@ -789,10 +938,7 @@ "type": "null" } ], - "description": "Post title", - "examples": [ - "Building a more generous internet" - ] + "description": "Post title" }, "displayTitle": { "anyOf": [ @@ -803,10 +949,7 @@ "type": "null" } ], - "description": "Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content.", - "examples": [ - "Building a more generous internet" - ] + "description": "The title, else a short markdown-stripped excerpt of the above-fold body, else null." }, "excerpt": { "anyOf": [ @@ -817,10 +960,7 @@ "type": "null" } ], - "description": "Truncated plain-text preview of the post body (markdown-stripped)", - "examples": [ - "A field guide to shared infrastructure for creative communities." - ] + "description": "Plain-text preview of the body, markdown-stripped" }, "body": { "anyOf": [ @@ -842,14 +982,14 @@ "items": { "$ref": "#/components/schemas/PublicMediaOutput" }, - "description": "Post attachments. Includes private media (audio/video/files), which carry time-limited signed `url` / `playbackUrl` values (see `urlExpiresAt`)." + "description": "Post attachments. Private media carries time-limited signed `url` / `playbackUrl` values; see `urlExpiresAt`." }, "bodyMedia": { "type": "array", "items": { "$ref": "#/components/schemas/PublicMediaOutput" }, - "description": "Media objects referenced inline in the post body markdown. The body carries `attachment://` tokens; resolve each against this array by its `id`. Always present (empty array when none)." + "description": "Media referenced inline in the body via `attachment://` tokens; resolve each by `id`. Empty when none." }, "author": { "anyOf": [ @@ -891,9 +1031,9 @@ "properties": { "url": { "type": "string", - "description": "The original shared URL represented by this link card", + "description": "The shared URL", "examples": [ - "https://example.com/field-notes" + "https://dfos.com/" ] }, "title": { @@ -905,9 +1045,9 @@ "type": "null" } ], - "description": "Resolved page title, or null when unavailable", + "description": "Resolved page title, or null", "examples": [ - "Field Notes" + "Dark Forest OS" ] }, "description": { @@ -919,7 +1059,7 @@ "type": "null" } ], - "description": "Resolved page description, or null when unavailable" + "description": "Resolved page description, or null" }, "siteName": { "anyOf": [ @@ -930,18 +1070,15 @@ "type": "null" } ], - "description": "Resolved site name, or null when unavailable", - "examples": [ - "Example" - ] + "description": "Resolved site name, or null" }, "imageUrl": { "type": "string", - "description": "Permanent public CDN URL for the cached link preview image, when available" + "description": "Permanent CDN URL for the cached preview image, when available" }, "faviconUrl": { "type": "string", - "description": "Permanent public CDN URL for the cached site favicon, when available" + "description": "Permanent CDN URL for the cached site favicon, when available" }, "sortOrder": { "type": "integer", @@ -960,7 +1097,7 @@ "additionalProperties": false, "description": "An extracted link card from a public post body" }, - "description": "Link cards extracted from URLs in the post body, in appearance order. Absent when the post has no links." + "description": "Link cards extracted from the body, in appearance order. Absent when there are none." }, "entityLinks": { "type": "array", @@ -969,15 +1106,15 @@ "properties": { "url": { "type": "string", - "description": "Normalized absolute internal URL as it appears in the served post body" + "description": "Absolute internal URL as it appears in the served body" }, "label": { "type": "string", - "description": "Viewer-independent display label for the entity" + "description": "Display label for the entity" }, "kind": { "type": "string", - "description": "Entity kind. Current values: `post`, `space`, or `event`. Open enum — clients must render unknown values as ordinary links." + "description": "Entity kind: `post`, `space`, or `event`. Open enum — render an unknown value as an ordinary link." } }, "required": [ @@ -988,15 +1125,15 @@ "additionalProperties": false, "description": "Resolved label for an internal entity link in a public post body" }, - "description": "Internal entity links derived from the served (folded) body. Viewer-independent; post and space labels resolve at the anonymous/public floor, while events must be published and space-visible in a space whose public profile is enabled. Omitted when no internal links resolve." + "description": "Internal entity links resolved from the served body. Absent when none resolve." }, "folded": { "type": "boolean", - "description": "True when the body was truncated at a fold marker — only the above-fold teaser is present. An anonymous read is always the non-reader projection, so a folded post is always truncated for it; a member projection carries the full body when the caller genuinely reads the post. Absent when the post has no fold." + "description": "True when only the above-fold teaser is present. Absent when the post has no fold." }, "canonicalUri": { "type": "string", - "description": "Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path." + "description": "Canonical public web permalink: the space public host plus `/post/{slug}-{id}`." }, "protocol": { "type": "object", @@ -1017,19 +1154,19 @@ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay." + "description": "Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only." }, "upvoteCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of upvotes on the post" + "description": "Number of upvotes" }, "commentCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of comments on the post" + "description": "Number of comments" }, "publishedAt": { "type": "string", @@ -1063,7 +1200,62 @@ "updatedAt" ], "additionalProperties": false, - "description": "Full public post content" + "description": "Full public post content", + "examples": [ + { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "body": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\n\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.", + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "attachments": [], + "bodyMedia": [ + { + "id": "media_rn2926ahdrnra6t4erc862", + "filename": "2026-09-04-siwd-consent-composite-equal-height.png", + "url": "https://dfos.imgix.net/media/public/rn2926ahdrnra6t4erc862-2026-09-04-siwd-consent-composite-equal-height.png", + "contentType": "image/png", + "contentLength": 273173, + "width": 2116, + "height": 1160, + "blurHash": "eRS6Pl%Mt7%May~qWBWBt7WBxuayWBofj[%Mxut7Rjj[-;WBWBt7ay" + } + ], + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "topics": [ + { + "id": "topic_6c2efd472dvt8rf9k4ftcc", + "name": "DFOS Blog" + } + ], + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + }, + "upvoteCount": 14, + "commentCount": 6, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z" + } + ] }, "PublicPostPageOutput": { "type": "object", @@ -1084,7 +1276,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -1095,7 +1287,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -1106,7 +1298,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -1114,7 +1306,83 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public posts" + "description": "A cursor-paginated page of public posts", + "examples": [ + { + "items": [ + { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "upvoteCount": 14, + "commentCount": 6, + "isPinned": false, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z", + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + } + }, + { + "id": "post_n7hhz3hv6n4h8rde8c2ed9", + "slug": "the-dangers-of-self-expression", + "format": "long-post", + "title": "The dangers of self-expression", + "displayTitle": "The dangers of self-expression", + "excerpt": "The thread running through this week’s issue: what happens when more and more of life becomes something to optimize, perform, and monetize? This is why we’re making DFOS: a different world becomes...", + "author": { + "did": "did:dfos:27z77a722tacvrne3k7f6k6rzd9evkc", + "displayName": "Yancey", + "username": "ystrickler", + "avatarUrl": "https://dfos.imgix.net/media/public/v3rr6d9z7f2dad4hhde8cd-yancey-headshot.jpeg" + }, + "cover": { + "id": "media_eet4k4rhdt3nt4n4kervvr", + "filename": "cover.png", + "url": "https://dfos.imgix.net/media/public/eet4k4rhdt3nt4n4kervvr-cover.png", + "contentType": "image/png", + "contentLength": 261229, + "width": 1051, + "height": 720, + "blurHash": "evQ,z6tQ}x%3A8xuj[oMj[ax=ij[EvWnw6%3j[bFj[afozj[Rjayt7" + }, + "upvoteCount": 20, + "commentCount": 16, + "isPinned": false, + "publishedAt": "2026-09-02T14:54:48.551Z", + "updatedAt": "2026-09-02T14:54:48.725Z", + "canonicalUri": "https://home.dfos.com/post/the-dangers-of-self-expression-n7hhz3hv6n4h8rde8c2ed9", + "protocol": { + "contentId": "en8ne3nzft9rrte866v2423rn26r3er", + "headOpCid": "bafyreicmvn5tzg6vo5oey2itz3pacvmeemfggwecc62rdbnercnb6lggrm" + } + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "totalCount": 10 + } + ] }, "PublicPostViewerOutput": { "type": "object", @@ -1128,7 +1396,12 @@ "upvoted" ], "additionalProperties": false, - "description": "The authenticated caller's own relationship to this post. Present ONLY on a member projection — a request that presented a credential (or identity proof) reaching this space. Absent on every anonymous response." + "description": "The caller's own relationship to this post. Present only on a member projection.", + "examples": [ + { + "upvoted": true + } + ] }, "FeedSpaceRefOutput": { "type": "object", @@ -1136,9 +1409,9 @@ "id": { "type": "string", "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", - "description": "The space's protocol DID — the canonical, stable identifier. Pass it back as `{space}` on any space-addressed route.", + "description": "The space's protocol DID. Pass it back as `{space}` on any space-addressed route.", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] }, "name": { @@ -1150,11 +1423,11 @@ "type": "null" } ], - "description": "Space display name, or null when it has none" + "description": "Space display name, or null" }, "url": { "type": "string", - "description": "The space's public web address (custom domain, else the `space-{id}` subdomain). Present for every space; a space with no public profile still has a canonical address, which is simply not anonymously reachable." + "description": "The space's public web address: its custom domain, else the `space-{id}` subdomain. Present for private spaces too, where the address is real but does not resolve anonymously." } }, "required": [ @@ -1163,24 +1436,25 @@ "url" ], "additionalProperties": false, - "description": "The space a feed item belongs to" + "description": "The space a feed item belongs to", + "examples": [ + { + "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "name": "DFOS", + "url": "https://home.dfos.com" + } + ] }, "FeedItemOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Post ID (use as `{postId}` on the single-post route)", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] + "description": "Post ID. Use as `{postId}` on the single-post route." }, "slug": { "type": "string", - "description": "Server-generated URL slug for the post", - "examples": [ - "building-a-more-generous-internet" - ] + "description": "URL slug for the post" }, "format": { "$ref": "#/components/schemas/PublicPostFormat" @@ -1194,10 +1468,7 @@ "type": "null" } ], - "description": "Post title", - "examples": [ - "Building a more generous internet" - ] + "description": "Post title" }, "displayTitle": { "anyOf": [ @@ -1208,10 +1479,7 @@ "type": "null" } ], - "description": "Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content.", - "examples": [ - "Building a more generous internet" - ] + "description": "The title, else a short markdown-stripped excerpt of the above-fold body, else null." }, "excerpt": { "anyOf": [ @@ -1222,10 +1490,7 @@ "type": "null" } ], - "description": "Truncated plain-text preview of the post body (markdown-stripped)", - "examples": [ - "A field guide to shared infrastructure for creative communities." - ] + "description": "Plain-text preview of the body, markdown-stripped" }, "author": { "anyOf": [ @@ -1246,17 +1511,17 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of upvotes on the post" + "description": "Number of upvotes" }, "commentCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Number of comments on the post" + "description": "Number of comments" }, "isPinned": { "type": "boolean", - "description": "Whether the space has pinned this post. The feed is ordered purely by recency (newest first); a client may use this flag to surface pinned posts itself." + "description": "Whether the space has pinned this post. The feed is ordered by recency alone." }, "publishedAt": { "type": "string", @@ -1270,7 +1535,7 @@ }, "canonicalUri": { "type": "string", - "description": "Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path. Same value as on the single-post response." + "description": "Canonical public web permalink for the post" }, "protocol": { "type": "object", @@ -1291,7 +1556,7 @@ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay." + "description": "Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only." }, "viewer": { "$ref": "#/components/schemas/PublicPostViewerOutput" @@ -1316,7 +1581,51 @@ "space" ], "additionalProperties": false, - "description": "A post in the cross-space feed, with its space" + "description": "A post in the cross-space feed, with its space", + "examples": [ + { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "upvoteCount": 14, + "commentCount": 6, + "isPinned": false, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z", + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + }, + "viewer": { + "upvoted": true + }, + "space": { + "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "name": "DFOS", + "url": "https://home.dfos.com" + } + } + ] }, "FeedPageOutput": { "type": "object", @@ -1337,7 +1646,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -1348,7 +1657,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -1359,7 +1668,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -1367,7 +1676,83 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of the cross-space feed" + "description": "A cursor-paginated page of the cross-space feed", + "examples": [ + { + "items": [ + { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "upvoteCount": 14, + "commentCount": 6, + "isPinned": false, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z", + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + }, + "viewer": { + "upvoted": true + }, + "space": { + "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "name": "DFOS", + "url": "https://home.dfos.com" + } + }, + { + "id": "post_4vfa2h8ekt3rc9n7zd6a2f", + "slug": "notes-from-the-studio-floor", + "format": "short-post", + "title": null, + "displayTitle": "Working in public means the process is the artifact.", + "excerpt": "Working in public means the process is the artifact. This week: three false starts, one that held, and the notes we kept along the way.", + "upvoteCount": 5, + "commentCount": 2, + "isPinned": false, + "publishedAt": "2026-09-03T09:41:12.000Z", + "updatedAt": "2026-09-03T09:41:12.000Z", + "author": { + "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + "displayName": "Aron", + "username": "aron", + "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + }, + "viewer": { + "upvoted": false + }, + "space": { + "id": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + "name": "POPULAR", + "url": "https://rakowwwski.dfos.com" + } + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + } + ] }, "PublicCommentSort": { "enum": [ @@ -1376,7 +1761,7 @@ "top" ], "type": "string", - "description": "Thread ordering. `newest` (default) — most recently active threads first, where a thread's activity is the latest of its root and its replies. `oldest` — the reverse. `top` — most upvoted first. Open enum; only send values supported by the current contract.", + "description": "Thread ordering. `newest` (default) sorts by a thread's latest activity, `oldest` reverses it, and `top` sorts by upvotes. Open enum.", "examples": [ "newest" ] @@ -1386,14 +1771,11 @@ "properties": { "id": { "type": "string", - "description": "Comment ID. Pass it back as `parentCommentId` to walk its replies.", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] + "description": "Comment ID. Pass it back as `parentCommentId` to walk its replies." }, "postId": { "type": "string", - "description": "The ROOT post this comment belongs to — the same id the route was called with." + "description": "The root post this comment belongs to." }, "parentCommentId": { "type": "string", @@ -1429,7 +1811,7 @@ "activityAt": { "type": "string", "format": "date-time", - "description": "The instant this row is ORDERED by, and the value the cursor carries. On a root comment it is the thread's last activity — the later of the comment's own publication and its most recent reply — so an active thread sorts ahead of an older one under `newest`. On a reply it is the reply's own `publishedAt`." + "description": "The ordering key the cursor carries. On a root comment, the later of its own publication and its most recent reply; on a reply, its own `publishedAt`." }, "upvoteCount": { "type": "integer", @@ -1455,7 +1837,7 @@ "upvoted" ], "additionalProperties": false, - "description": "The authenticated caller's own relationship to this comment. This route is always authenticated, so the block is always present." + "description": "The caller's own relationship to this comment. Always present on this route." } }, "required": [ @@ -1469,7 +1851,27 @@ "replyCount" ], "additionalProperties": false, - "description": "A comment on a post" + "description": "A comment on a post", + "examples": [ + { + "id": "comment_9rze4tk2vdc7fa38nhe6c2", + "postId": "post_ze2kh2d47tzerkhet8348c", + "author": { + "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + "displayName": "Aron", + "username": "aron", + "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + }, + "body": "The exit-key framing is the part that clicks for me: the door exists before anyone needs it.", + "publishedAt": "2026-09-04T19:12:40.000Z", + "activityAt": "2026-09-04T20:01:05.000Z", + "upvoteCount": 3, + "replyCount": 1, + "viewer": { + "upvoted": true + } + } + ] }, "PublicCommentPageOutput": { "type": "object", @@ -1490,7 +1892,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -1501,7 +1903,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -1512,7 +1914,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -1520,21 +1922,45 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of post comments" + "description": "A cursor-paginated page of post comments", + "examples": [ + { + "items": [ + { + "id": "comment_9rze4tk2vdc7fa38nhe6c2", + "postId": "post_ze2kh2d47tzerkhet8348c", + "author": { + "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + "displayName": "Aron", + "username": "aron", + "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + }, + "body": "The exit-key framing is the part that clicks for me: the door exists before anyone needs it.", + "publishedAt": "2026-09-04T19:12:40.000Z", + "activityAt": "2026-09-04T20:01:05.000Z", + "upvoteCount": 3, + "replyCount": 1, + "viewer": { + "upvoted": true + } + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 5 + } + ] }, "PublicCommentWriteOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Comment ID. Pass it back as `parentCommentId` to walk its replies.", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] + "description": "Comment ID. Pass it back as `parentCommentId` to walk its replies." }, "postId": { "type": "string", - "description": "The ROOT post this comment belongs to — the same id the route was called with." + "description": "The root post this comment belongs to." }, "parentCommentId": { "type": "string", @@ -1591,7 +2017,7 @@ "upvoted" ], "additionalProperties": false, - "description": "The authenticated caller's own relationship to this comment. This route is always authenticated, so the block is always present." + "description": "The caller's own relationship to this comment. Always present on this route." } }, "required": [ @@ -1604,20 +2030,40 @@ "replyCount" ], "additionalProperties": false, - "description": "A comment, as returned by a write" + "description": "A comment, as returned by a write", + "examples": [ + { + "id": "comment_d6ah3f9rkt2ez48vc7n4rc", + "postId": "post_ze2kh2d47tzerkhet8348c", + "parentCommentId": "comment_9rze4tk2vdc7fa38nhe6c2", + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "body": "Same. That and origin binding running in both directions.", + "publishedAt": "2026-09-04T20:01:05.000Z", + "upvoteCount": 0, + "replyCount": 0, + "viewer": { + "upvoted": false + } + } + ] }, "PublicUpvoteStateOutput": { "type": "object", "properties": { "upvoted": { "type": "boolean", - "description": "Whether the granting user's upvote is on AFTER this call. `PUT` answers `true` and `DELETE` answers `false`, including when the call changed nothing." + "description": "Whether the caller's upvote is on after this call. `PUT` answers `true` and `DELETE` `false`, including when nothing changed." }, "upvoteCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Upvote count on the post or comment after this call" + "description": "Upvote count after this call" } }, "required": [ @@ -1625,56 +2071,58 @@ "upvoteCount" ], "additionalProperties": false, - "description": "Upvote state after a toggle" + "description": "Upvote state after a toggle", + "examples": [ + { + "upvoted": true, + "upvoteCount": 15 + } + ] }, "PublicDeletedOutput": { "type": "object", "properties": { "deleted": { "const": true, - "description": "Always `true`. A failed delete is an error status, never this body." + "description": "Always `true`; a failed delete returns an error status." } }, "required": [ "deleted" ], "additionalProperties": false, - "description": "Confirmation that the content was deleted" + "description": "Confirmation that the content was deleted", + "examples": [ + { + "deleted": true + } + ] }, "PublicPageSummaryOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable page entity id. Store this identifier; the slug may change.", - "examples": [ - "page_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable page id. Prefer it to the slug." }, "slug": { "type": "string", - "description": "Mutable page slug alias (accepted as `{page}` on the single-page route)", - "examples": [ - "about" - ] + "description": "Mutable slug alias." }, "title": { "type": "string", - "description": "Page title", - "examples": [ - "About this space" - ] + "description": "Page title." }, "sortOrder": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, - "description": "Flat ordering within the space (ascending); the list is pre-sorted" + "description": "Ascending sort position." }, "updatedAt": { "type": "string", "format": "date-time", - "description": "When the page was last updated (ISO 8601 UTC)" + "description": "Last update (ISO 8601 UTC)." } }, "required": [ @@ -1685,31 +2133,31 @@ "updatedAt" ], "additionalProperties": false, - "description": "A page in a space public page list" + "description": "A page in a space's page list", + "examples": [ + { + "id": "page_c7dnfhc3zn2eh7frrc3nhc", + "slug": "about", + "title": "About", + "sortOrder": 0, + "updatedAt": "2026-07-22T13:12:02.748Z" + } + ] }, "PublicPageOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable page entity id. Store this identifier; the slug may change.", - "examples": [ - "page_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable page id." }, "slug": { "type": "string", - "description": "Mutable page slug alias", - "examples": [ - "about" - ] + "description": "Mutable slug alias." }, "title": { "type": "string", - "description": "Page title", - "examples": [ - "About this space" - ] + "description": "Page title." }, "body": { "anyOf": [ @@ -1720,29 +2168,29 @@ "type": "null" } ], - "description": "Page content body (markdown). Inline media appears as `attachment://` tokens — resolve each against `bodyMedia` by id." + "description": "Markdown body, or null. Inline media appears as `attachment://`; resolve against `bodyMedia`." }, "cover": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Page cover image, when present" + "description": "Cover image." }, "bodyMedia": { "type": "array", "items": { "$ref": "#/components/schemas/PublicMediaOutput" }, - "description": "Media objects referenced inline in the page body markdown" + "description": "Media referenced inline in `body`." }, "sortOrder": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, - "description": "Flat ordering within the space (ascending)" + "description": "Ascending sort position." }, "updatedAt": { "type": "string", "format": "date-time", - "description": "When the page was last updated (ISO 8601 UTC)" + "description": "Last update (ISO 8601 UTC)." } }, "required": [ @@ -1754,7 +2202,29 @@ "updatedAt" ], "additionalProperties": false, - "description": "Full public page content" + "description": "Full page content", + "examples": [ + { + "id": "page_c7dnfhc3zn2eh7frrc3nhc", + "slug": "about", + "title": "About", + "body": "![dfos](attachment://media_9tnfvccf6t372vdnv43ftv)DFOS is a tool for making worlds of your own.\n\nA DFOS can be a community, publication, studio, institution, business, archive, scene, private internet, or something that doesn’t have a name yet.\n\nIt can be public or private. Free or paid.", + "bodyMedia": [ + { + "id": "media_9tnfvccf6t372vdnv43ftv", + "filename": "dfos.png", + "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + "contentType": "image/png", + "contentLength": 7045192, + "width": 2729, + "height": 2129, + "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + } + ], + "sortOrder": 0, + "updatedAt": "2026-07-22T13:12:02.748Z" + } + ] }, "PublicPagePageOutput": { "type": "object", @@ -1775,7 +2245,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -1786,7 +2256,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -1797,7 +2267,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -1805,24 +2275,34 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public pages" + "description": "A cursor-paginated list of pages", + "examples": [ + { + "items": [ + { + "id": "page_c7dnfhc3zn2eh7frrc3nhc", + "slug": "about", + "title": "About", + "sortOrder": 0, + "updatedAt": "2026-07-22T13:12:02.748Z" + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 1 + } + ] }, "PublicTopicOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Topic ID (use as the `topic` filter on the posts listing)", - "examples": [ - "topic_6encc4akrze2ah9kntzd9t" - ] + "description": "Topic ID — pass it as the `topic` filter on the posts listing" }, "name": { "type": "string", - "description": "Topic name", - "examples": [ - "Announcements" - ] + "description": "Topic name" }, "description": { "anyOf": [ @@ -1833,10 +2313,7 @@ "type": "null" } ], - "description": "Topic description", - "examples": [ - "News and updates from the community." - ] + "description": "Topic description" } }, "required": [ @@ -1845,7 +2322,14 @@ "description" ], "additionalProperties": false, - "description": "A public topic" + "description": "A public topic", + "examples": [ + { + "id": "topic_6c2efd472dvt8rf9k4ftcc", + "name": "DFOS Blog", + "description": "Announcements, product notes, and essays from the DFOS team." + } + ] }, "PublicEventType": { "enum": [ @@ -1853,7 +2337,7 @@ "attend" ], "type": "string", - "description": "What the event is: `attend` (people show up — a session, a call, a party) or `update` (the space will publish something on this date). Open enum — treat an unrecognized value as a generic event.", + "description": "`attend` (people show up) or `update` (the space publishes something that day). Open enum.", "examples": [ "attend" ] @@ -1865,7 +2349,7 @@ "chat" ], "type": "string", - "description": "How an `attend` event happens: `call` (video/audio call), `place` (a physical location), or `chat` (in the space itself). Presentational only, and null on `update` events. Open enum — treat an unrecognized value as unspecified.", + "description": "How an `attend` event happens: `call` (video or audio), `place` (a physical location), or `chat` (in the space itself). Presentational only, and null on `update` events. Open enum.", "examples": [ "place" ] @@ -1875,17 +2359,14 @@ "properties": { "id": { "type": "string", - "description": "Space short ID — canonical and stable.", - "examples": [ - "space_6encc4akrze2ah9kntzd9t" - ] + "description": "Space short ID — canonical and stable." }, "did": { "$ref": "#/components/schemas/ProtocolDid" }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical." + "description": "Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -1907,7 +2388,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" } }, "required": [ @@ -1918,17 +2399,23 @@ "avatarUrl" ], "additionalProperties": false, - "description": "The space an item belongs to" + "description": "The space an item belongs to", + "examples": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + ] }, "PublicProductPriceOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable price-tier id. Identifies which tier a purchase is for when a product offers more than one.", - "examples": [ - "pprice_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable price-tier id. Names which tier a purchase is for." }, "type": { "enum": [ @@ -1937,32 +2424,26 @@ "recurring" ], "type": "string", - "description": "Pricing model. `one_time` is a fixed price; `pwyw` lets the buyer choose an amount at or above the floor; `recurring` bills on the stated `interval`. Open enum — tolerate unrecognized values." + "description": "Pricing model. `one_time` is a fixed price, `pwyw` a buyer-chosen amount at or above the floor, `recurring` bills on `interval`. Open enum." }, "amountCents": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "For `one_time`, the price. For `pwyw`, the MINIMUM the buyer may pay (0 means a free floor).", - "examples": [ - 1000 - ] + "description": "For `one_time`, the price. For `pwyw`, the minimum the buyer may pay (0 is a free floor)." }, "suggestedCents": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Suggested amount for a `pwyw` price, to prefill a buyer amount input. Absent when there is no suggestion, and always absent for `one_time`.", + "description": "Amount to prefill for a `pwyw` price. Absent for other price types and when there is no suggestion.", "examples": [ 1500 ] }, "currency": { "type": "string", - "description": "ISO 4217 currency code, lowercase", - "examples": [ - "usd" - ] + "description": "ISO 4217 currency code, lowercase" }, "interval": { "enum": [ @@ -1976,7 +2457,7 @@ "type": "integer", "minimum": 1, "maximum": 9007199254740991, - "description": "Number of `interval` units between billings (1 = every month/year). Always present alongside `interval`; absent for `one_time` and `pwyw`.", + "description": "Number of `interval` units between billings. Present with `interval`, absent otherwise.", "examples": [ 1 ] @@ -1989,46 +2470,48 @@ "currency" ], "additionalProperties": false, - "description": "A purchasable price tier for a public product" + "description": "A purchasable price tier for a public product", + "examples": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ] }, "PublicEventTicketOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Store product id — the same `id` the products routes return. Stable; store this rather than the slug.", - "examples": [ - "sprod_6encc4akrze2ah9kntzd9t" - ] + "description": "Store product id. Prefer it to the slug." }, "slug": { "type": "string", - "description": "The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "softr-ticket" + "the-dark-forest-anthology-of-the-internet-digital" ] }, "name": { "type": "string", - "description": "Ticket name", - "examples": [ - "General admission" - ] + "description": "Ticket name." }, "prices": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProductPriceOutput" }, - "description": "ACTIVE price tiers for this ticket — the identical shape and the identical selection the products routes emit for the same product (archived tiers never appear). One-time and pay-what-you-want only; a ticket never carries a recurring price." + "description": "Active price tiers, as the products routes emit them. One-time and pay-what-you-want only." }, "isSoldOut": { "type": "boolean", - "description": "True when this ticket had limited capacity and it is exhausted. Advisory — availability is re-checked at purchase." + "description": "True when limited capacity is exhausted. Re-checked at purchase." }, "purchasable": { "type": "boolean", - "description": "Whether a purchase can be STARTED right now — false when the platform payment rail is paused or the ticket has no completable price tier. Render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message. Same meaning, and the same derivation, as `purchasable` on the products routes." + "description": "Whether a purchase can be started now, independent of `isSoldOut`. False when the payment rail is paused or no price tier is completable." } }, "required": [ @@ -2040,41 +2523,46 @@ "purchasable" ], "additionalProperties": false, - "description": "A ticket on sale for a public event" + "description": "A ticket on sale", + "examples": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "isSoldOut": false, + "purchasable": true + } + ] }, "PublicEventOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Event ID. Identifies the EVENT; a recurring event yields one item per occurrence, so use `(seriesId, recurrenceId)` to address a specific one.", - "examples": [ - "evt_6encc4akrze2ah9kntzd9t" - ] + "description": "Event id. A recurring event yields one item per occurrence; address one with `(seriesId, recurrenceId)`." }, "seriesId": { "type": "string", - "description": "The series this occurrence belongs to — the same value as `id` today. Half of the occurrence identity pair.", - "examples": [ - "evt_6encc4akrze2ah9kntzd9t" - ] + "description": "The series this occurrence belongs to; same value as `id` today." }, "recurrenceId": { "type": "string", - "description": "RFC 5545 RECURRENCE-ID: this occurrence's stable identity within its series — its local wall-time (`YYYY-MM-DDTHH:MM:SS`) or, for an all-day event, its date (`YYYY-MM-DD`). Stable across DST; never identify an occurrence by its resolved UTC instant.", - "examples": [ - "2026-08-15T19:00:00" - ] + "description": "This occurrence's identity within its series (RFC 5545 RECURRENCE-ID): local wall-time `YYYY-MM-DDTHH:MM:SS`, or `YYYY-MM-DD` when all-day. Stable across DST." }, "space": { "$ref": "#/components/schemas/PublicSpaceRefOutput" }, "title": { "type": "string", - "description": "Event title", - "examples": [ - "Listening session" - ] + "description": "Event title." }, "description": { "anyOf": [ @@ -2085,7 +2573,7 @@ "type": "null" } ], - "description": "Event description (plain text), or null" + "description": "Plain-text description, or null." }, "location": { "anyOf": [ @@ -2096,7 +2584,7 @@ "type": "null" } ], - "description": "Free-text location as the organizer wrote it, or null" + "description": "Location as the organizer wrote it, or null." }, "type": { "$ref": "#/components/schemas/PublicEventType" @@ -2110,7 +2598,7 @@ "type": "null" } ], - "description": "Attend-event flavor; null when unspecified or on an `update` event." + "description": "Attend-event flavor. Null when unspecified or on an `update` event." }, "url": { "anyOf": [ @@ -2121,11 +2609,11 @@ "type": "null" } ], - "description": "Waypoint URL the organizer attached (a call link, a ticket page), or null. Arbitrary organizer-supplied content — treat it as untrusted." + "description": "URL the organizer attached (a call link, a ticket page), or null. Treat it as untrusted." }, "isAllDay": { "type": "boolean", - "description": "True for a date-based event: `startDate`/`endDate` carry the dates and every instant field is null. False for a timed event, where the reverse holds." + "description": "True for a date-based event: `startDate`/`endDate` are set and the instant fields are null." }, "startsAt": { "anyOf": [ @@ -2137,7 +2625,7 @@ "type": "null" } ], - "description": "This occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event.", + "description": "This occurrence's start instant (ISO 8601 UTC). Null when all-day.", "examples": [ "2026-08-16T00:00:00.000Z" ] @@ -2152,7 +2640,7 @@ "type": "null" } ], - "description": "This occurrence's resolved end instant (ISO 8601 UTC). Null for an all-day event or an open-ended one; equal to `startsAt` for a point-in-time event." + "description": "This occurrence's end instant (ISO 8601 UTC). Null when all-day or open-ended; equal to `startsAt` for a point-in-time event." }, "startDate": { "anyOf": [ @@ -2163,10 +2651,7 @@ "type": "null" } ], - "description": "All-day only: this occurrence's start date (`YYYY-MM-DD`). Null for a timed event.", - "examples": [ - "2026-08-15" - ] + "description": "All-day only: this occurrence's start date (`YYYY-MM-DD`). Null on a timed event." }, "endDate": { "anyOf": [ @@ -2177,7 +2662,7 @@ "type": "null" } ], - "description": "All-day only: EXCLUSIVE end date (`YYYY-MM-DD`) — null means a single day. Null for a timed event." + "description": "All-day only: exclusive end date (`YYYY-MM-DD`). Null for a single day or a timed event." }, "localStart": { "anyOf": [ @@ -2188,7 +2673,7 @@ "type": "null" } ], - "description": "Timed only: this occurrence's start as local wall-clock time in `timeZone` (`YYYY-MM-DDTHH:MM:SS`) — the time the organizer actually authored. Equal to `recurrenceId` for a timed occurrence. Null for an all-day event." + "description": "Timed only: this occurrence's start as wall-clock time in `timeZone`. Equal to `recurrenceId`. Null when all-day." }, "timeZone": { "anyOf": [ @@ -2199,7 +2684,7 @@ "type": "null" } ], - "description": "Timed only: the IANA zone the event was authored in — the zone `localStart` is expressed in. Null for an all-day event.", + "description": "Timed only: the IANA zone `localStart` is expressed in. Null when all-day.", "examples": [ "America/Chicago" ] @@ -2208,22 +2693,22 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "How many members have marked interest in the event (its RSVP count). Members only — this API cannot RSVP." + "description": "How many members marked interest." }, "requiresTicket": { "type": "boolean", - "description": "Whether attending requires buying a ticket. Always false on an `update` event. Independent of `tickets`: an event can require a ticket and still list none here, when the seller has not published a public purchase page for it." + "description": "Whether attending requires a ticket. Always false on an `update` event. An event can require one and still list none in `tickets`." }, "tickets": { "type": "array", "items": { "$ref": "#/components/schemas/PublicEventTicketOutput" }, - "description": "Tickets on sale for this event that an anonymous caller can buy, in the order the organizer attached them. Empty when the event sells no tickets, or sells them only through pages that are not public. Viewer-independent, like everything else here — it says what is on sale, never what you already hold." + "description": "Tickets on sale, in the order the organizer attached them." }, "cover": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "The cover image to render for THIS occurrence, when present: the occurrence's own cover art if the organizer set one, otherwise the event's. Always a public image with a PERMANENT unsigned URL — no `urlExpiresAt`, safe to hotlink while the event references it." + "description": "This occurrence's cover image, falling back to the event's. Permanent unsigned URL, safe to hotlink." } }, "required": [ @@ -2249,7 +2734,47 @@ "tickets" ], "additionalProperties": false, - "description": "One occurrence of a public event" + "description": "One occurrence of a public event", + "examples": [ + { + "id": "evt_ehrrkh8thkkdh6c383zkf9", + "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + "recurrenceId": "2026-09-09", + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + }, + "title": "DFOS product weekly", + "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + "location": null, + "type": "update", + "mode": null, + "url": null, + "isAllDay": true, + "startsAt": null, + "endsAt": null, + "startDate": "2026-09-09", + "endDate": null, + "localStart": null, + "timeZone": null, + "interestCount": 0, + "requiresTicket": false, + "tickets": [], + "cover": { + "id": "media_r22avnnrfch9ee62nhzkcn", + "filename": "cover.png", + "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + "contentType": "image/png", + "contentLength": 114085, + "width": 3000, + "height": 2100, + "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + } + } + ] }, "PublicEventPageOutput": { "type": "object", @@ -2270,7 +2795,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -2281,7 +2806,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -2292,7 +2817,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -2300,10 +2825,94 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public event occurrences" - }, - "PublicTopicPageOutput": { - "type": "object", + "description": "A cursor-paginated list of occurrences", + "examples": [ + { + "items": [ + { + "id": "evt_ehrrkh8thkkdh6c383zkf9", + "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + "recurrenceId": "2026-09-09", + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + }, + "title": "DFOS product weekly", + "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + "location": null, + "type": "update", + "mode": null, + "url": null, + "isAllDay": true, + "startsAt": null, + "endsAt": null, + "startDate": "2026-09-09", + "endDate": null, + "localStart": null, + "timeZone": null, + "interestCount": 0, + "requiresTicket": false, + "tickets": [], + "cover": { + "id": "media_r22avnnrfch9ee62nhzkcn", + "filename": "cover.png", + "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + "contentType": "image/png", + "contentLength": 114085, + "width": 3000, + "height": 2100, + "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + } + }, + { + "id": "evt_ehrrkh8thkkdh6c383zkf9", + "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + "recurrenceId": "2026-09-16", + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + }, + "title": "DFOS product weekly", + "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + "location": null, + "type": "update", + "mode": null, + "url": null, + "isAllDay": true, + "startsAt": null, + "endsAt": null, + "startDate": "2026-09-16", + "endDate": null, + "localStart": null, + "timeZone": null, + "interestCount": 0, + "requiresTicket": false, + "tickets": [], + "cover": { + "id": "media_r22avnnrfch9ee62nhzkcn", + "filename": "cover.png", + "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + "contentType": "image/png", + "contentLength": 114085, + "width": 3000, + "height": 2100, + "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + } + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null + } + ] + }, + "PublicTopicPageOutput": { + "type": "object", "properties": { "items": { "type": "array", @@ -2321,7 +2930,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -2332,7 +2941,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -2343,7 +2952,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -2351,24 +2960,32 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public topics" + "description": "A cursor-paginated page of public topics", + "examples": [ + { + "items": [ + { + "id": "topic_6c2efd472dvt8rf9k4ftcc", + "name": "DFOS Blog", + "description": "Announcements, product notes, and essays from the DFOS team." + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 1 + } + ] }, "PublicProductReleaseRefOutput": { "type": "object", "properties": { "slug": { "type": "string", - "description": "The release's public page slug — hand it to `GET /spaces/{space}/releases/{slug}` for the full page. Mutable, like every slug here.", - "examples": [ - "blue-record" - ] + "description": "The release's public page slug. Pass it to `GET /spaces/{space}/releases/{slug}`." }, "name": { "type": "string", - "description": "Release name", - "examples": [ - "Blue Record" - ] + "description": "Release name" } }, "required": [ @@ -2376,31 +2993,31 @@ "name" ], "additionalProperties": false, - "description": "The release a product belongs to" + "description": "The release a product belongs to", + "examples": [ + { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + } + ] }, "PublicProductOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable store-product id. Store this identifier; the slug may change.", - "examples": [ - "sprod_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable store-product id." }, "slug": { "type": "string", - "description": "The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "softr-ticket" + "the-dark-forest-anthology-of-the-internet-digital" ] }, "name": { "type": "string", - "description": "Product name", - "examples": [ - "SOFTR Ticket" - ] + "description": "Product name" }, "description": { "anyOf": [ @@ -2411,7 +3028,7 @@ "type": "null" } ], - "description": "Seller-authored plain-text description. Newlines are significant; render with preserved whitespace." + "description": "Plain-text description. Newlines are significant." }, "kind": { "enum": [ @@ -2419,22 +3036,22 @@ "physical" ], "type": "string", - "description": "What the product delivers. Open enum — treat an unrecognized value as an opaque string." + "description": "What the product delivers. Open enum." }, "image": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Hero image, when the product has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL." + "description": "Hero image, when the product has one. Always a permanent public CDN URL." }, "prices": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProductPriceOutput" }, - "description": "Every purchasable price tier, one-time and recurring alike. Multiple tiers are normal (e.g. a one-time price alongside a monthly subscription)." + "description": "Every purchasable price tier." }, "purchaseGrantsMembership": { "type": "boolean", - "description": "Whether buying this product makes the buyer a member of the space. A PRODUCT fact, identical for every caller — it says what the purchase does, not what any particular viewer would get from it (a caller who is already a member gains nothing new, and this field does not know or say so)." + "description": "Whether buying this product makes the buyer a member of the space." }, "available": { "anyOf": [ @@ -2447,7 +3064,7 @@ "type": "null" } ], - "description": "Remaining stock, or null when the product has unlimited stock. Advisory only — availability is re-checked at purchase." + "description": "Remaining stock, or null when unlimited. Advisory; stock is re-checked at purchase." }, "isSoldOut": { "type": "boolean", @@ -2455,33 +3072,24 @@ }, "purchasable": { "type": "boolean", - "description": "Whether a purchase can be STARTED right now. False when the platform payment rail is paused or the product has no completable price tier — render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message." + "description": "Whether a purchase can be started right now. False when the payment rail is paused or no price tier is completable." }, "sortOrder": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "This product's position in the space's public product list, counting from 0 — LOWER SORTS FIRST. Dense and contiguous across the whole list (0, 1, 2, …) and meaningful only within one space; products with no public page are not counted, so this reveals nothing about a space's unpublished catalog. The index is ordered by this field. It is a POSITION, not a stable identifier — publishing, unpublishing, or reordering shifts it, so never store it as a key.", - "examples": [ - 0 - ] + "description": "Position in the space's public product list, from 0, lowest first. Dense over publicly-visible products only, and not a stable key." }, "ticketFor": { "type": "object", "properties": { "seriesId": { "type": "string", - "description": "The event series this product admits — the same `seriesId` the events namespace emits.", - "examples": [ - "evt_6encc4akrze2ah9kntzd9t" - ] + "description": "The event series this product admits." }, "recurrenceId": { "type": "string", - "description": "RFC 5545 RECURRENCE-ID of the NEXT upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence.", - "examples": [ - "2026-08-15T19:00:00" - ] + "description": "RFC 5545 RECURRENCE-ID of the next upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event." }, "startsAt": { "anyOf": [ @@ -2493,10 +3101,7 @@ "type": "null" } ], - "description": "That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event.", - "examples": [ - "2026-08-16T00:00:00.000Z" - ] + "description": "The occurrence's start instant (ISO 8601 UTC). Null for an all-day event." }, "startDate": { "anyOf": [ @@ -2507,10 +3112,7 @@ "type": "null" } ], - "description": "All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event.", - "examples": [ - "2026-08-15" - ] + "description": "All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event." } }, "required": [ @@ -2520,19 +3122,16 @@ "startDate" ], "additionalProperties": false, - "description": "The next upcoming PUBLIC event occurrence this product admits, when one exists — a ticket product’s backlink to what it sells entry to. SINGULAR BY DESIGN: a product may admit several events (a season pass), and this names the SOONEST upcoming occurrence across all of them — the one date a purchase page should send a buyer to — never a claim that only one event is admitted. Read the events namespace for the full relation. Absent when the product is not a ticket, when its ticket binding has been detached, when the event it admits is not publicly visible, and when no occurrence falls inside the window this API serves (the same ~1 year the events routes look ahead)." + "description": "The soonest upcoming public event occurrence this product admits. A product may admit several; read the events namespace for the full relation. Absent when the product is not a ticket, its binding is detached, the event is not public, or no occurrence falls in the coming year." }, "release": { "$ref": "#/components/schemas/PublicProductReleaseRefOutput", - "description": "The release this product belongs to, when a publicly-visible one holds it — a one-line context chip, not a projection of the release. SINGULAR: a product composed into several releases carries the EARLIEST-published public one (ties broken by release id). Absent when the product is in no release, and equally absent when every release holding it is unpublished — the two are deliberately indistinguishable. Read `GET /spaces/{space}/releases/{slug}` for the release itself." + "description": "The release this product belongs to, when a public one holds it. A product in several releases carries the earliest-published public one." }, "createdAt": { "type": "string", "format": "date-time", - "description": "When the product was created (ISO 8601 UTC). The cross-space index `GET /products` is ordered by this value, newest first. It is the row's own creation stamp — NOT \"when it was published\", which this surface does not record, and not a position in any catalog.", - "examples": [ - "2026-09-01T17:00:00.000Z" - ] + "description": "When the product was created (ISO 8601 UTC). `GET /products` orders by this value, newest first." } }, "required": [ @@ -2550,7 +3149,44 @@ "createdAt" ], "additionalProperties": false, - "description": "A store product with a public purchase page" + "description": "A store product with a public purchase page", + "examples": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "kind": "digital", + "image": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "purchaseGrantsMembership": true, + "available": null, + "isSoldOut": false, + "purchasable": true, + "sortOrder": 0, + "release": { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + }, + "createdAt": "2026-08-08T13:24:36.433Z" + } + ] }, "PublicProductPageOutput": { "type": "object", @@ -2571,7 +3207,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -2582,7 +3218,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -2593,7 +3229,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -2601,31 +3237,69 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public products" + "description": "A page of public products", + "examples": [ + { + "items": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "kind": "digital", + "image": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "purchaseGrantsMembership": true, + "available": null, + "isSoldOut": false, + "purchasable": true, + "sortOrder": 0, + "release": { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + }, + "createdAt": "2026-08-08T13:24:36.433Z" + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null, + "totalCount": 2 + } + ] }, "PublicProductFeedItemOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable store-product id. Store this identifier; the slug may change.", - "examples": [ - "sprod_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable store-product id." }, "slug": { "type": "string", - "description": "The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "softr-ticket" + "the-dark-forest-anthology-of-the-internet-digital" ] }, "name": { "type": "string", - "description": "Product name", - "examples": [ - "SOFTR Ticket" - ] + "description": "Product name" }, "description": { "anyOf": [ @@ -2636,7 +3310,7 @@ "type": "null" } ], - "description": "Seller-authored plain-text description. Newlines are significant; render with preserved whitespace." + "description": "Plain-text description. Newlines are significant." }, "kind": { "enum": [ @@ -2644,22 +3318,22 @@ "physical" ], "type": "string", - "description": "What the product delivers. Open enum — treat an unrecognized value as an opaque string." + "description": "What the product delivers. Open enum." }, "image": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Hero image, when the product has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL." + "description": "Hero image, when the product has one. Always a permanent public CDN URL." }, "prices": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProductPriceOutput" }, - "description": "Every purchasable price tier, one-time and recurring alike. Multiple tiers are normal (e.g. a one-time price alongside a monthly subscription)." + "description": "Every purchasable price tier." }, "purchaseGrantsMembership": { "type": "boolean", - "description": "Whether buying this product makes the buyer a member of the space. A PRODUCT fact, identical for every caller — it says what the purchase does, not what any particular viewer would get from it (a caller who is already a member gains nothing new, and this field does not know or say so)." + "description": "Whether buying this product makes the buyer a member of the space." }, "available": { "anyOf": [ @@ -2672,7 +3346,7 @@ "type": "null" } ], - "description": "Remaining stock, or null when the product has unlimited stock. Advisory only — availability is re-checked at purchase." + "description": "Remaining stock, or null when unlimited. Advisory; stock is re-checked at purchase." }, "isSoldOut": { "type": "boolean", @@ -2680,33 +3354,24 @@ }, "purchasable": { "type": "boolean", - "description": "Whether a purchase can be STARTED right now. False when the platform payment rail is paused or the product has no completable price tier — render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message." + "description": "Whether a purchase can be started right now. False when the payment rail is paused or no price tier is completable." }, "sortOrder": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "This product's position in the space's public product list, counting from 0 — LOWER SORTS FIRST. Dense and contiguous across the whole list (0, 1, 2, …) and meaningful only within one space; products with no public page are not counted, so this reveals nothing about a space's unpublished catalog. The index is ordered by this field. It is a POSITION, not a stable identifier — publishing, unpublishing, or reordering shifts it, so never store it as a key.", - "examples": [ - 0 - ] + "description": "Position in the space's public product list, from 0, lowest first. Dense over publicly-visible products only, and not a stable key." }, "ticketFor": { "type": "object", "properties": { "seriesId": { "type": "string", - "description": "The event series this product admits — the same `seriesId` the events namespace emits.", - "examples": [ - "evt_6encc4akrze2ah9kntzd9t" - ] + "description": "The event series this product admits." }, "recurrenceId": { "type": "string", - "description": "RFC 5545 RECURRENCE-ID of the NEXT upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence.", - "examples": [ - "2026-08-15T19:00:00" - ] + "description": "RFC 5545 RECURRENCE-ID of the next upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event." }, "startsAt": { "anyOf": [ @@ -2718,10 +3383,7 @@ "type": "null" } ], - "description": "That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event.", - "examples": [ - "2026-08-16T00:00:00.000Z" - ] + "description": "The occurrence's start instant (ISO 8601 UTC). Null for an all-day event." }, "startDate": { "anyOf": [ @@ -2732,10 +3394,7 @@ "type": "null" } ], - "description": "All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event.", - "examples": [ - "2026-08-15" - ] + "description": "All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event." } }, "required": [ @@ -2745,19 +3404,16 @@ "startDate" ], "additionalProperties": false, - "description": "The next upcoming PUBLIC event occurrence this product admits, when one exists — a ticket product’s backlink to what it sells entry to. SINGULAR BY DESIGN: a product may admit several events (a season pass), and this names the SOONEST upcoming occurrence across all of them — the one date a purchase page should send a buyer to — never a claim that only one event is admitted. Read the events namespace for the full relation. Absent when the product is not a ticket, when its ticket binding has been detached, when the event it admits is not publicly visible, and when no occurrence falls inside the window this API serves (the same ~1 year the events routes look ahead)." + "description": "The soonest upcoming public event occurrence this product admits. A product may admit several; read the events namespace for the full relation. Absent when the product is not a ticket, its binding is detached, the event is not public, or no occurrence falls in the coming year." }, "release": { "$ref": "#/components/schemas/PublicProductReleaseRefOutput", - "description": "The release this product belongs to, when a publicly-visible one holds it — a one-line context chip, not a projection of the release. SINGULAR: a product composed into several releases carries the EARLIEST-published public one (ties broken by release id). Absent when the product is in no release, and equally absent when every release holding it is unpublished — the two are deliberately indistinguishable. Read `GET /spaces/{space}/releases/{slug}` for the release itself." + "description": "The release this product belongs to, when a public one holds it. A product in several releases carries the earliest-published public one." }, "createdAt": { "type": "string", "format": "date-time", - "description": "When the product was created (ISO 8601 UTC). The cross-space index `GET /products` is ordered by this value, newest first. It is the row's own creation stamp — NOT \"when it was published\", which this surface does not record, and not a position in any catalog.", - "examples": [ - "2026-09-01T17:00:00.000Z" - ] + "description": "When the product was created (ISO 8601 UTC). `GET /products` orders by this value, newest first." }, "space": { "$ref": "#/components/schemas/PublicSpaceRefOutput" @@ -2779,7 +3435,51 @@ "space" ], "additionalProperties": false, - "description": "A public product with the space it belongs to" + "description": "A public product with the space it belongs to", + "examples": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "kind": "digital", + "image": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "purchaseGrantsMembership": true, + "available": null, + "isSoldOut": false, + "purchasable": true, + "sortOrder": 0, + "release": { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + }, + "createdAt": "2026-08-08T13:24:36.433Z", + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + } + ] }, "PublicProductFeedPageOutput": { "type": "object", @@ -2800,7 +3500,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -2811,7 +3511,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -2822,7 +3522,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -2830,7 +3530,57 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of cross-space public products" + "description": "A page of cross-space public products", + "examples": [ + { + "items": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "kind": "digital", + "image": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "purchaseGrantsMembership": true, + "available": null, + "isSoldOut": false, + "purchasable": true, + "sortOrder": 0, + "release": { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + }, + "createdAt": "2026-08-08T13:24:36.433Z", + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null + } + ] }, "PublicReleaseState": { "enum": [ @@ -2839,24 +3589,18 @@ "ended" ], "type": "string", - "description": "Where the release is in time, DERIVED from its dates rather than stored. `upcoming` = published, but `releasesAt` is still in the future; `live` = out; `ended` = `closesAt` has passed and the page is archival. Open enum — treat an unrecognized value as an opaque string." + "description": "Where the release is in time: `upcoming` before `releasesAt`, `live` once out, `ended` past `closesAt`. Open enum." }, "PublicReleaseCreditOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable credit id.", - "examples": [ - "relcred_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable credit id." }, "displayName": { "type": "string", - "description": "The credited name, exactly as the release renders it.", - "examples": [ - "Lena Ortiz" - ] + "description": "The credited name." }, "role": { "anyOf": [ @@ -2867,10 +3611,7 @@ "type": "null" } ], - "description": "What they did. Free text authored by the runner, never an enum — \"mastering\", \"cover photograph\", \"with thanks to\".", - "examples": [ - "mastering" - ] + "description": "The credit role, free text (\"mastering\"). Null when unset." } }, "required": [ @@ -2879,17 +3620,21 @@ "role" ], "additionalProperties": false, - "description": "A credit on a release" + "description": "A credit on a release", + "examples": [ + { + "id": "relcred_aha44ncvfnfk8t23ct3v7r", + "displayName": "Yancey", + "role": "editor" + } + ] }, "PublicReleaseMediaOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable wall-entry id (not the media id — that is `media.id`).", - "examples": [ - "relmed_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable wall-entry id (not `media.id`)." }, "caption": { "anyOf": [ @@ -2900,25 +3645,22 @@ "type": "null" } ], - "description": "Runner-authored caption for this entry, when there is one." + "description": "Caption for this entry, when there is one." }, "media": { "type": "object", "properties": { "id": { "type": "string", - "description": "Media object id — the value an `attachment://` inline body token references. Use it to associate a body token with its entry in `bodyMedia` (required when a body carries more than one inline media item). Media ids already appear verbatim in the body markdown, so this exposes nothing new.", - "examples": [ - "media_6encc4akrze2ah9kntzd9t" - ] + "description": "Media object id — what an `attachment://` inline body token references. Use it to match a token to its `bodyMedia` entry." }, "filename": { "type": "string", - "description": "Original uploaded filename of the media object" + "description": "Original uploaded filename" }, "url": { "type": "string", - "description": "Resolved URL for the media object. A permanent, unsigned imgix CDN URL for public images; a time-limited SIGNED URL for private media (audio/video/files). When signed, `urlExpiresAt` is present — never persist a signed URL, re-fetch the post for a fresh one." + "description": "Resolved URL for the media object. Permanent for public images; a time-limited signed URL for private media, which carries `urlExpiresAt`." }, "contentType": { "type": "string", @@ -2928,7 +3670,7 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "Size of the media object in bytes (absent until upload is finalized)" + "description": "Size in bytes, absent until the upload is finalized" }, "width": { "type": "integer", @@ -2948,7 +3690,7 @@ }, "alt": { "type": "string", - "description": "Uploader-authored caption / alt text, when present" + "description": "Uploader-authored caption or alt text" }, "durationMs": { "type": "integer", @@ -2958,16 +3700,16 @@ }, "posterUrl": { "type": "string", - "description": "CDN URL of an extracted poster frame / cover art (audio/video only)" + "description": "CDN URL of an extracted poster frame or cover art (audio/video only)" }, "playbackUrl": { "type": "string", - "description": "Streamable MP4 rendition URL (audio/video only). A permanent CDN URL for public media; a time-limited SIGNED URL for private media (in which case `urlExpiresAt` is present)." + "description": "Streamable MP4 rendition URL (audio/video only). Permanent for public media; a time-limited signed URL for private media." }, "urlExpiresAt": { "type": "string", "format": "date-time", - "description": "When the SIGNED `url` / `playbackUrl` expire (ISO 8601 UTC). PRESENT iff those URLs are time-limited signed URLs (private media); ABSENT means they are permanent (public images). Never persist a signed URL — re-fetch the post to obtain fresh ones." + "description": "When the signed `url` and `playbackUrl` expire (ISO 8601 UTC). Present only for private media. Signed URLs are ephemeral — re-fetch rather than persisting them." }, "waveformPeaks": { "type": "array", @@ -2987,7 +3729,19 @@ "contentType" ], "additionalProperties": false, - "description": "The asset. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL, so a wall entry whose asset is private media is omitted from the wall entirely rather than served with an expiring link." + "description": "The asset. Always a permanent public CDN URL.", + "examples": [ + { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + } + ] } }, "required": [ @@ -2996,35 +3750,42 @@ "media" ], "additionalProperties": false, - "description": "An entry on a release media wall" + "description": "An entry on a release media wall", + "examples": [ + { + "id": "relmed_h4zdvrtz9nr73nc7v3n99e", + "caption": null, + "media": { + "id": "media_9tnfvccf6t372vdnv43ftv", + "filename": "dfos.png", + "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + "contentType": "image/png", + "contentLength": 7045192, + "width": 2729, + "height": 2129, + "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + } + } + ] }, "PublicReleaseEventOutput": { "type": "object", "properties": { "seriesId": { "type": "string", - "description": "The event series — the same `seriesId` the events namespace emits.", - "examples": [ - "evt_6encc4akrze2ah9kntzd9t" - ] + "description": "The event series id." }, "recurrenceId": { "type": "string", - "description": "RFC 5545 RECURRENCE-ID of the occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence.", - "examples": [ - "2026-09-12T19:00:00" - ] + "description": "RFC 5545 RECURRENCE-ID of the occurrence: local wall-time (`YYYY-MM-DDTHH:MM:SS`), or a date (`YYYY-MM-DD`) when all-day." }, "title": { "type": "string", - "description": "Event title", - "examples": [ - "Listening party" - ] + "description": "Event title" }, "isAllDay": { "type": "boolean", - "description": "True for a date-based occurrence: `startDate` carries the date and `startsAt` is null. False for a timed one, where the reverse holds." + "description": "True for a date-based occurrence: `startDate` is set, `startsAt` is null." }, "timeZone": { "anyOf": [ @@ -3035,7 +3796,7 @@ "type": "null" } ], - "description": "Timed only: the IANA zone the event was authored in — the zone `recurrenceId` is expressed in. Null for an all-day event.", + "description": "Timed only: the IANA zone `recurrenceId` is expressed in. Null for an all-day event.", "examples": [ "America/New_York" ] @@ -3050,10 +3811,7 @@ "type": "null" } ], - "description": "That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event.", - "examples": [ - "2026-09-13T00:00:00.000Z" - ] + "description": "The occurrence's start instant (ISO 8601 UTC). Null for an all-day event." }, "startDate": { "anyOf": [ @@ -3064,10 +3822,7 @@ "type": "null" } ], - "description": "All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event.", - "examples": [ - "2026-09-12" - ] + "description": "All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event." } }, "required": [ @@ -3080,31 +3835,36 @@ "startDate" ], "additionalProperties": false, - "description": "A public calendar event a release points at" + "description": "A public event a release points at. Address it in the events namespace as `(seriesId, recurrenceId)`.", + "examples": [ + { + "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + "recurrenceId": "2026-09-09", + "title": "DFOS product weekly", + "isAllDay": true, + "timeZone": null, + "startsAt": null, + "startDate": "2026-09-09" + } + ] }, "PublicReleaseSummaryOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable release id. Store this identifier; the slug may change.", - "examples": [ - "rel_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable release id." }, "slug": { "type": "string", - "description": "The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "blue-record" + "the-dark-forest-anthology-of-the-internet" ] }, "name": { "type": "string", - "description": "Release name", - "examples": [ - "Blue Record" - ] + "description": "Release name" }, "state": { "$ref": "#/components/schemas/PublicReleaseState" @@ -3118,11 +3878,11 @@ "type": "null" } ], - "description": "One-line blurb. Newlines are not expected here; render as a single line of text." + "description": "One-line blurb." }, "cover": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL." + "description": "Cover image, when the release has one. Always a permanent public CDN URL." }, "catalogNumber": { "anyOf": [ @@ -3133,7 +3893,7 @@ "type": "null" } ], - "description": "The space's own catalog expression (\"MTL-001\", \"no. 4\"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it.", + "description": "The space's own catalog expression. Free text, not unique, never a key.", "examples": [ "MTL-001" ] @@ -3147,7 +3907,7 @@ "type": "null" } ], - "description": "Freeform place string, purely presentational. Never geocoded, never a key.", + "description": "Freeform place string. Never geocoded, never a key.", "examples": [ "Austin, TX" ] @@ -3155,10 +3915,7 @@ "publishedAt": { "type": "string", "format": "date-time", - "description": "When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts.", - "examples": [ - "2026-09-01T17:00:00.000Z" - ] + "description": "When this release was published (ISO 8601 UTC). Release lists order by this value, newest first." }, "releasesAt": { "anyOf": [ @@ -3170,10 +3927,7 @@ "type": "null" } ], - "description": "When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case.", - "examples": [ - "2026-09-12T17:00:00.000Z" - ] + "description": "When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set." }, "closesAt": { "anyOf": [ @@ -3185,7 +3939,7 @@ "type": "null" } ], - "description": "When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date.", + "description": "When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set.", "examples": [ "2026-10-12T17:00:00.000Z" ] @@ -3194,10 +3948,7 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published.", - "examples": [ - 2 - ] + "description": "How many of the composed products are publicly purchasable." } }, "required": [ @@ -3214,31 +3965,50 @@ "productCount" ], "additionalProperties": false, - "description": "A release in a space public release shelf" + "description": "A release in a space's public shelf", + "examples": [ + { + "id": "rel_vdeaff8zac8cr49fa624dh", + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet", + "state": "live", + "shortDescription": "The digital edition, readable anywhere, out now.", + "cover": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "catalogNumber": null, + "location": null, + "publishedAt": "2026-08-08T13:24:36.433Z", + "releasesAt": "2026-08-08T13:24:36.433Z", + "closesAt": null, + "productCount": 1 + } + ] }, "PublicReleaseOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable release id. Store this identifier; the slug may change.", - "examples": [ - "rel_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable release id." }, "slug": { "type": "string", - "description": "The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "blue-record" + "the-dark-forest-anthology-of-the-internet" ] }, "name": { "type": "string", - "description": "Release name", - "examples": [ - "Blue Record" - ] + "description": "Release name" }, "state": { "$ref": "#/components/schemas/PublicReleaseState" @@ -3252,11 +4022,11 @@ "type": "null" } ], - "description": "One-line blurb. Newlines are not expected here; render as a single line of text." + "description": "One-line blurb." }, "cover": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL." + "description": "Cover image, when the release has one. Always a permanent public CDN URL." }, "catalogNumber": { "anyOf": [ @@ -3267,7 +4037,7 @@ "type": "null" } ], - "description": "The space's own catalog expression (\"MTL-001\", \"no. 4\"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it.", + "description": "The space's own catalog expression. Free text, not unique, never a key.", "examples": [ "MTL-001" ] @@ -3281,7 +4051,7 @@ "type": "null" } ], - "description": "Freeform place string, purely presentational. Never geocoded, never a key.", + "description": "Freeform place string. Never geocoded, never a key.", "examples": [ "Austin, TX" ] @@ -3289,10 +4059,7 @@ "publishedAt": { "type": "string", "format": "date-time", - "description": "When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts.", - "examples": [ - "2026-09-01T17:00:00.000Z" - ] + "description": "When this release was published (ISO 8601 UTC). Release lists order by this value, newest first." }, "releasesAt": { "anyOf": [ @@ -3304,10 +4071,7 @@ "type": "null" } ], - "description": "When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case.", - "examples": [ - "2026-09-12T17:00:00.000Z" - ] + "description": "When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set." }, "closesAt": { "anyOf": [ @@ -3319,7 +4083,7 @@ "type": "null" } ], - "description": "When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date.", + "description": "When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set.", "examples": [ "2026-10-12T17:00:00.000Z" ] @@ -3328,10 +4092,7 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published.", - "examples": [ - 2 - ] + "description": "How many of the composed products are publicly purchasable." }, "longDescription": { "anyOf": [ @@ -3342,35 +4103,35 @@ "type": "null" } ], - "description": "Long-form release copy. Newlines are significant; render with preserved whitespace." + "description": "Long-form release copy. Newlines are significant." }, "media": { "type": "array", "items": { "$ref": "#/components/schemas/PublicReleaseMediaOutput" }, - "description": "The media wall, in the runner's wall order. Entries whose asset is private media are omitted (this surface emits no signed URLs), so an empty wall is a valid answer." + "description": "The media wall, in wall order. Entries backed by private media are omitted." }, "credits": { "type": "array", "items": { "$ref": "#/components/schemas/PublicReleaseCreditOutput" }, - "description": "Credits, in credit order. Display text only — a name and an optional role. If credits ever carry a subject, only accepted ones appear, and a pending invitation is indistinguishable from a credit that was never offered." + "description": "Credits, in credit order. A name and an optional role." }, "products": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProductOutput" }, - "description": "The composed products, in the release's curated order — the SAME objects `products.getProduct` returns, filtered by the store's own public-visibility rule. A product with no public purchase page is absent from this list while the release page renders normally, and is not counted in `productCount`." + "description": "The composed products, in the release's curated order. Products with no public page are absent and uncounted." }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/PublicReleaseEventOutput" }, - "description": "Public calendar events attached to this release, soonest first. Each is filtered by the EVENTS visibility rules; an event an anonymous caller may not see, or one with no occurrence in the window this API serves, is simply absent." + "description": "Public events attached to this release, soonest first. Events an anonymous caller may not see are absent." } }, "required": [ @@ -3392,7 +4153,94 @@ "events" ], "additionalProperties": false, - "description": "A release public page" + "description": "A release public page", + "examples": [ + { + "id": "rel_vdeaff8zac8cr49fa624dh", + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet", + "state": "live", + "shortDescription": "The digital edition, readable anywhere, out now.", + "cover": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "catalogNumber": null, + "location": null, + "publishedAt": "2026-08-08T13:24:36.433Z", + "releasesAt": "2026-08-08T13:24:36.433Z", + "closesAt": null, + "productCount": 1, + "longDescription": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "media": [ + { + "id": "relmed_h4zdvrtz9nr73nc7v3n99e", + "caption": null, + "media": { + "id": "media_9tnfvccf6t372vdnv43ftv", + "filename": "dfos.png", + "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + "contentType": "image/png", + "contentLength": 7045192, + "width": 2729, + "height": 2129, + "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + } + } + ], + "credits": [ + { + "id": "relcred_aha44ncvfnfk8t23ct3v7r", + "displayName": "Yancey", + "role": "editor" + } + ], + "products": [ + { + "id": "sprod_ra73keekz9f22k7889ren7", + "slug": "the-dark-forest-anthology-of-the-internet-digital", + "name": "The Dark Forest Anthology of the Internet (digital)", + "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + "kind": "digital", + "image": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "prices": [ + { + "id": "pprice_97hfv7rcea93267z8t677d", + "type": "one_time", + "amountCents": 1000, + "currency": "usd" + } + ], + "purchaseGrantsMembership": true, + "available": null, + "isSoldOut": false, + "purchasable": true, + "sortOrder": 0, + "release": { + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet" + }, + "createdAt": "2026-08-08T13:24:36.433Z" + } + ], + "events": [] + } + ] }, "PublicReleasePageOutput": { "type": "object", @@ -3413,7 +4261,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -3424,7 +4272,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -3435,7 +4283,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -3443,31 +4291,57 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public releases" + "description": "A page of public releases", + "examples": [ + { + "items": [ + { + "id": "rel_vdeaff8zac8cr49fa624dh", + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet", + "state": "live", + "shortDescription": "The digital edition, readable anywhere, out now.", + "cover": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "catalogNumber": null, + "location": null, + "publishedAt": "2026-08-08T13:24:36.433Z", + "releasesAt": "2026-08-08T13:24:36.433Z", + "closesAt": null, + "productCount": 1 + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 1 + } + ] }, "PublicReleaseFeedItemOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "Stable release id. Store this identifier; the slug may change.", - "examples": [ - "rel_6encc4akrze2ah9kntzd9t" - ] + "description": "Stable release id." }, "slug": { "type": "string", - "description": "The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical.", "examples": [ - "blue-record" + "the-dark-forest-anthology-of-the-internet" ] }, "name": { "type": "string", - "description": "Release name", - "examples": [ - "Blue Record" - ] + "description": "Release name" }, "state": { "$ref": "#/components/schemas/PublicReleaseState" @@ -3481,11 +4355,11 @@ "type": "null" } ], - "description": "One-line blurb. Newlines are not expected here; render as a single line of text." + "description": "One-line blurb." }, "cover": { "$ref": "#/components/schemas/PublicMediaOutput", - "description": "Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL." + "description": "Cover image, when the release has one. Always a permanent public CDN URL." }, "catalogNumber": { "anyOf": [ @@ -3496,7 +4370,7 @@ "type": "null" } ], - "description": "The space's own catalog expression (\"MTL-001\", \"no. 4\"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it.", + "description": "The space's own catalog expression. Free text, not unique, never a key.", "examples": [ "MTL-001" ] @@ -3510,7 +4384,7 @@ "type": "null" } ], - "description": "Freeform place string, purely presentational. Never geocoded, never a key.", + "description": "Freeform place string. Never geocoded, never a key.", "examples": [ "Austin, TX" ] @@ -3518,10 +4392,7 @@ "publishedAt": { "type": "string", "format": "date-time", - "description": "When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts.", - "examples": [ - "2026-09-01T17:00:00.000Z" - ] + "description": "When this release was published (ISO 8601 UTC). Release lists order by this value, newest first." }, "releasesAt": { "anyOf": [ @@ -3533,10 +4404,7 @@ "type": "null" } ], - "description": "When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case.", - "examples": [ - "2026-09-12T17:00:00.000Z" - ] + "description": "When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set." }, "closesAt": { "anyOf": [ @@ -3548,7 +4416,7 @@ "type": "null" } ], - "description": "When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date.", + "description": "When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set.", "examples": [ "2026-10-12T17:00:00.000Z" ] @@ -3557,10 +4425,7 @@ "type": "integer", "minimum": 0, "maximum": 9007199254740991, - "description": "How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published.", - "examples": [ - 2 - ] + "description": "How many of the composed products are publicly purchasable." }, "space": { "$ref": "#/components/schemas/PublicSpaceRefOutput" @@ -3581,7 +4446,39 @@ "space" ], "additionalProperties": false, - "description": "A public release with the space it belongs to" + "description": "A public release with the space it belongs to", + "examples": [ + { + "id": "rel_vdeaff8zac8cr49fa624dh", + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet", + "state": "live", + "shortDescription": "The digital edition, readable anywhere, out now.", + "cover": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "catalogNumber": null, + "location": null, + "publishedAt": "2026-08-08T13:24:36.433Z", + "releasesAt": "2026-08-08T13:24:36.433Z", + "closesAt": null, + "productCount": 1, + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + } + ] }, "PublicReleaseFeedPageOutput": { "type": "object", @@ -3602,7 +4499,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -3613,7 +4510,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -3624,7 +4521,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -3632,7 +4529,45 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of cross-space public releases" + "description": "A page of cross-space public releases", + "examples": [ + { + "items": [ + { + "id": "rel_vdeaff8zac8cr49fa624dh", + "slug": "the-dark-forest-anthology-of-the-internet", + "name": "The Dark Forest Anthology of the Internet", + "state": "live", + "shortDescription": "The digital edition, readable anywhere, out now.", + "cover": { + "id": "media_a9e9r767ca9frz7dvrfvna", + "filename": "GpE09OWasAAw1BX.jpeg", + "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + "contentType": "image/jpeg", + "contentLength": 2468014, + "width": 3200, + "height": 3200, + "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + }, + "catalogNumber": null, + "location": null, + "publishedAt": "2026-08-08T13:24:36.433Z", + "releasesAt": "2026-08-08T13:24:36.433Z", + "closesAt": null, + "productCount": 1, + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null + } + ] }, "PublicProfileLinkOutput": { "type": "object", @@ -3683,7 +4618,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the cached OG preview image, or null" + "description": "Public CDN URL for the cached OG preview image, or null" } }, "required": [ @@ -3694,14 +4629,23 @@ "imageUrl" ], "additionalProperties": false, - "description": "A link on a public user profile" + "description": "A link on a public user profile", + "examples": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ] }, "PublicUserOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "The user's identity entity id (`identity_…`) — a stable, URL-safe identifier, accepted anywhere `{user}` is. The canonical profile-URL identifier when the user has no handle." + "description": "The user's identity entity id (`identity_…`). Stable, and accepted anywhere `{user}` is." }, "did": { "$ref": "#/components/schemas/ProtocolDid" @@ -3715,7 +4659,7 @@ "type": "null" } ], - "description": "The user's handle, or null if unset. A MUTABLE alias — a handle can change or be released; the `id` and `did` are the canonical, stable identifiers to store." + "description": "The user's handle, or null if unset. A mutable alias; the `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -3737,7 +4681,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the avatar, or null" + "description": "Public CDN URL for the avatar, or null" }, "bio": { "anyOf": [ @@ -3759,7 +4703,7 @@ "type": "null" } ], - "description": "A domain this user has proven control of via origin binding, or null. The claim is a `DfosOrigin` entry on the user's identity chain, and the domain independently publishes the user's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the user claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction." + "description": "A domain this user has proven control of through origin binding, or null. Null when no claim is proven, or when the domain now attests a different identity." }, "links": { "type": "array", @@ -3794,21 +4738,21 @@ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for the identity profile content chain. Present iff the identity has a non-empty profile chain; absent when the profile is empty or its chain has not been created." + "description": "Protocol handles for the identity profile content chain. Absent when there is no profile chain." } }, "required": [ "headOpCid" ], "additionalProperties": false, - "description": "Protocol proof-plane handles for this user's identity and optional profile content chain. Present iff the identity chain exists; absent for legacy identities without a chain. Combine with `GET /protocol` to fetch and verify the chains from the relay." + "description": "Protocol proof-plane handles for this user's identity chain. Absent for identities without one. Resolve them through `GET /protocol`." }, "spaces": { "type": "array", "items": { "$ref": "#/components/schemas/PublicUserSpaceOutput" }, - "description": "The user's public space memberships: every pinned space (in slot order) followed by up to 20 more, most recently joined first. May be empty. Use `GET /users/{user}/spaces` for the complete, paginated list." + "description": "Every pinned space, then up to 20 more of the user's public spaces, most recently joined first. Use `GET /users/{user}/spaces` for the full list." } }, "required": [ @@ -3823,14 +4767,65 @@ "spaces" ], "additionalProperties": false, - "description": "A public user profile" + "description": "A public user profile", + "examples": [ + { + "id": "identity_3drf9nfk93na7vfdk723r6", + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "username": "bvalosek", + "displayName": "Brandon", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + "verifiedDomain": "bvalosek.com", + "links": [ + { + "url": "https://bvalosek.com", + "label": null, + "title": null, + "description": null, + "imageUrl": null + } + ], + "protocol": { + "headOpCid": "bafyreig6d4y67d5jmux475unozzu7tr5g7fqhjbegozwepumcy3pen3woy", + "profile": { + "contentId": "f4nrnnv7zv89t7nazdnk8t2zn7chvcf", + "headOpCid": "bafyreiasbpcbkct4mcqdopnz7vbk6yhdd4e35eotfnxsmg5vuxsw32ybx4" + } + }, + "spaces": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "memberCountSummary": "thousands of members", + "pinned": true, + "pinMessage": "Where I write about what we are building.", + "pinnedAt": "2026-08-14T16:41:07.882Z" + }, + { + "id": "space_z94a849d9kdftfvv3n9hn7", + "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + "domain": "rakowwwski", + "displayName": "POPULAR", + "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + "memberCountSummary": "about a dozen members", + "pinned": false, + "pinMessage": null, + "pinnedAt": null + } + ] + } + ] }, "PublicUserListItemOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "The user's identity entity id (`identity_…`) — a stable, URL-safe identifier, accepted anywhere `{user}` is. The canonical profile-URL identifier when the user has no handle." + "description": "The user's identity entity id (`identity_…`). Stable, and accepted anywhere `{user}` is." }, "did": { "$ref": "#/components/schemas/ProtocolDid" @@ -3844,7 +4839,7 @@ "type": "null" } ], - "description": "The user's handle, or null if unset. A MUTABLE alias — a handle can change or be released; the `id` and `did` are the canonical, stable identifiers to store." + "description": "The user's handle, or null if unset. A mutable alias; the `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -3866,7 +4861,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the avatar, or null" + "description": "Public CDN URL for the avatar, or null" }, "bio": { "anyOf": [ @@ -3888,7 +4883,7 @@ "type": "null" } ], - "description": "A domain this user has proven control of via origin binding, or null. The claim is a `DfosOrigin` entry on the user's identity chain, and the domain independently publishes the user's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the user claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction." + "description": "A domain this user has proven control of through origin binding, or null. Null when no claim is proven, or when the domain now attests a different identity." }, "links": { "type": "array", @@ -3902,7 +4897,7 @@ "items": { "$ref": "#/components/schemas/PublicUserSpaceCardOutput" }, - "description": "The user's pinned spaces in slot order (may be empty). Terse cards — fetch the profile for the full membership shelf." + "description": "The user's pinned spaces in slot order (may be empty)." } }, "required": [ @@ -3917,7 +4912,35 @@ "pinnedSpaces" ], "additionalProperties": false, - "description": "A public user profile in the directory listing" + "description": "A public user profile in the directory listing", + "examples": [ + { + "id": "identity_3drf9nfk93na7vfdk723r6", + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "username": "bvalosek", + "displayName": "Brandon", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + "verifiedDomain": "bvalosek.com", + "links": [ + { + "url": "https://bvalosek.com", + "label": null, + "title": null, + "description": null, + "imageUrl": null + } + ], + "pinnedSpaces": [ + { + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + ] + } + ] }, "PublicUserPageOutput": { "type": "object", @@ -3938,7 +4961,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -3949,7 +4972,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -3960,7 +4983,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -3968,14 +4991,60 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of public user profiles" + "description": "A cursor-paginated page of public user profiles", + "examples": [ + { + "items": [ + { + "id": "identity_3drf9nfk93na7vfdk723r6", + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "username": "bvalosek", + "displayName": "Brandon", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + "verifiedDomain": "bvalosek.com", + "links": [ + { + "url": "https://bvalosek.com", + "label": null, + "title": null, + "description": null, + "imageUrl": null + } + ], + "pinnedSpaces": [ + { + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + ] + }, + { + "id": "identity_349f9ka72nrefnf8kvrnhh", + "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + "username": "aron", + "displayName": "Aron", + "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg", + "bio": "Designing and navigating various contextual arrangements w/ an enigmatic sense of hope.", + "verifiedDomain": null, + "links": [], + "pinnedSpaces": [] + } + ], + "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + "previousCursor": null, + "totalCount": null + } + ] }, "PublicUserSpaceOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "The space's entity id (`space_…`). Already public as the permanent `space-{id}` subdomain form; a stable join key for consumers that also see the space through other surfaces." + "description": "The space's entity id (`space_…`). Canonical and stable." }, "did": { "$ref": "#/components/schemas/ProtocolDid" @@ -4004,15 +5073,15 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" }, "memberCountSummary": { "type": "string", - "description": "Worded member-count summary (e.g. \"a few dozen members\"). Public surfaces deliberately avoid exact counts." + "description": "Worded member-count summary, e.g. \"a few dozen members\". Spaces do not publish exact counts." }, "pinned": { "type": "boolean", - "description": "Whether the user pinned this space to the top of their profile. Pinned spaces come first in the array." + "description": "Whether the user pinned this space. Pinned spaces come first." }, "pinMessage": { "anyOf": [ @@ -4023,7 +5092,7 @@ "type": "null" } ], - "description": "The user's own short note about this space, or null. Only ever set when pinned." + "description": "The user's note about this space, or null. Set only when pinned." }, "pinnedAt": { "anyOf": [ @@ -4050,7 +5119,20 @@ "pinnedAt" ], "additionalProperties": false, - "description": "A space on a user's public profile" + "description": "A space on a user's public profile", + "examples": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "memberCountSummary": "thousands of members", + "pinned": true, + "pinMessage": "Where I write about what we are building.", + "pinnedAt": "2026-08-14T16:41:07.882Z" + } + ] }, "PublicUserSpaceCardOutput": { "type": "object", @@ -4060,7 +5142,7 @@ }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias." + "description": "Effective subdomain — custom domain if set, otherwise the `space-{id}` form. A mutable alias." }, "displayName": { "anyOf": [ @@ -4082,7 +5164,7 @@ "type": "null" } ], - "description": "Resolved public CDN URL for the space avatar, or null" + "description": "Public CDN URL for the space avatar, or null" } }, "required": [ @@ -4092,7 +5174,15 @@ "avatarUrl" ], "additionalProperties": false, - "description": "A terse pinned-space card on a directory listing row" + "description": "A terse pinned-space card on a directory listing row", + "examples": [ + { + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + } + ] }, "PublicUserSpacePageOutput": { "type": "object", @@ -4113,7 +5203,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -4124,7 +5214,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -4135,7 +5225,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -4143,16 +5233,47 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of a user's public spaces" - }, - "PublicGetPostOutput": { - "anyOf": [ + "description": "A cursor-paginated page of a user's public spaces", + "examples": [ { - "type": "object", - "properties": { - "state": { - "const": "eligible", - "description": "The post is anonymously readable — `post` carries the full public content." + "items": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "memberCountSummary": "thousands of members", + "pinned": true, + "pinMessage": "Where I write about what we are building.", + "pinnedAt": "2026-08-14T16:41:07.882Z" + }, + { + "id": "space_z94a849d9kdftfvv3n9hn7", + "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + "domain": "rakowwwski", + "displayName": "POPULAR", + "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + "memberCountSummary": "about a dozen members", + "pinned": false, + "pinMessage": null, + "pinnedAt": null + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 2 + } + ] + }, + "PublicGetPostOutput": { + "anyOf": [ + { + "type": "object", + "properties": { + "state": { + "const": "eligible", + "description": "The caller can read the post; `post` carries its content." }, "post": { "$ref": "#/components/schemas/PublicPostOutput" @@ -4167,14 +5288,14 @@ "space" ], "additionalProperties": false, - "description": "An anonymously-readable post with compact space context" + "description": "A readable post with compact space context" }, { "type": "object", "properties": { "state": { "const": "gated", - "description": "The post exists in this public space but is not anonymously readable — no content is returned, only a slim space CTA." + "description": "The post exists in this space but the caller cannot read it; only a space CTA is returned." }, "reason": { "enum": [ @@ -4183,7 +5304,7 @@ "restricted" ], "type": "string", - "description": "Why the post is gated. `signIn` — open to any signed-in visitor, so signing in reveals it (a sign-in wall, e.g. a `non-members` override). `spaceMembers` — plain space membership is sufficient to read, so joining the space grants access. `restricted` — a finer gate applies (a pass, a group, a private topic tier, etc.); joining the space is NOT enough on its own — visit the space to see how to gain access. Treat this as an open enum: finer reason values may be ADDED later, so handle an unrecognized value as `restricted`." + "description": "Why the post is gated: `signIn` (sign in to read it), `spaceMembers` (join the space), or `restricted` (a pass, group, or private topic). Open enum; unknown means `restricted`." }, "space": { "$ref": "#/components/schemas/PublicSpaceCtaOutput" @@ -4198,25 +5319,102 @@ "description": "A gated post — space CTA only, no content" } ], - "description": "A public post, or a gated space CTA when it is not anonymously readable" + "description": "A readable post, or a space CTA when it is gated", + "examples": [ + { + "state": "eligible", + "post": { + "id": "post_ze2kh2d47tzerkhet8348c", + "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + "format": "long-post", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + "body": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\n\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.", + "cover": { + "id": "media_efrvdarnc6zd3nhv389cf2", + "filename": "p04.jpg", + "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + "contentType": "image/jpeg", + "contentLength": 2890442, + "width": 2752, + "height": 1536, + "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + }, + "attachments": [], + "bodyMedia": [ + { + "id": "media_rn2926ahdrnra6t4erc862", + "filename": "2026-09-04-siwd-consent-composite-equal-height.png", + "url": "https://dfos.imgix.net/media/public/rn2926ahdrnra6t4erc862-2026-09-04-siwd-consent-composite-equal-height.png", + "contentType": "image/png", + "contentLength": 273173, + "width": 2116, + "height": 1160, + "blurHash": "eRS6Pl%Mt7%May~qWBWBt7WBxuayWBofj[%Mxut7Rjj[-;WBWBt7ay" + } + ], + "author": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "displayName": "Brandon", + "username": "bvalosek", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + }, + "topics": [ + { + "id": "topic_6c2efd472dvt8rf9k4ftcc", + "name": "DFOS Blog" + } + ], + "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + "protocol": { + "contentId": "d4743469vf6heca8t466ckknvzknha2", + "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + }, + "upvoteCount": 14, + "commentCount": 6, + "publishedAt": "2026-09-04T18:05:33.531Z", + "updatedAt": "2026-09-04T18:06:01.020Z" + }, + "space": { + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "domain": "home", + "joinMode": "open", + "subscribeEnabled": true, + "memberCountSummary": "thousands of members" + } + }, + { + "state": "gated", + "reason": "spaceMembers", + "space": { + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "domain": "home", + "joinMode": "open", + "subscribeEnabled": true, + "memberCountSummary": "thousands of members" + } + } + ] }, "PublicProtocolInfoOutput": { "type": "object", "properties": { "relayUrl": { "type": "string", - "description": "Base URL of the read-only DFOS relay serving the identity and content chains for the protocol DIDs this API emits (as `did` on spaces, authors, and future entities). Combine with a `did` to resolve its DID document / identity-chain head (e.g. `{relayUrl}/proof/v1/identities/{did}`). One relay per deployment stage.", - "examples": [ - "https://relay.dfos.com" - ] + "description": "Base URL of the read-only relay serving the chains for the DIDs this API emits. Combine it with a `did` (e.g. `{relayUrl}/proof/v1/identities/{did}`)." }, "didMethod": { "const": "did:dfos", - "description": "The DID method used by DFOS protocol identities" + "description": "The DID method DFOS identities use" }, "specUrl": { "const": "https://protocol.dfos.com/spec", - "description": "Canonical DFOS protocol specification URL" + "description": "The DFOS protocol specification URL" }, "endpoints": { "type": "object", @@ -4270,7 +5468,7 @@ "indexIdentities" ], "additionalProperties": false, - "description": "Templates for the relay's proof-plane and index endpoints; substitute the placeholders with values from `protocol` blocks on posts, users, and spaces." + "description": "URL templates for the relay proof plane and indexes. Substitute placeholders with values from the `protocol` blocks on posts, users, and spaces." } }, "required": [ @@ -4280,24 +5478,39 @@ "endpoints" ], "additionalProperties": false, - "description": "DFOS protocol discovery info" + "description": "DFOS protocol discovery info", + "examples": [ + { + "relayUrl": "https://relay.dfos.com", + "didMethod": "did:dfos", + "specUrl": "https://protocol.dfos.com/spec", + "endpoints": { + "wellKnown": "https://relay.dfos.com/.well-known/dfos-relay", + "identity": "https://relay.dfos.com/proof/v1/identities/{did}", + "identityLog": "https://relay.dfos.com/proof/v1/identities/{did}/log", + "content": "https://relay.dfos.com/proof/v1/content/{contentId}", + "contentLog": "https://relay.dfos.com/proof/v1/content/{contentId}/log", + "operation": "https://relay.dfos.com/proof/v1/operations/{cid}", + "blob": "https://relay.dfos.com/content/{contentId}/blob", + "indexContent": "https://relay.dfos.com/index/v0/content", + "indexIdentities": "https://relay.dfos.com/index/v0/identities" + } + } + ] }, "MembershipSpaceOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "The space's entity id (`space_…`) — canonical and stable.", - "examples": [ - "space_6encc4akrze2ah9kntzd9t" - ] + "description": "The space's entity id (`space_…`) — canonical and stable." }, "did": { "$ref": "#/components/schemas/ProtocolDid" }, "domain": { "type": "string", - "description": "Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A MUTABLE alias, and for a private space it may not resolve publicly at all; the `id` and `did` are canonical." + "description": "Effective subdomain, or the normalized `space-{id}` form. A mutable alias that may not resolve publicly for a private space; `id` and `did` are canonical." }, "displayName": { "anyOf": [ @@ -4341,7 +5554,7 @@ }, "memberCountSummary": { "type": "string", - "description": "Worded member-count summary (e.g. \"a few dozen members\"). DFOS surfaces deliberately avoid exact counts." + "description": "Worded member-count summary, e.g. \"a few dozen members\". Spaces do not publish exact counts." } }, "required": [ @@ -4355,17 +5568,34 @@ "memberCountSummary" ], "additionalProperties": false, - "description": "A space the credential subject belongs to" + "description": "A space the subject belongs to", + "examples": [ + { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "memberCountSummary": "thousands of members" + } + ] }, "MembershipGroupOutput": { "type": "object", "properties": { "id": { "type": "string", - "description": "The group's entity id (`group_…`) — canonical and stable.", - "examples": [ - "group_79h6z77had2kc68ffdkhac" - ] + "description": "The group's entity id (`group_…`) — canonical and stable." }, "did": { "$ref": "#/components/schemas/ProtocolDid" @@ -4431,24 +5661,18 @@ "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, - "description": "EXACT count of the group's active members — not a worded bucket. Spaces publish a worded `memberCountSummary` because a room's population is ambient; a group is an operational unit (the editors, the moderators, a paid tier) whose size has a real answer its own members already know. Reading it requires the `read:memberships` grant that opens this endpoint.", - "examples": [ - 7 - ] + "description": "Exact count of the group's active members. Groups publish an exact count; spaces publish the worded `memberCountSummary` instead." }, "spaceId": { "type": "string", - "description": "Entity id of the space this group belongs to. Correlate with `space.id` on `GET /memberships` to reassemble the full graph.", - "examples": [ - "space_6encc4akrze2ah9kntzd9t" - ] + "description": "Entity id of the space this group belongs to. Correlate with `space.id` on `GET /memberships`." }, "spaceDid": { "type": "string", "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", "description": "Protocol DID of the space this group belongs to — the space's own `did`.", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, @@ -4464,7 +5688,20 @@ "spaceDid" ], "additionalProperties": false, - "description": "A group the credential subject belongs to" + "description": "A group the subject belongs to", + "examples": [ + { + "id": "group_4kt7cn9vza3fe2h8dr6ac4", + "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + "name": "Developers", + "description": "Building on the DFOS API and protocol.", + "avatarUrl": null, + "color": "cyan", + "memberCount": 42, + "spaceId": "space_vnzfk7hth9vadc3daahd48", + "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + } + ] }, "MembershipOutput": { "type": "object", @@ -4485,10 +5722,7 @@ "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, - "description": "How many groups the subject belongs to inside this space (ACTIVE group memberships only). Often 0 — most members belong to no group. Walk `GET /group-memberships?space=…` for the groups themselves.", - "examples": [ - 2 - ] + "description": "How many groups the subject belongs to inside this space. Walk `GET /group-memberships?space=…` for the groups." }, "joinedAt": { "type": "string", @@ -4503,7 +5737,32 @@ "joinedAt" ], "additionalProperties": false, - "description": "One of the credential subject's space memberships" + "description": "One of the subject's space memberships", + "examples": [ + { + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "memberCountSummary": "thousands of members" + }, + "role": "member", + "groupCount": 1, + "joinedAt": "2026-01-14T19:02:11.000Z" + } + ] }, "MembershipPageOutput": { "type": "object", @@ -4524,7 +5783,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -4535,7 +5794,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -4546,7 +5805,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -4554,7 +5813,54 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of the credential subject's space memberships" + "description": "A cursor-paginated page of the subject's space memberships", + "examples": [ + { + "items": [ + { + "space": { + "id": "space_vnzfk7hth9vadc3daahd48", + "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + "domain": "home", + "displayName": "DFOS", + "description": "The official DFOS of DFOS", + "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + "links": [ + { + "url": "https://dfos.com/", + "label": null, + "title": "Dark Forest OS", + "description": null, + "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + } + ], + "memberCountSummary": "thousands of members" + }, + "role": "member", + "groupCount": 1, + "joinedAt": "2026-01-14T19:02:11.000Z" + }, + { + "space": { + "id": "space_z94a849d9kdftfvv3n9hn7", + "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + "domain": "rakowwwski", + "displayName": "POPULAR", + "description": "Popular is a space we share our creative process and research practices. We get to know our work to get to know ourselves.", + "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + "links": [], + "memberCountSummary": "about a dozen members" + }, + "role": "member", + "groupCount": 0, + "joinedAt": "2026-04-02T13:35:52.000Z" + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 2 + } + ] }, "GroupMembershipOutput": { "type": "object", @@ -4583,7 +5889,24 @@ "joinedAt" ], "additionalProperties": false, - "description": "One of the credential subject's group memberships" + "description": "One of the subject's group memberships", + "examples": [ + { + "group": { + "id": "group_4kt7cn9vza3fe2h8dr6ac4", + "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + "name": "Developers", + "description": "Building on the DFOS API and protocol.", + "avatarUrl": null, + "color": "cyan", + "memberCount": 42, + "spaceId": "space_vnzfk7hth9vadc3daahd48", + "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + }, + "role": "admin", + "joinedAt": "2026-01-14T19:02:11.000Z" + } + ] }, "GroupMembershipPageOutput": { "type": "object", @@ -4604,7 +5927,7 @@ "type": "null" } ], - "description": "Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse." + "description": "Cursor for the next page, or null at the end. Pass back verbatim as `after`." }, "previousCursor": { "anyOf": [ @@ -4615,7 +5938,7 @@ "type": "null" } ], - "description": "Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse." + "description": "Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported." }, "totalCount": { "anyOf": [ @@ -4626,7 +5949,7 @@ "type": "null" } ], - "description": "Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence." + "description": "Total matching items, or null when not computed. May be omitted; do not depend on its presence." } }, "required": [ @@ -4634,7 +5957,31 @@ "nextCursor" ], "additionalProperties": false, - "description": "A cursor-paginated page of the credential subject's group memberships" + "description": "A cursor-paginated page of the subject's group memberships", + "examples": [ + { + "items": [ + { + "group": { + "id": "group_4kt7cn9vza3fe2h8dr6ac4", + "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + "name": "Developers", + "description": "Building on the DFOS API and protocol.", + "avatarUrl": null, + "color": "cyan", + "memberCount": 42, + "spaceId": "space_vnzfk7hth9vadc3daahd48", + "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + }, + "role": "admin", + "joinedAt": "2026-01-14T19:02:11.000Z" + } + ], + "nextCursor": null, + "previousCursor": null, + "totalCount": 1 + } + ] }, "PublicCredentialTier": { "enum": [ @@ -4642,7 +5989,7 @@ "loopback" ], "type": "string", - "description": "How the application was resolved when this credential was issued. `jit` = resolved live from the app's `/.well-known/dfos-app.json`, which is how every domain-backed app resolves — there is no registry and no approval gate. `loopback` = the key-proven local tier: a client on somebody's machine that proved it holds the key the grant is addressed to. An open enum like every enum on this API: a member named `approved` existed in an earlier revision and was removed with the registry that backed it; no credential was ever issued under it." + "description": "How the application was resolved at consent time. `jit` = live from its `/.well-known/dfos-app.json`; `loopback` = a local client that proved its key. Open enum." }, "CredentialIntrospectionOutput": { "type": "object", @@ -4650,17 +5997,17 @@ "subjectDid": { "type": "string", "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", - "description": "Protocol DID of the user who ISSUED this credential — the subject every gated route serves. The same `did` `GET /profile` returns.", + "description": "Protocol DID of the user who issued this credential — the subject every gated route serves.", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] }, "clientDid": { "type": "string", "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", - "description": "Protocol DID of the application this credential was issued TO — the audience. A credential is inert without this identity’s key; it is not a bearer token.", + "description": "Protocol DID of the application this credential was issued to. Not a bearer token: inert without that identity’s key.", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] }, "scopes": { @@ -4668,13 +6015,7 @@ "items": { "type": "string" }, - "description": "The actions this grant carries, e.g. `[\"read:profile\",\"read:memberships\"]`, in DFOS’s canonical order. This is the authoritative list — a route refuses with `403` when the action it needs is absent here. Treat unrecognized members as opaque strings.", - "examples": [ - [ - "read:profile", - "read:memberships" - ] - ] + "description": "The actions this grant carries, e.g. `[\"read:profile\",\"read:memberships\"]`. A route refuses with `403` when the action it needs is absent. Unrecognized members are opaque strings." }, "attenuation": { "type": "array", @@ -4683,17 +6024,11 @@ "properties": { "resource": { "type": "string", - "description": "The resource this entry grants over. `api:` is the whole API — for a space-level action that means EVERY space the user belongs to, including ones they join later. `api:/spaces/<31-char space id>` is one named space.", - "examples": [ - "api:api.dfos.com" - ] + "description": "The resource this entry grants over. `api:` is the whole API, including spaces the user joins later; `api:/spaces/` is one space." }, "action": { "type": "string", - "description": "The comma-separated SET of action tokens this entry carries, in DFOS’s canonical order.", - "examples": [ - "read:profile,read:email" - ] + "description": "Comma-separated set of action tokens this entry carries, in canonical order." } }, "required": [ @@ -4702,15 +6037,7 @@ ], "additionalProperties": false }, - "description": "THE GRANT ITSELF, exactly as it was signed — one entry per resource. `scopes` above says WHAT this credential carries; this says what it carries WHERE, and it is the authoritative answer to both. A credential minted before grants had places carries a single `api:` entry; one narrowed to specific spaces carries one child entry each, and may carry no `api:` entry at all. Treat unrecognized resource forms as opaque strings and do not infer coverage from their shape — the grant you hold is the authority, not a rule you derived from it.", - "examples": [ - [ - { - "resource": "api:api.dfos.com", - "action": "read:profile,read:email" - } - ] - ] + "description": "The grant as signed, one entry per resource: `scopes` says what this credential carries, this says where. Treat unrecognized resource forms as opaque strings." }, "tier": { "$ref": "#/components/schemas/PublicCredentialTier" @@ -4724,10 +6051,7 @@ "type": "null" } ], - "description": "Bare hostname the grant was issued to, or `null`. **A null domain means a LOCAL application** — the `loopback` tier has no domain because a local client proved a key rather than an origin, so there is no hostname that would be true to show. Fall back to `clientDid` rather than inventing one.", - "examples": [ - "example.com" - ] + "description": "Bare hostname the grant was issued to, or `null`. Null on the `loopback` tier, which proved a key rather than an origin; identify the holder by `clientDid`." }, "issuedAt": { "type": "string", @@ -4737,7 +6061,7 @@ "expiresAt": { "type": "string", "format": "date-time", - "description": "When this credential lapses on its own (ISO 8601 UTC). Expiry is the backstop, not the lever: a user revoking a grant ends it on the app’s very next request, long before this." + "description": "When this credential lapses on its own (ISO 8601 UTC). A revoked grant ends sooner, on the app’s next request." } }, "required": [ @@ -4751,579 +6075,519 @@ "expiresAt" ], "additionalProperties": false, - "description": "The presented credential, as the DFOS issuance ledger records it" + "description": "The presented credential.", + "examples": [ + { + "subjectDid": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "clientDid": "did:dfos:7ken2dr4ahc9vtfz3n8ce6ad2kr7h9t", + "scopes": [ + "read:profile", + "read:email", + "read:memberships", + "read:posts", + "write:posts" + ], + "attenuation": [ + { + "resource": "api:api.dfos.com", + "action": "read:profile,read:email,read:memberships" + }, + { + "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k", + "action": "read:posts,write:posts" + } + ], + "tier": "jit", + "domain": "fieldnotes.example", + "issuedAt": "2026-09-01T16:20:00.000Z", + "expiresAt": "2026-11-30T16:20:00.000Z" + } + ] + }, + "ErrorEnvelope": { + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "A stable machine-readable error code, e.g. `E_NOT_FOUND`. This and `status` are what a client branches on." + }, + "status": { + "type": "integer", + "description": "The HTTP status, repeated in the body." + }, + "message": { + "type": "string", + "description": "A human-readable explanation, safe to log and to show a developer. NOT part of the contract — the wording of any given refusal can change, so never branch on it." + }, + "data": { + "description": "Machine-readable detail; shape depends on `code`" + } + }, + "description": "The one error envelope every non-2xx response carries. Additional members are permitted and must be tolerated: the wire also carries a transport marker `defined`, which is an oRPC client concern rather than part of this contract." } - } - }, - "externalDocs": { - "url": "https://docs.dfos.com/api", - "description": "DFOS API guides, quickstart, and content-visibility model" - }, - "servers": [ - { - "url": "https://api.dfos.com/v1", - "description": "The DFOS API HTTPS endpoint" - } - ], - "tags": [ - { - "name": "Spaces", - "description": "Space profiles, site config, and discovery" - }, - { - "name": "Posts", - "description": "Space feeds and individual public posts" - }, - { - "name": "Comments", - "description": "The comment thread on a post, as the granting user sees it — member-only, no anonymous projection" - }, - { - "name": "Feed", - "description": "The granting user’s cross-space post feed — the one route with no space in its path" - }, - { - "name": "Pages", - "description": "Space public pages (about, docs, and other durable documents)" - }, - { - "name": "Products", - "description": "Store products with a public purchase page" }, - { - "name": "Releases", - "description": "Releases with a public page — the work a space put out, and the products, images, credits and dates that belong to it" - }, - { - "name": "Topics", - "description": "World-readable topics within a space" - }, - { - "name": "Events", - "description": "Public calendar events, per space and across all public spaces" - }, - { - "name": "Users", - "description": "Public user profiles" - }, - { - "name": "Profile", - "description": "The granting user’s own profile — the credential-gated tier (API-AUTH request proof)" - }, - { - "name": "Memberships", - "description": "The granting user’s spaces and groups — credential-gated (API-AUTH request proof)" - }, - { - "name": "Credential", - "description": "Introspection of the presented credential — credential-gated, but needs no particular scope" - }, - { - "name": "Key proof", - "description": "The CLI-facing half of the key-add ceremony — present a signed envelope proving possession of a signing key, then poll while the identity’s owner adopts or rejects it. Anonymous: the envelope is the credential. Presenting commits nothing to the chain. Discovery is at `/.well-known/dfos-key-proof?code=…` on this host, outside the `/v1` prefix" - }, - { - "name": "Protocol", - "description": "Protocol-layer discovery (relay endpoint)" - } - ], - "openapi": "3.1.1", - "paths": { - "/spaces/{space}": { - "get": { - "operationId": "spaces.getSpace", - "summary": "Get a public space", - "description": "Fetch a space public profile + site config by its subdomain, entity id, or protocol DID. Returns 404 if the space does not exist or has no public profile (no existence leak — private and missing are indistinguishable).", - "tags": [ - "Spaces" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, + "responses": { + "NotFound": { + "description": "Not found. A missing space and a non-public one return the same 404, so the two are indistinguishable.", + "content": { + "application/json": { "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string" + } + } + }, + "example": { + "code": "E_NOT_FOUND", + "status": 404, + "message": "Cannot resolve space -- not found" } } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SpaceOutput" + } + }, + "MembershipNotFound": { + "description": "Not found. \"No such space or group\" and \"the granting user is not in it\" are the same answer — this credential discloses the user's own memberships and nothing else.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string" } } + }, + "example": { + "code": "E_NOT_FOUND", + "status": 404, + "message": "Not found" } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + } + } + }, + "BadRequest": { + "description": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve. Request bodies are closed — an unknown member is refused, not ignored.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_INVALID_REQUEST" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string" } } + }, + "example": { + "code": "E_INVALID_REQUEST", + "status": 400, + "message": "Nothing to edit" } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + } + } + }, + "KeyProofRejected": { + "description": "The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer \"this ceremony is not open\"; only a bad signature consumes the ceremony, so any other refusal leaves the code live to re-resolve and retry.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_INVALID_REQUEST" + }, + "status": { + "const": 400 + }, + "message": { + "type": "string" } } + }, + "example": { + "code": "E_INVALID_REQUEST", + "status": 400, + "message": "this ceremony is not open" } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + } + } + }, + "ProofRequired": { + "description": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body; `WWW-Authenticate` is best-effort — infrastructure between your client and this API can rename or drop it — so branch on the status and the error body.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_AUTHENTICATION_FAILED" + }, + "status": { + "const": 401 + }, + "message": { + "type": "string" } } + }, + "example": { + "code": "E_AUTHENTICATION_FAILED", + "status": 401, + "message": "a DFOS request proof is required for this endpoint" } } } - } - }, - "/spaces": { - "get": { - "operationId": "spaces.listSpaces", - "summary": "List public spaces", - "description": "List public, discoverable spaces, cursor-paginated. Lean discovery-card projection per item. Optionally filter by `joinMode`, recency (`activeWithinDays`), `category`, or whether the space explicitly opted in to the directory (`optedIn`). Every filter is a request parameter, never encoded in a cursor. Ordered by recent activity.", - "tags": [ - "Spaces" - ], - "parameters": [ - { - "name": "joinMode", - "in": "query", + }, + "Forbidden": { + "description": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host.", + "content": { + "application/json": { "schema": { - "$ref": "#/components/schemas/PublicSpaceJoinMode", - "description": "Filter the listing to spaces with this join mode. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order." + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_UNAUTHORIZED" + }, + "status": { + "const": 403 + }, + "message": { + "type": "string" + } + } }, - "style": "deepObject", - "explode": true, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "activeWithinDays", - "in": "query", + "example": { + "code": "E_UNAUTHORIZED", + "status": 403, + "message": "credential has been revoked" + } + } + } + }, + "Replayed": { + "description": "This `jti` was already accepted, so the first attempt may have succeeded. Re-read state before retrying, and retry with a new `jti`.", + "content": { + "application/json": { "schema": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991, - "description": "Filter the listing to spaces active within this many days. CLAMPED to [1, 365] rather than rejected, so an out-of-range value is never an error; a space with no recorded activity is excluded. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - 7 - ] + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_CONFLICT" + }, + "status": { + "const": 409 + }, + "message": { + "type": "string" + } + } }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "category", - "in": "query", + "example": { + "code": "E_CONFLICT", + "status": 409, + "message": "request already seen" + } + } + } + }, + "PayloadTooLarge": { + "description": "The request body exceeds the maximum size this endpoint will authenticate.", + "content": { + "application/json": { "schema": { - "type": "string", - "maxLength": 64, - "description": "Filter the listing to spaces filed under this discovery category. Normalized (trimmed, whitespace-collapsed, lowercased) before comparison, so \"Music \" matches a space stored as \"music\"; a value that normalizes to empty filters nothing. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "music" - ] + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_INVALID_REQUEST" + }, + "status": { + "const": 413 + }, + "message": { + "type": "string" + } + } }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "optedIn", - "in": "query", + "example": { + "code": "E_INVALID_REQUEST", + "status": 413, + "message": "Request body exceeds the maximum size this endpoint will authenticate." + } + } + } + }, + "UnsupportedMediaType": { + "description": "A request body must be `application/json` (optionally `; charset=utf-8`) and must not carry a `Content-Encoding` other than `identity`.", + "content": { + "application/json": { "schema": { - "type": "boolean", - "description": "Filter on the space's explicit opt-in to the directory: true returns only opted-in spaces, false only those that have not. Omit for both. Opting in is CURATION, not privacy — a public space that never opted in is still listed unless you ask for the opted-in set. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order." + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_INVALID_REQUEST" + }, + "status": { + "const": 415 + }, + "message": { + "type": "string" + } + } }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "limit", - "in": "query", + "example": { + "code": "E_INVALID_REQUEST", + "status": 415, + "message": "Request bodies on this API must be `application/json` (optionally `; charset=utf-8`). Exactly one media type is served so that the octets a proof binds have exactly one parse." + } + } + } + }, + "RateLimited": { + "description": "Rate limit exceeded — retry after `retryAfterMs`.", + "content": { + "application/json": { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] + "type": "object", + "required": [ + "code", + "status", + "message", + "data" + ], + "properties": { + "code": { + "const": "E_RATE_LIMITED" + }, + "status": { + "const": 429 + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "scope": { + "type": "string", + "description": "Which per-IP budget was exhausted" + }, + "retryAfterMs": { + "type": "number", + "description": "Milliseconds to wait before retrying" + } + }, + "required": [ + "scope", + "retryAfterMs" + ] + } + } }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "after", - "in": "query", + "example": { + "code": "E_RATE_LIMITED", + "status": 429, + "message": "Rate limit exceeded", + "data": { + "scope": "global", + "retryAfterMs": 1200 + } + } + } + } + }, + "ServiceUnavailable": { + "description": "Service temporarily unavailable — this request could not be checked, so it was refused rather than allowed. Retry shortly.", + "content": { + "application/json": { "schema": { - "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] + "type": "object", + "required": [ + "code", + "status", + "message" + ], + "properties": { + "code": { + "const": "E_SERVICE_UNAVAILABLE" + }, + "status": { + "const": 503 + }, + "message": { + "type": "string" + } + } }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "before", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SpaceDiscoveryPageOutput" - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } + "example": { + "code": "E_SERVICE_UNAVAILABLE", + "status": 503, + "message": "Rate limit service temporarily unavailable" } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } + } + } + }, + "Error": { + "description": "Any other error the API itself produces. Branch on `code` and `status`; the enumerated statuses on each operation are what this contract promises. An intermediary may answer with a non-JSON 5xx body — check `Content-Type` before parsing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" } } } } + } + }, + "externalDocs": { + "url": "https://docs.dfos.com/api", + "description": "DFOS API guides and reference" + }, + "servers": [ + { + "url": "https://api.dfos.com/v1", + "description": "The DFOS API HTTPS endpoint" + } + ], + "tags": [ + { + "name": "Spaces", + "description": "Space profiles, site config, and discovery" }, - "/spaces/{space}/posts": { + { + "name": "Posts", + "description": "Space feeds and individual public posts" + }, + { + "name": "Comments", + "description": "A post’s comment thread, as the granting user sees it" + }, + { + "name": "Feed", + "description": "The granting user’s cross-space post feed" + }, + { + "name": "Pages", + "description": "Space public pages — about, docs, and other durable documents" + }, + { + "name": "Products", + "description": "Store products with a public purchase page" + }, + { + "name": "Releases", + "description": "Releases with a public page — products, credits, images, dates" + }, + { + "name": "Topics", + "description": "World-readable topics within a space" + }, + { + "name": "Events", + "description": "Public calendar events, per space and across all public spaces" + }, + { + "name": "Users", + "description": "Public user profiles" + }, + { + "name": "Profile", + "description": "The granting user’s own profile" + }, + { + "name": "Memberships", + "description": "The granting user’s own spaces and groups" + }, + { + "name": "Credential", + "description": "Introspection of the presented credential" + }, + { + "name": "Key proof", + "description": "The CLI-facing half of the key-add ceremony" + }, + { + "name": "Protocol", + "description": "Protocol-layer discovery (relay endpoint)" + } + ], + "openapi": "3.1.1", + "paths": { + "/spaces/{space}": { "get": { - "operationId": "posts.listPosts", - "summary": "List a space public feed", - "description": "List the posts in a space, newest first by default, cursor-paginated. Optionally filter by format, topic, or publication window, and select oldest-first ordering.\n\n**Anonymously**, only posts explicitly published to the public surface are returned (a public per-post view-access override, or a post in a topic marked world-readable). **With a credential covering this space and `read:posts`** (or a bare identity proof), the feed is the one the granting user sees: every post their membership reaches, with the full body and attachments where they genuinely read it, and a `viewer` block per post. A valid credential that does NOT cover this space returns exactly the anonymous feed — a grant only ever adds. Returns 404 if the space has no public profile, for every caller alike.", + "operationId": "spaces.getSpace", + "summary": "Get a public space", + "description": "A space's public profile and site configuration, by subdomain, entity id, or protocol DID. Missing and non-public spaces give the same 404.", "tags": [ - "Posts" + "Spaces" ], "parameters": [ { @@ -5332,7224 +6596,101 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpaceOutput" + } + } + } }, - { - "name": "format", - "in": "query", - "schema": { - "$ref": "#/components/schemas/PublicPostFormat", - "description": "Filter to this post format. Open enum — only send values supported by the current contract. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "long-post" - ] - }, - "style": "deepObject", - "explode": true, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "topic", - "in": "query", - "schema": { - "type": "string", - "description": "Filter to a topic ID — a world-readable one anonymously, or any topic the granting user reaches on a credentialed read. The filter MATCHES within whatever the caller can already see: an unknown, unreachable, or foreign topic produces an EMPTY PAGE, never an error, so it can never be used to probe which topics exist. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "topic_6encc4akrze2ah9kntzd9t" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "sort", - "in": "query", - "schema": { - "enum": [ - "newest", - "oldest" - ], - "type": "string", - "description": "Sort by publication time: `newest` (default) or `oldest`. Open enum — only send values supported by the current contract. `after` always means the next page in this order. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "newest" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "publishedAfter", - "in": "query", - "schema": { - "type": "string", - "format": "date-time", - "description": "Return posts published strictly after this ISO 8601 timestamp. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "2026-07-01T00:00:00.000Z" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "publishedBefore", - "in": "query", - "schema": { - "type": "string", - "format": "date-time", - "description": "Return posts published strictly before this ISO 8601 timestamp. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "2026-08-01T00:00:00.000Z" - ] - }, - "allowEmptyValue": true, - "allowReserved": true + "404": { + "$ref": "#/components/responses/NotFound" }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "after", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "before", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicPostPageOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - {}, - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "read:posts" - ] - }, - "post": { - "operationId": "posts.createPost", - "summary": "Write a post", - "description": "Write a post as the granting user, into a topic they can post in.\n\nText only. `title` decides the format — with one the post is a `long-post`, without one a `short-post` — and there is deliberately no way to attach media, set a cover, announce or broadcast the post, backdate it, override its view access, or fulfil an event: those fields do not exist on this input. Body length limits, the topic's own format policy, and the `post:create` permission are all enforced by the same engine the DFOS app writes through.\n\nRequires `write:posts` covering this space under the delegated profile, or a bare identity proof. Returns 404 when the space has no public profile or the topic is one the user cannot reach — the same 404 for every caller, so the route never reports which topics exist.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "topic": { - "type": "string", - "description": "Topic ID to post into, from `GET /spaces/{space}/topics`. Required — this API never picks a topic for you, because which room a post lands in is a decision the writer makes.", - "examples": [ - "topic_6encc4akrze2ah9kntzd9t" - ] - }, - "title": { - "type": "string", - "description": "Post title. Its presence is what makes this a `long-post`; omit it for a `short-post`.", - "examples": [ - "Building a more generous internet" - ] - }, - "body": { - "type": "string", - "description": "Post body (markdown)" - } - }, - "required": [ - "topic", - "body" - ], - "additionalProperties": false - } - } - } - }, - "responses": { - "201": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicPostOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:posts" - ] - } - }, - "/spaces/{space}/posts/{postId}": { - "get": { - "operationId": "posts.getPost", - "summary": "Get a post", - "description": "Fetch a single post in a space by ID. Returns a discriminated union on `state`: `eligible` (with the post content) when the caller can read it, or `gated` (with a slim space CTA, no content) when the post exists in this public space but the caller cannot.\n\n**With a credential covering this space and `read:posts`** (or a bare identity proof), \"can read\" means what the granting user can read, so a members-only post comes back `eligible` with the full body, attachments, and a `viewer` block. A valid credential that does NOT cover this space returns exactly the anonymous response. Returns 404 if the space has no public profile, the post does not exist, or the post is reached through the wrong space — the same 404, for every caller alike.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicGetPostOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - {}, - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "read:posts" - ] - }, - "patch": { - "operationId": "posts.editPost", - "summary": "Edit a post", - "description": "Edit one of the granting user's OWN posts. Send at least one of `title`, `body`, or `topic`; anything you omit is left alone.\n\nOWN CONTENT ONLY, and that is narrower than the app: a space admin editing somebody else's post is a moderation act, and this tier does not reach it. Editing a post the user did not write is a 403 even when their role in the space would allow it in the DFOS app.\n\nRequires `write:posts` covering this space under the delegated profile, or a bare identity proof. A post that does not exist, sits in another space, or lives in a topic the user cannot reach is the same 404.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Replacement title" - }, - "body": { - "type": "string", - "description": "Replacement body (markdown)" - }, - "topic": { - "type": "string", - "description": "Move the post to this topic ID, within the same space.", - "examples": [ - "topic_6encc4akrze2ah9kntzd9t" - ] - } - }, - "additionalProperties": false - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicPostOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:posts" - ] - }, - "delete": { - "operationId": "posts.deletePost", - "summary": "Delete a post", - "description": "Delete one of the granting user's OWN posts.\n\nOWN CONTENT ONLY — deleting another member's post is moderation, and this tier does not reach it even for an admin. Requires `write:posts` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicDeletedOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:posts" - ] - } - }, - "/spaces/{space}/posts/{postId}/upvote": { - "put": { - "operationId": "posts.upvotePost", - "summary": "Upvote a post", - "description": "Add the granting user's upvote to a post. IDEMPOTENT: upvoting a post they have already upvoted succeeds and returns the same state, so a client may safely re-send.\n\nThis writes the user's name onto a signal other members can see. Requires `write:upvotes` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicUpvoteStateOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:upvotes" - ] - }, - "delete": { - "operationId": "posts.removePostUpvote", - "summary": "Remove a post upvote", - "description": "Remove the granting user's upvote from a post. IDEMPOTENT: removing an upvote that is not there succeeds and returns the same state.\n\nRequires `write:upvotes` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Posts" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicUpvoteStateOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:upvotes" - ] - } - }, - "/spaces/{space}/posts/{postId}/comments": { - "get": { - "operationId": "comments.listPostComments", - "summary": "List a post’s comments", - "description": "The comment thread on a post, as the granting user sees it — cursor-paginated, flat, root comments by default.\n\nRequires `read:posts` covering this space under the delegated profile, or a bare identity proof. There is NO anonymous projection: comment visibility inherits the post's everywhere in DFOS, and the conversation under a post is space interior rather than something the space published. Every refusal that is not an authentication failure collapses into the SAME 404 as an unknown post — a grant that does not reach this space, a post in another space, a post the user cannot read, and a post that does not exist are one answer.\n\nPass `parentCommentId` to walk one comment's replies instead of the roots.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Post ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - }, - { - "name": "parentCommentId", - "in": "query", - "schema": { - "type": "string", - "description": "List the REPLIES to this comment instead of the post's root comments. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "sort", - "in": "query", - "schema": { - "$ref": "#/components/schemas/PublicCommentSort", - "description": "Thread ordering — see `PublicCommentSort`. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "newest" - ] - }, - "style": "deepObject", - "explode": true, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "after", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "before", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicCommentPageOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "read:posts" - ] - }, - "post": { - "operationId": "comments.createComment", - "summary": "Write a comment", - "description": "Write a comment on a post, as the granting user. Pass `parentCommentId` to reply to an existing comment instead of commenting on the post itself — threads are one level deep, so a reply to a reply attaches to the same root.\n\nText only: there is no way to attach media to a comment on this API. The parent post's visibility gate and the topic's `comment:create` permission are enforced by the same engine the DFOS app writes through, so a post the user cannot read is the same 404 the read routes give.\n\nRequires `write:comments` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "postId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The ROOT post to comment on.", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "Comment body (markdown)" - }, - "parentCommentId": { - "type": "string", - "description": "Reply to this comment rather than to the post. Must be a comment on the same root post.", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - }, - "required": [ - "body" - ], - "additionalProperties": false - } - } - } - }, - "responses": { - "201": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicCommentWriteOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:comments" - ] - } - }, - "/spaces/{space}/comments/{commentId}": { - "patch": { - "operationId": "comments.editComment", - "summary": "Edit a comment", - "description": "Edit one of the granting user's OWN comments.\n\nOWN CONTENT ONLY, and narrower than the app: editing another member's comment is not something this tier can do for anybody, whatever their role in the space.\n\nRequires `write:comments` covering this space under the delegated profile, or a bare identity proof. A comment that does not exist, sits in another space, or hangs off a post the user cannot read is the same 404.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "commentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Comment ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "Replacement body (markdown)" - } - }, - "required": [ - "body" - ], - "additionalProperties": false - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicCommentWriteOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:comments" - ] - }, - "delete": { - "operationId": "comments.deleteComment", - "summary": "Delete a comment", - "description": "Delete one of the granting user's OWN comments. Replies to it are not deleted with it.\n\nOWN CONTENT ONLY — deleting another member's comment is moderation, which this tier does not reach. Requires `write:comments` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "commentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Comment ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicDeletedOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:comments" - ] - } - }, - "/spaces/{space}/comments/{commentId}/upvote": { - "put": { - "operationId": "comments.upvoteComment", - "summary": "Upvote a comment", - "description": "Add the granting user's upvote to a comment. IDEMPOTENT: upvoting one they have already upvoted succeeds and returns the same state.\n\nRequires `write:upvotes` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "commentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Comment ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicUpvoteStateOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:upvotes" - ] - }, - "delete": { - "operationId": "comments.removeCommentUpvote", - "summary": "Remove a comment upvote", - "description": "Remove the granting user's upvote from a comment. IDEMPOTENT: removing one that is not there succeeds and returns the same state.\n\nRequires `write:upvotes` covering this space under the delegated profile, or a bare identity proof.\n\nEvery write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry.", - "tags": [ - "Comments" - ], - "parameters": [ - { - "name": "space", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", - "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - } - }, - { - "name": "commentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "Comment ID", - "examples": [ - "post_6encc4akrze2ah9kntzd9t" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicUpvoteStateOutput" - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_CONFLICT" - }, - "status": { - "const": 409 - }, - "message": { - "type": "string", - "default": "This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "415": { - "description": "415", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 415 - }, - "message": { - "type": "string", - "default": "Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "429": { + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] - }, - { - "dfosRequestProof": [], - "dfosCredential": [] - } - ], - "x-dfos-actions": [ - "write:upvotes" - ] + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } } }, - "/feed": { + "/spaces": { "get": { - "operationId": "feed.listFeed", - "summary": "The cross-space feed", - "description": "The granting user's own post feed across the spaces this grant reaches — newest first, cursor-paginated, one flat stream. Each item carries the space it came from.\n\nRequires `read:posts` under the delegated profile, or a bare identity proof for a caller reading their own feed. The reach is the grant's: a credential covering all the user's spaces feeds every space they are an active member of, including ones joined later; a credential naming particular spaces feeds exactly those. A space the user has left contributes nothing and is not reported — losing a space looks like silence, not an error. A caller in no reachable space gets an empty page.\n\nPinned posts are NOT hoisted: a pin is a fact about one space's own feed, and hoisting every space's pins onto page one would bury the chronology this route exists for.", + "operationId": "spaces.listSpaces", + "summary": "List public spaces", + "description": "Public, discoverable spaces as lean cards, ordered by recent activity. Filter by `joinMode`, `activeWithinDays`, `category`, or `optedIn`.", "tags": [ - "Feed" + "Spaces" ], "parameters": [ + { + "name": "joinMode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/PublicSpaceJoinMode", + "description": "Only spaces with this join mode. Hold it constant while walking cursors." + }, + "style": "deepObject", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "activeWithinDays", + "in": "query", + "schema": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Only spaces active within this many days. Clamped to [1, 365] rather than rejected; spaces with no recorded activity are excluded. Hold it constant while walking cursors.", + "examples": [ + 7 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "category", + "in": "query", + "schema": { + "type": "string", + "maxLength": 64, + "description": "Only spaces filed under this discovery category. Normalized (trimmed, whitespace-collapsed, lowercased) before comparison. Hold it constant while walking cursors.", + "examples": [ + "music" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "optedIn", + "in": "query", + "schema": { + "type": "boolean", + "description": "Filter on the space's directory opt-in: true for opted-in spaces, false for the rest, omit for both. Opting in is curation, not privacy. Hold it constant while walking cursors." + }, + "allowEmptyValue": true, + "allowReserved": true + }, { "name": "limit", "in": "query", @@ -12557,7 +6698,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -12570,9 +6711,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -12583,9 +6724,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -12598,170 +6739,30 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FeedPageOutput" + "$ref": "#/components/schemas/SpaceDiscoveryPageOutput" } } } }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - }, - "security": [ - { - "dfosIdentityProof": [] + "$ref": "#/components/responses/ServiceUnavailable" }, - { - "dfosRequestProof": [], - "dfosCredential": [] + "default": { + "$ref": "#/components/responses/Error" } - ], - "x-dfos-actions": [ - "read:posts" - ] + } } }, - "/spaces/{space}/pages": { + "/spaces/{space}/posts": { "get": { - "operationId": "pages.listPages", - "summary": "List a space public pages", - "description": "List a space's published pages, ordered by sort order then ID and cursor-paginated. Returns 404 if the space has no public profile. Pages are always fully public; only enabled pages appear.", + "operationId": "posts.listPosts", + "summary": "List a space public feed", + "description": "List a space's posts, newest first. Filter by format, topic, or publication window, or sort oldest-first.\n\nWithout a proof the feed is the space's public posts. A grant covering this space and `read:posts` returns the feed the granting user sees; an uncovered grant returns the anonymous feed.", "tags": [ - "Pages" + "Posts" ], "parameters": [ { @@ -12770,302 +6771,127 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "limit", + "name": "format", "in": "query", "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "$ref": "#/components/schemas/PublicPostFormat", + "description": "Filter to this post format. Hold it constant while walking cursors.", "examples": [ - 20 + "long-post" ] }, + "style": "deepObject", + "explode": true, "allowEmptyValue": true, "allowReserved": true }, { - "name": "after", + "name": "topic", "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Filter to a topic ID. An unknown or unreachable topic returns an empty page, never an error. Hold it constant while walking cursors.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "topic_6c2efd472dvt8rf9k4ftcc" ] }, "allowEmptyValue": true, "allowReserved": true }, { - "name": "before", + "name": "sort", "in": "query", "schema": { + "enum": [ + "newest", + "oldest" + ], "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Sort by publication time: `newest` (default) or `oldest`. `after` is always the next page in this order. Hold it constant while walking cursors.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "newest" ] }, "allowEmptyValue": true, "allowReserved": true - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicPagePageOutput" - } - } - } }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + { + "name": "publishedAfter", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Return posts published strictly after this ISO 8601 timestamp. Hold it constant while walking cursors.", + "examples": [ + "2026-07-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + { + "name": "publishedBefore", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Return posts published strictly before this ISO 8601 timestamp. Hold it constant while walking cursors.", + "examples": [ + "2026-08-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - } - } - } - }, - "/spaces/{space}/pages/{page}": { - "get": { - "operationId": "pages.getPage", - "summary": "Get a public page", - "description": "Fetch a single published page in a space by stable page ID or slug, with its full markdown body and resolved inline media. Returns 404 if the space has no public profile, the page does not exist, belongs to another space, or is not enabled — all byte-identical (no existence leak).", - "tags": [ - "Pages" - ], - "parameters": [ { - "name": "space", - "in": "path", - "required": true, + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] - } + }, + "allowEmptyValue": true, + "allowReserved": true }, { - "name": "page", - "in": "path", - "required": true, + "name": "before", + "in": "query", "schema": { "type": "string", - "description": "A page identifier: its stable entity id (`page_...`) or its mutable slug alias. Store the id; slugs may change.", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "page_6encc4akrze2ah9kntzd9t", - "about" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] - } + }, + "allowEmptyValue": true, + "allowReserved": true } ], "responses": { @@ -13074,218 +6900,195 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicPageOutput" + "$ref": "#/components/schemas/PublicPostPageOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + {}, + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (anonymous)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts'" + }, + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + }, + "post": { + "operationId": "posts.createPost", + "summary": "Write a post", + "description": "Write a post as the granting user, into a topic they can post in. Text only: `title` makes it a `long-post`. Media, covers, announce, backdating, and view-access overrides are not on this input. An unreachable topic is a 404.", + "tags": [ + "Posts" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "Topic ID to post into. `GET /spaces/{space}/topics` enumerates world-readable topics only, so it supplies ids for public spaces; for a member or private topic, take the id from `post.topics[]` on a credentialed read.", + "examples": [ + "topic_6c2efd472dvt8rf9k4ftcc" + ] + }, + "title": { + "type": "string", + "description": "Post title. Its presence makes the post a `long-post`.", + "examples": [ + "DFOS beyond DFOS: sign-in, domains, and your own keys" + ] + }, + "body": { + "type": "string", + "description": "Post body (markdown)" + } + }, + "required": [ + "topic", + "body" + ], + "additionalProperties": false, + "examples": [ + { + "topic": "topic_6c2efd472dvt8rf9k4ftcc", + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + "body": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\n\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives." + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicPostOutput" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X POST 'https://api.dfos.com/v1/spaces/home/posts' \\\n -H 'Authorization: DFOS ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"topic\": \"topic_6c2efd472dvt8rf9k4ftcc\",\n \"title\": \"DFOS beyond DFOS: sign-in, domains, and your own keys\",\n \"body\": \"Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\\n\\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.\"\n}'" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X POST 'https://api.dfos.com/v1/spaces/home/posts' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"topic\": \"topic_6c2efd472dvt8rf9k4ftcc\",\n \"title\": \"DFOS beyond DFOS: sign-in, domains, and your own keys\",\n \"body\": \"Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\\n\\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.\"\n}'" + } + ] } }, - "/spaces/{space}/events": { + "/spaces/{space}/posts/{postId}": { "get": { - "operationId": "events.listSpaceEvents", - "summary": "List a space public events", - "description": "List a space's PUBLIC events within a time window, earliest first, cursor-paginated. Only events the space marked public and published appear — member-only, group-only, and admin-only events are absent, and so are drafts and cancelled events (no existence leak: an absent event is indistinguishable from one that was never created). Returns 404 if the space has no public profile. Recurring events are expanded into individual occurrences within the window; a cursor walk reaches at most the first 500 occurrences of that window (narrow the window to see past them).", + "operationId": "posts.getPost", + "summary": "Get a post", + "description": "Fetch a post by ID. Returns a union on `state`: `eligible` with the post, or `gated` with a space CTA when the caller cannot read it.\n\nA missing post, a private space, and a post reached through the wrong space are the same 404.", "tags": [ - "Events" + "Posts" ], "parameters": [ { @@ -13294,302 +7097,327 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "from", - "in": "query", + "name": "postId", + "in": "path", + "required": true, "schema": { "type": "string", - "format": "date-time", - "description": "Window start (ISO 8601). Defaults to now. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", + "description": "Post ID", "examples": [ - "2026-08-01T00:00:00.000Z" + "post_ze2kh2d47tzerkhet8348c" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicGetPostOutput" + } + } + } + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + {}, { - "name": "to", - "in": "query", - "schema": { - "type": "string", - "format": "date-time", - "description": "Window end (ISO 8601). Defaults to 90 days after `from`. Windows longer than ~1 year, or ending before they start, are clamped rather than rejected. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", - "examples": [ - "2026-09-01T00:00:00.000Z" - ] - }, - "allowEmptyValue": true, - "allowReserved": true + "dfosIdentityProof": [] }, { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] - }, - "allowEmptyValue": true, - "allowReserved": true + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (anonymous)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c'" }, { - "name": "after", - "in": "query", + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + }, + "patch": { + "operationId": "posts.editPost", + "summary": "Edit a post", + "description": "Edit one of the granting user's own posts. Send at least one of `title`, `body`, or `topic`; omitted fields are left alone. Own content only: editing another member's post is a 403 even for a space admin.", + "tags": [ + "Posts" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } }, { - "name": "before", - "in": "query", + "name": "postId", + "in": "path", + "required": true, "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Post ID", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "post_ze2kh2d47tzerkhet8348c" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Replacement title" + }, + "body": { + "type": "string", + "description": "Replacement body (markdown)" + }, + "topic": { + "type": "string", + "description": "Move the post to this topic ID, within the same space.", + "examples": [ + "topic_6c2efd472dvt8rf9k4ftcc" + ] + } + }, + "additionalProperties": false, + "examples": [ + { + "title": "DFOS beyond DFOS: sign-in, domains, and your own keys" + } + ] + } + } + } + }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicEventPageOutput" + "$ref": "#/components/schemas/PublicPostOutput" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X PATCH 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"title\": \"DFOS beyond DFOS: sign-in, domains, and your own keys\"\n}'" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X PATCH 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"title\": \"DFOS beyond DFOS: sign-in, domains, and your own keys\"\n}'" + } + ] + }, + "delete": { + "operationId": "posts.deletePost", + "summary": "Delete a post", + "description": "Delete one of the granting user's own posts. Own content only: another member's post is a 403 even for a space admin.", + "tags": [ + "Posts" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] } }, - "429": { - "description": "429", + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Post ID", + "examples": [ + "post_ze2kh2d47tzerkhet8348c" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicDeletedOutput" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] } }, - "/spaces/{space}/events/{eventId}": { - "get": { - "operationId": "events.getSpaceEvent", - "summary": "Get a public event", - "description": "Fetch ONE occurrence of a space's public event by its event id — the direct read behind a per-event page, and the same item shape the listings return. Defaults to the event's SOONEST UPCOMING public occurrence within the ~1-year look-ahead this API serves; pass `occ` to address a specific occurrence, including one beyond that horizon. Returns 404 if the space has no public profile, the event does not exist, belongs to another space, is a draft, is cancelled, is not public, or has no upcoming public occurrence at all — every case byte-identical (no existence leak). Resolution runs the SAME expansion as `GET /spaces/{space}/events`, so `recurrenceId` here is byte-identical to the one the listing emits for the same occurrence, and any pair you take off a listing addresses the same occurrence when you hand it back.", + "/spaces/{space}/posts/{postId}/upvote": { + "put": { + "operationId": "posts.upvotePost", + "summary": "Upvote a post", + "description": "Add the granting user's upvote to a post. Idempotent.", "tags": [ - "Events" + "Posts" ], "parameters": [ { @@ -13598,38 +7426,25 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "eventId", + "name": "postId", "in": "path", "required": true, "schema": { "type": "string", - "description": "An event identifier: the stable entity id (`evt_...`) from the events listings — the `id`/`seriesId` field, which addresses the SERIES, not one occurrence.", + "description": "Post ID", "examples": [ - "evt_6encc4akrze2ah9kntzd9t" + "post_ze2kh2d47tzerkhet8348c" ] } - }, - { - "name": "occ", - "in": "query", - "schema": { - "type": "string", - "description": "Pin the response to ONE occurrence of a recurring event: pass a `recurrenceId` exactly as a listing emitted it. Any value that does not name a live occurrence of THIS event — omitted, empty, malformed, a date that has since passed, a recurrence rule that changed, or a recurrenceId belonging to another event — falls back to the series' soonest upcoming occurrence, so a shared link degrades to 'the next one' rather than dead-ending. Check the `recurrenceId` you get back when you need to know which happened.", - "examples": [ - "2026-08-15T19:00:00" - ] - }, - "allowEmptyValue": true, - "allowReserved": true } ], "responses": { @@ -13638,245 +7453,230 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicEventOutput" + "$ref": "#/components/schemas/PublicUpvoteStateOutput" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:upvotes" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X PUT 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/upvote' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X PUT 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/upvote' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + }, + "delete": { + "operationId": "posts.removePostUpvote", + "summary": "Remove a post upvote", + "description": "Remove the granting user's upvote from a post. Idempotent.", + "tags": [ + "Posts" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] } }, - "503": { - "description": "503", + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Post ID", + "examples": [ + "post_ze2kh2d47tzerkhet8348c" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicUpvoteStateOutput" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:upvotes" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/upvote' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/upvote' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] } }, - "/events/upcoming": { + "/spaces/{space}/posts/{postId}/comments": { "get": { - "operationId": "events.listUpcomingEvents", - "summary": "List upcoming public events", - "description": "List upcoming PUBLIC events across every publicly-discoverable space, earliest first, cursor-paginated. Each item carries the space it belongs to. Same content rule as the per-space listing — public, published events only — with the space gate matching public space discovery (`GET /spaces`) rather than direct space lookup. Recurring events are expanded into individual occurrences within the window; a cursor walk reaches at most the first 500 occurrences of that window (narrow the window to see past them).", + "operationId": "comments.listPostComments", + "summary": "List a post’s comments", + "description": "The comment thread on a post, cursor-paginated and flat, root comments by default. Pass `parentCommentId` to walk one comment's replies instead. There is no anonymous projection of a comment thread: an uncovered grant, a post in another space, a post the user cannot read, and a post that does not exist are all the same 404.", "tags": [ - "Events" + "Comments" ], "parameters": [ { - "name": "from", + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Post ID", + "examples": [ + "post_ze2kh2d47tzerkhet8348c" + ] + } + }, + { + "name": "parentCommentId", "in": "query", "schema": { "type": "string", - "format": "date-time", - "description": "Window start (ISO 8601). Defaults to now. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", + "description": "List the replies to this comment instead of the post's root comments. Hold it constant while walking cursors.", "examples": [ - "2026-08-01T00:00:00.000Z" + "comment_9rze4tk2vdc7fa38nhe6c2" ] }, "allowEmptyValue": true, "allowReserved": true }, { - "name": "to", + "name": "sort", "in": "query", "schema": { - "type": "string", - "format": "date-time", - "description": "Window end (ISO 8601). Defaults to 90 days after `from`. Windows longer than ~1 year, or ending before they start, are clamped rather than rejected. Hold this value constant while walking pagination cursors; cursors do not encode filters or sort order.", + "$ref": "#/components/schemas/PublicCommentSort", + "description": "Thread ordering. Hold it constant while walking cursors.", "examples": [ - "2026-09-01T00:00:00.000Z" + "newest" ] }, + "style": "deepObject", + "explode": true, "allowEmptyValue": true, "allowReserved": true }, @@ -13887,7 +7687,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -13900,9 +7700,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -13913,9 +7713,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -13928,158 +7728,192 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicEventPageOutput" + "$ref": "#/components/schemas/PublicCommentPageOutput" } } } }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/comments' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/comments' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + }, + "post": { + "operationId": "comments.createComment", + "summary": "Write a comment", + "description": "Write a comment on a post, or pass `parentCommentId` to reply to an existing comment. Threads are one level deep: a reply to a reply attaches to the same root. Text only — a comment cannot carry media. A post the user cannot read answers the same 404 as one that does not exist.", + "tags": [ + "Comments" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The root post to comment on.", + "examples": [ + "post_ze2kh2d47tzerkhet8348c" + ] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "Comment body (markdown)" + }, + "parentCommentId": { + "type": "string", + "description": "Reply to this comment rather than to the post. Must be a comment on the same root post.", + "examples": [ + "comment_9rze4tk2vdc7fa38nhe6c2" + ] + } + }, + "required": [ + "body" + ], + "additionalProperties": false, + "examples": [ + { + "body": "Same. That and origin binding running in both directions.", + "parentCommentId": "comment_9rze4tk2vdc7fa38nhe6c2" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicCommentWriteOutput" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:comments" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X POST 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/comments' \\\n -H 'Authorization: DFOS ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"body\": \"Same. That and origin binding running in both directions.\",\n \"parentCommentId\": \"comment_9rze4tk2vdc7fa38nhe6c2\"\n}'" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X POST 'https://api.dfos.com/v1/spaces/home/posts/post_ze2kh2d47tzerkhet8348c/comments' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"body\": \"Same. That and origin binding running in both directions.\",\n \"parentCommentId\": \"comment_9rze4tk2vdc7fa38nhe6c2\"\n}'" + } + ] } }, - "/spaces/{space}/products": { - "get": { - "operationId": "products.listProducts", - "summary": "List a space public products", - "description": "List the store products a space has published public purchase pages for, in the space's curated display order (`sortOrder`, lower first), cursor-paginated. A space that has never reordered its store reads newest-first. Each item is the same object the single-product route returns. Returns 404 if the space has no public profile (byte-identical to every other not-found — no existence leak). Products the seller has not published, and archived or inactive ones, never appear and are never counted in `sortOrder`; an empty list is a valid answer and reveals nothing.", + "/spaces/{space}/comments/{commentId}": { + "patch": { + "operationId": "comments.editComment", + "summary": "Edit a comment", + "description": "Replace the body of one of the granting user's own comments. Own content only: this tier cannot edit another member's comment, whatever the caller's role in the space. A comment that does not exist, sits in another space, or hangs off a post the user cannot read is the same 404.", "tags": [ - "Products" + "Comments" ], "parameters": [ { @@ -14088,316 +7922,152 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "after", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "before", - "in": "query", + "name": "commentId", + "in": "path", + "required": true, "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Comment ID", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "comment_d6ah3f9rkt2ez48vc7n4rc" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "Replacement body (markdown)" + } + }, + "required": [ + "body" + ], + "additionalProperties": false, + "examples": [ + { + "body": "Same. That and origin binding running in both directions." + } + ] + } + } + } + }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicProductPageOutput" + "$ref": "#/components/schemas/PublicCommentWriteOutput" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } - } - }, - "/products": { - "get": { - "operationId": "products.listAllProducts", - "summary": "List public products across all spaces", - "description": "List every store product with a public purchase page, across every publicly-discoverable space, newest-created first, cursor-paginated. Each item is the same object the space-scoped routes return plus the `space` it belongs to. The space gate matches public space discovery (`GET /spaces`) rather than direct space lookup, so a space excluded from the directory is absent here even though its own product routes still serve it — and a space turning its public site off drops every one of its products from this feed by predicate. No filters beyond pagination in this version.", + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:comments" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X PATCH 'https://api.dfos.com/v1/spaces/home/comments/comment_d6ah3f9rkt2ez48vc7n4rc' \\\n -H 'Authorization: DFOS ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"body\": \"Same. That and origin binding running in both directions.\"\n}'" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X PATCH 'https://api.dfos.com/v1/spaces/home/comments/comment_d6ah3f9rkt2ez48vc7n4rc' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: ' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"body\": \"Same. That and origin binding running in both directions.\"\n}'" + } + ] + }, + "delete": { + "operationId": "comments.deleteComment", + "summary": "Delete a comment", + "description": "Delete one of the granting user's own comments. Replies to it are not deleted with it. Own content only: deleting another member's comment is moderation, which this tier does not reach.", "tags": [ - "Products" + "Comments" ], "parameters": [ { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", - "examples": [ - 20 - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "after", - "in": "query", + "name": "space", + "in": "path", + "required": true, "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } }, { - "name": "before", - "in": "query", + "name": "commentId", + "in": "path", + "required": true, "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Comment ID", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "comment_d6ah3f9rkt2ez48vc7n4rc" ] - }, - "allowEmptyValue": true, - "allowReserved": true + } } ], "responses": { @@ -14406,158 +8076,75 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicProductFeedPageOutput" + "$ref": "#/components/schemas/PublicDeletedOutput" } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:comments" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/comments/comment_d6ah3f9rkt2ez48vc7n4rc' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/comments/comment_d6ah3f9rkt2ez48vc7n4rc' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] } }, - "/spaces/{space}/products/{slug}": { - "get": { - "operationId": "products.getProduct", - "summary": "Get a public product", - "description": "Fetch a store product's public purchase page by its slug. Returns 404 if the space has no public profile, the slug matches no product, the product belongs to another space, the seller has not published a public page for it, or the product is archived or inactive — all byte-identical (no existence leak).", + "/spaces/{space}/comments/{commentId}/upvote": { + "put": { + "operationId": "comments.upvoteComment", + "summary": "Upvote a comment", + "description": "Add the granting user's upvote to a comment. Idempotent: upvoting a comment they have already upvoted returns the same state.", "tags": [ - "Products" + "Comments" ], "parameters": [ { @@ -14566,23 +8153,23 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "slug", + "name": "commentId", "in": "path", "required": true, "schema": { "type": "string", - "description": "The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "Comment ID", "examples": [ - "softr-ticket" + "comment_9rze4tk2vdc7fa38nhe6c2" ] } } @@ -14592,219 +8179,74 @@ "description": "OK", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicProductOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "schema": { + "$ref": "#/components/schemas/PublicUpvoteStateOutput" } } } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } - } - }, - "/spaces/{space}/releases": { - "get": { - "operationId": "releases.listSpaceReleases", - "summary": "List a space public releases", - "description": "List the releases a space has published public pages for, newest-published first (`publishedAt` descending), cursor-paginated. Returns 404 if the space has no public profile (byte-identical to every other not-found — no existence leak). Unpublished releases and releases with no slug never appear and are never counted; an empty list is a valid answer and reveals nothing.", + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:upvotes" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X PUT 'https://api.dfos.com/v1/spaces/home/comments/comment_9rze4tk2vdc7fa38nhe6c2/upvote' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X PUT 'https://api.dfos.com/v1/spaces/home/comments/comment_9rze4tk2vdc7fa38nhe6c2/upvote' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + }, + "delete": { + "operationId": "comments.removeCommentUpvote", + "summary": "Remove a comment upvote", + "description": "Remove the granting user's upvote from a comment. Idempotent: removing an upvote that is not there returns the same state.", "tags": [ - "Releases" + "Comments" ], "parameters": [ { @@ -14813,14 +8255,104 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Comment ID", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "comment_9rze4tk2vdc7fa38nhe6c2" ] } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUpvoteStateOutput" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Replayed" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "write:upvotes" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/comments/comment_9rze4tk2vdc7fa38nhe6c2/upvote' \\\n -H 'Authorization: DFOS '" }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl -X DELETE 'https://api.dfos.com/v1/spaces/home/comments/comment_9rze4tk2vdc7fa38nhe6c2/upvote' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + } + }, + "/feed": { + "get": { + "operationId": "feed.listFeed", + "summary": "The cross-space feed", + "description": "The granting user's own feed across the spaces this grant reaches — newest first, one flat stream. Each item carries its space.\n\nA grant covering all the user's spaces feeds every space they are an active member of, including ones joined later. A space the user has left contributes nothing. Pinned posts are not hoisted.", + "tags": [ + "Feed" + ], + "parameters": [ { "name": "limit", "in": "query", @@ -14828,7 +8360,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -14841,9 +8373,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -14854,9 +8386,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -14869,220 +8401,70 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicReleasePageOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/FeedPageOutput" } } } }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:posts" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/feed' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/feed' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] } }, - "/releases": { + "/spaces/{space}/pages": { "get": { - "operationId": "releases.listReleases", - "summary": "List public releases across all spaces", - "description": "List releases with a public page across every publicly-discoverable space, newest-published first (`publishedAt` descending), cursor-paginated. Each item is the same summary a space's own shelf returns plus the `space` it belongs to. The space gate matches public space discovery (`GET /spaces`) rather than direct space lookup, so a space excluded from the directory is absent here even though its own release routes still serve it. Unpublished releases and releases with no slug never appear; an empty page is a valid answer and reveals nothing. No filters beyond pagination in this version.", + "operationId": "pages.listPages", + "summary": "List a space public pages", + "description": "A space's published pages, ordered by `sortOrder` then id. Only enabled pages appear.", "tags": [ - "Releases" + "Pages" ], "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, { "name": "limit", "in": "query", @@ -15090,7 +8472,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -15103,9 +8485,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -15116,9 +8498,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -15131,158 +8513,33 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicReleaseFeedPageOutput" + "$ref": "#/components/schemas/PublicPagePageOutput" } } } }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/spaces/{space}/releases/{slug}": { + "/spaces/{space}/pages/{page}": { "get": { - "operationId": "releases.getSpaceRelease", - "summary": "Get a public release", - "description": "Fetch a release's public page by its slug, with its media wall, credits, composed products, and public dates. Returns 404 if the space has no public profile, the slug matches no release, the release belongs to another space, it has not been published, or it has been deleted — all byte-identical (no existence leak).", + "operationId": "pages.getPage", + "summary": "Get a public page", + "description": "One published page by id or slug, with its full body and inline media. A missing, disabled, foreign-space, or non-public page is the same 404.", "tags": [ - "Releases" + "Pages" ], "parameters": [ { @@ -15291,23 +8548,24 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "slug", + "name": "page", "in": "path", "required": true, "schema": { "type": "string", - "description": "The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`.", + "description": "A page id (`page_...`) or its slug alias.", "examples": [ - "blue-record" + "page_c7dnfhc3zn2eh7frrc3nhc", + "about" ] } } @@ -15318,218 +8576,33 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicReleaseOutput" + "$ref": "#/components/schemas/PublicPageOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/spaces/{space}/topics": { + "/spaces/{space}/events": { "get": { - "operationId": "topics.listTopics", - "summary": "List a space's public topics", - "description": "List the live, world-readable topics in a space, ordered by name then ID, cursor-paginated. Returns 404 if the space has no public profile.", + "operationId": "events.listSpaceEvents", + "summary": "List a space public events", + "description": "A space's public, published events within a time window, earliest first. Non-public, draft, and cancelled events are absent. Recurring events expand into occurrences; a cursor walk reaches the first 500 in the window — narrow the window rather than paging past them.", "tags": [ - "Topics" + "Events" ], "parameters": [ { @@ -15538,14 +8611,42 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, + { + "name": "from", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Window start (ISO 8601). Defaults to now. Hold it constant while walking cursors.", + "examples": [ + "2026-08-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "to", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Window end (ISO 8601). Defaults to 90 days after `from`. Over-long or inverted windows are clamped. Hold it constant while walking cursors.", + "examples": [ + "2026-09-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, { "name": "limit", "in": "query", @@ -15553,7 +8654,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -15566,9 +8667,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -15579,9 +8680,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -15589,223 +8690,38 @@ } ], "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublicTopicPageOutput" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicEventPageOutput" } } } }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/spaces/{space}/topics/{topic}": { + "/spaces/{space}/events/{eventId}": { "get": { - "operationId": "topics.getTopic", - "summary": "Get a public topic", - "description": "Fetch a single live, world-readable topic in a space by its stable topic ID — the direct read behind a per-topic page. Pair it with `GET /spaces/{space}/posts?topic=...` for the topic's public posts. Returns 404 if the space has no public profile, the topic does not exist, belongs to another space, is deleted, is not world-readable, or carries no public post feed — all byte-identical (no existence leak).", + "operationId": "events.getSpaceEvent", + "summary": "Get a public event", + "description": "One occurrence of a space's public event, in the shape the listings return. Defaults to the soonest upcoming occurrence; pass `occ` for a specific one, including beyond the ~1-year look-ahead. A missing, draft, cancelled, non-public, foreign-space, or fully past event is the same 404.", "tags": [ - "Topics" + "Events" ], "parameters": [ { @@ -15814,25 +8730,38 @@ "required": true, "schema": { "type": "string", - "description": "A space identifier: its subdomain (custom domain if set, else the normalized `space-{id}` form), its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The subdomain is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, { - "name": "topic", + "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", - "description": "A topic identifier: the stable entity id (`topic_...`) from the topics listing.", + "description": "An event id (`evt_...`) as the listings emit it. Addresses the series, not one occurrence.", "examples": [ - "topic_6encc4akrze2ah9kntzd9t" + "evt_ehrrkh8thkkdh6c383zkf9" ] } + }, + { + "name": "occ", + "in": "query", + "schema": { + "type": "string", + "description": "Pin the response to one occurrence: a `recurrenceId` exactly as a listing emitted it. Anything else falls back to the soonest upcoming occurrence.", + "examples": [ + "2026-09-09" + ] + }, + "allowEmptyValue": true, + "allowReserved": true } ], "responses": { @@ -15841,233 +8770,191 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicTopicOutput" + "$ref": "#/components/schemas/PublicEventOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/events/upcoming": { + "get": { + "operationId": "events.listUpcomingEvents", + "summary": "List upcoming public events", + "description": "Upcoming public events across every publicly-discoverable space, earliest first. Each item carries its space. Only spaces that appear in `GET /spaces` contribute. Recurring events expand into occurrences; a cursor walk reaches the first 500 in the window — narrow the window rather than paging past them.", + "tags": [ + "Events" + ], + "parameters": [ + { + "name": "from", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Window start (ISO 8601). Defaults to now. Hold it constant while walking cursors.", + "examples": [ + "2026-08-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "to", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "description": "Window end (ISO 8601). Defaults to 90 days after `from`. Over-long or inverted windows are clamped. Hold it constant while walking cursors.", + "examples": [ + "2026-09-01T00:00:00.000Z" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicEventPageOutput" } } } + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/users/{user}": { + "/spaces/{space}/products": { "get": { - "operationId": "users.getUser", - "summary": "Get a public user profile", - "description": "Fetch a public user profile by its handle (username), its identity entity id, or its protocol DID. Handles are mutable aliases — the id and DID are canonical. Returns 404 if the user does not exist, has opted their profile out of public visibility, or is not a user identity (no existence leak — private and missing are indistinguishable).", + "operationId": "products.listProducts", + "summary": "List a space public products", + "description": "A space's public store products, in its display order (`sortOrder`, lowest first). Unpublished, archived, and inactive products never appear. Returns 404 when the space has no public profile.", "tags": [ - "Users" + "Products" ], "parameters": [ { - "name": "user", + "name": "space", "in": "path", "required": true, "schema": { "type": "string", - "description": "A user identifier: their handle (username), their identity entity id (`identity_…`), or their protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The handle is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "bvalosek", - "identity_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true } ], "responses": { @@ -16076,218 +8963,33 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicUserOutput" + "$ref": "#/components/schemas/PublicProductPageOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/users": { + "/products": { "get": { - "operationId": "users.listUsers", - "summary": "List public user profiles", - "description": "List public member profiles, cursor-paginated. Profiles are public by default and owners can opt out — an opted-out profile does not appear here. Each row carries only the terse pinned-space cards; fetch the profile for the full membership shelf. Ordering is deterministic and cursor-stable but otherwise unspecified; do not depend on it.", + "operationId": "products.listAllProducts", + "summary": "List public products across all spaces", + "description": "Every store product with a public purchase page across every publicly-discoverable space, newest-created first. Each item adds the `space` it belongs to. A space absent from `GET /spaces` is absent here even though its own product routes still serve it.", "tags": [ - "Users" + "Products" ], "parameters": [ { @@ -16297,7 +8999,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -16310,9 +9012,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -16323,9 +9025,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -16338,171 +9040,105 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicUserPageOutput" + "$ref": "#/components/schemas/PublicProductFeedPageOutput" } } } }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/spaces/{space}/products/{slug}": { + "get": { + "operationId": "products.getProduct", + "summary": "Get a public product", + "description": "Fetch a store product's public purchase page by its slug. Returns 404 when the space has no public profile or no publicly purchasable product matches the slug.", + "tags": [ + "Products" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical.", + "examples": [ + "the-dark-forest-anthology-of-the-internet-digital" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "schema": { + "$ref": "#/components/schemas/PublicProductOutput" } } } + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/users/{user}/spaces": { + "/spaces/{space}/releases": { "get": { - "operationId": "users.getUserSpaces", - "summary": "List a user's public spaces", - "description": "The complete list of public spaces a user belongs to, cursor-paginated: pinned spaces first in the user's chosen order, then the rest by join recency. A space appears only if it is public, the space allows it, and the user has not hidden it — so this is a curated shelf, not a membership audit. Never includes roles, member numbers, or join dates. Returns 404 if the user does not exist, has opted their profile out of public visibility, or is not a user identity (no existence leak). A public profile with nothing to show returns an empty list, not a 404.", + "operationId": "releases.listSpaceReleases", + "summary": "List a space public releases", + "description": "A space's published public releases, newest-published first. Returns 404 when the space has no public profile.", "tags": [ - "Users" + "Releases" ], "parameters": [ { - "name": "user", + "name": "space", "in": "path", "required": true, "schema": { "type": "string", - "description": "A user identifier: their handle (username), their identity entity id (`identity_…`), or their protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). The handle is a mutable alias; the id and DID are canonical. Legacy internal identifiers are rejected.", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", "examples": [ - "bvalosek", - "identity_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } }, @@ -16513,7 +9149,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -16526,9 +9162,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -16539,9 +9175,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -16554,671 +9190,374 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicUserSpacePageOutput" + "$ref": "#/components/schemas/PublicReleasePageOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } }, - "/profile": { + "/releases": { "get": { - "operationId": "profile.getOwnProfile", - "summary": "Get your own profile", - "description": "Read a user's own profile. Two ways to call it, per the DFOS API-AUTH specification. **Your own key:** `Authorization: DFOS ` and no `X-Credential` — the subject is the proof's `kid` DID, which is the signer itself, and the response carries every field, because you are reading your own data. **On a user's behalf:** `Authorization: DFOS ` alongside `X-Credential: ` — here the signer is the credential's AUDIENCE (your application) and the subject is the credential chain's ROOT issuer, the user who granted access; the route requires the `read:profile` **or** `read:email` action on this host, and the response is assembled from the actions the grant actually carries: the profile fields (`username`, `displayName`, `description`, `avatarUrl`, `createdAt`) under `read:profile`, `email` under `read:email`, so a `read:email`-only credential receives `{did, email}`. Under both, the presented artifact alone selects the subject: there is no path parameter and no way to name another user, and only `did` is unconditional. Presenting an identity proof together with `X-Credential` is malformed and refused.", + "operationId": "releases.listReleases", + "summary": "List public releases across all spaces", + "description": "Releases with a public page across every publicly-discoverable space, newest-published first. Each item is the space-shelf summary plus its `space`. A space absent from `GET /spaces` is absent here even though its own release routes still serve it.", "tags": [ - "Profile" + "Releases" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + } ], - "parameters": [], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "did": { - "$ref": "#/components/schemas/ProtocolDid" - }, - "username": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "The user's handle, or null if unset. A MUTABLE alias — the `did` is the canonical, stable identifier to store. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof." - }, - "displayName": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Display name, or null. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof." - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Profile bio / description, or null. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof." - }, - "avatarUrl": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Permanent public CDN URL for the profile avatar, or null when they have none. NOT a signed URL — an avatar is public media, so this link is stable while the media is referenced. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof." - }, - "email": { - "type": "string", - "description": "The account email of the authenticated subject. PRIVATE — it is served only under a verified proof: to the subject themselves under an identity proof, and to a third party only when the credential the subject issued carries `read:email`. ABSENT (not null) otherwise. Revoking a credential ends that app’s access immediately; it does not un-share what was already read." - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "When the user joined DFOS (ISO 8601 UTC). ABSENT under a credential that does not carry `read:profile`; always present under an identity proof." - } - }, - "required": [ - "did" - ], - "additionalProperties": false, - "description": "The authenticated user’s own profile" + "$ref": "#/components/schemas/PublicReleaseFeedPageOutput" } } } - }, - "401": { - "description": "401", + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/spaces/{space}/releases/{slug}": { + "get": { + "operationId": "releases.getSpaceRelease", + "summary": "Get a public release", + "description": "Fetch a release's public page by its slug: media wall, credits, composed products, events. Returns 404 when nothing publicly visible matches.", + "tags": [ + "Releases" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical.", + "examples": [ + "the-dark-forest-anthology-of-the-internet" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicReleaseOutput" } } } }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/spaces/{space}/topics": { + "get": { + "operationId": "topics.listTopics", + "summary": "List a space's public topics", + "description": "A space's public topics, ordered by name. Pass an `id` as the `topic` filter on the posts listing.", + "tags": [ + "Topics" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] } }, - "413": { - "description": "413", + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicTopicPageOutput" } } } }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "429": { - "description": "429", + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/spaces/{space}/topics/{topic}": { + "get": { + "operationId": "topics.getTopic", + "summary": "Get a public topic", + "description": "A single public topic. Pair it with `GET /spaces/{space}/posts?topic=...` for the topic's posts. Missing, deleted, non-public, and out-of-space topics all return the same 404.", + "tags": [ + "Topics" + ], + "parameters": [ + { + "name": "space", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A space identifier: its subdomain, its entity id (`space_…`), or its protocol DID. The subdomain is a mutable alias; `id` and `did` are canonical. An identifier of any other shape will not resolve.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + } + }, + { + "name": "topic", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A topic identifier: the entity id (`topic_...`) the topics listing emits.", + "examples": [ + "topic_6c2efd472dvt8rf9k4ftcc" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicTopicOutput" } } } }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, "503": { - "description": "503", + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/users/{user}": { + "get": { + "operationId": "users.getUser", + "summary": "Get a public user profile", + "description": "Fetch a public user profile by handle, identity entity id, or protocol DID. A missing user and one whose profile is not public both return the same 404.", + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "user", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A user's handle, their identity entity id (`identity_…`), or their protocol DID (`did:dfos:` + 31 chars). An identifier of any other shape will not resolve.", + "examples": [ + "bvalosek", + "identity_3drf9nfk93na7vfdk723r6", + "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicUserOutput" } } } - } - }, - "security": [ - { - "dfosIdentityProof": [] }, - { - "dfosRequestProof": [], - "dfosCredential": [] + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - ], - "x-dfos-actions": [ - "read:profile", - "read:email" - ] + } } }, - "/memberships": { + "/users": { "get": { - "operationId": "memberships.listMemberships", - "summary": "List your space memberships", - "description": "The spaces the subject currently belongs to, cursor-paginated, each with their role, how many groups they belong to inside it, and when the membership began. Callable two ways: with your own key (`Authorization: DFOS `, no `X-Credential`), where the subject is the proof’s `kid` DID — the signer itself; or on a user’s behalf with a request proof plus a credential carrying the `read:memberships` action on this host, where the signer is the credential’s audience (your application) and the subject is the credential chain’s ROOT issuer, the user who granted access. Under both, the presented artifact alone selects the subject: there is no path parameter and no way to name another user. **Every** current membership is listed, private and unlisted spaces included — that is what this route is for, and it is what the consent line grants when a third party is the caller. Memberships the user has left, and spaces that were deleted, are not listed. Ordered by `joinedAt` ascending with the space `id` as a stable tiebreak, so a walk never skips or repeats. To check ONE space instead of walking, call `GET /membership/{space}`; for the groups themselves, walk `GET /group-memberships`.", + "operationId": "users.listUsers", + "summary": "List public user profiles", + "description": "List public user profiles, cursor-paginated. An owner who opted out does not appear. Ordering is deterministic and cursor-stable but otherwise unspecified.", "tags": [ - "Memberships" + "Users" ], "parameters": [ - { - "name": "role", - "in": "query", - "schema": { - "anyOf": [ - { - "enum": [ - "owner", - "admin", - "member" - ], - "type": "string", - "description": "The subject's role in the space" - }, - { - "type": "array", - "items": { - "enum": [ - "owner", - "admin", - "member" - ], - "type": "string", - "description": "The subject's role in the space" - } - } - ], - "description": "Restrict to these roles. REPEATABLE — `?role=owner&role=admin` means either. Omit for every role. Hold it CONSTANT while walking pagination cursors: a cursor encodes a position in the filtered set, not the filter itself.", - "examples": [ - "owner" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, { "name": "limit", "in": "query", @@ -17226,7 +9565,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -17239,9 +9578,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -17252,9 +9591,9 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] }, "allowEmptyValue": true, @@ -17265,367 +9604,88 @@ "200": { "description": "OK", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MembershipPageOutput" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicUserPageOutput" } } } - } - }, - "security": [ - { - "dfosIdentityProof": [] }, - { - "dfosRequestProof": [], - "dfosCredential": [] + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } - ], - "x-dfos-actions": [ - "read:memberships" - ] + } } }, - "/membership/{space}": { + "/users/{user}/spaces": { "get": { - "operationId": "memberships.getMembership", - "summary": "Check one space membership", - "description": "Is the subject a member of this space? Returns the single membership entry when they are, and `404` when they are not. **The `404` is collapsed by design: \"no such space\" and \"the user is not a member\" are deliberately indistinguishable** — these routes disclose the subject's own memberships, never the existence of anything else, so the identifier is matched against their membership rows rather than resolved against the platform. This is the gating primitive for a relying party that only needs to ask \"does this user belong to our space\". Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it.", + "operationId": "users.getUserSpaces", + "summary": "List a user's public spaces", + "description": "The public spaces a user belongs to, cursor-paginated: pinned first in the user's chosen order, then the rest by join recency. Only spaces that are public and unhidden appear; a profile with nothing to show returns an empty page, not a 404. 404s exactly as `GET /users/{user}` does — opted out, missing, or not a user identity.", "tags": [ - "Memberships" + "Users" ], "parameters": [ { - "name": "space", + "name": "user", "in": "path", "required": true, "schema": { "type": "string", - "description": "A space: its subdomain, its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). Legacy internal DIDs are never accepted.", + "description": "A user's handle, their identity entity id (`identity_…`), or their protocol DID (`did:dfos:` + 31 chars). An identifier of any other shape will not resolve.", "examples": [ - "metalabel", - "space_6encc4akrze2ah9kntzd9t", - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "bvalosek", + "identity_3drf9nfk93na7vfdk723r6", + "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73" ] } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true } ], "responses": { @@ -17634,387 +9694,137 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MembershipOutput" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/PublicUserSpacePageOutput" } } } }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. \"No such space or group\" and \"the granting user is not in it\" are deliberately indistinguishable — this credential discloses the user's own memberships, never the existence of anything else." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/NotFound" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/profile": { + "get": { + "operationId": "profile.getOwnProfile", + "summary": "Get your own profile", + "description": "The authenticated user's own profile. Under a credential the response is assembled from the actions the grant carries: the profile fields under `read:profile`, `email` under `read:email`, and only `did` unconditionally. A field the grant does not cover is absent rather than null, so check for the key. Under a bare identity proof every field is present.", + "tags": [ + "Profile" + ], + "parameters": [], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] + "type": "object", + "properties": { + "did": { + "$ref": "#/components/schemas/ProtocolDid" }, - { - "type": "object", - "properties": { - "defined": { - "const": false + "username": { + "anyOf": [ + { + "type": "string" }, - "code": { + { + "type": "null" + } + ], + "description": "The user's handle, or null if unset. A mutable alias; the `did` is canonical. Absent without `read:profile`." + }, + "displayName": { + "anyOf": [ + { "type": "string" }, - "status": { - "type": "number" + { + "type": "null" + } + ], + "description": "Display name, or null. Absent without `read:profile`." + }, + "description": { + "anyOf": [ + { + "type": "string" }, - "message": { + { + "type": "null" + } + ], + "description": "Profile bio, or null. Absent without `read:profile`." + }, + "avatarUrl": { + "anyOf": [ + { "type": "string" }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] + { + "type": "null" + } + ], + "description": "Permanent public CDN URL for the profile avatar, or null. Not a signed URL. Absent without `read:profile`." + }, + "email": { + "type": "string", + "description": "The subject's account email, served only under a verified proof. Absent without `read:email`." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "When the user joined DFOS (ISO 8601 UTC). Absent without `read:profile`." + } + }, + "required": [ + "did" + ], + "additionalProperties": false, + "description": "The authenticated user’s own profile", + "examples": [ + { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "username": "bvalosek", + "displayName": "Brandon", + "description": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + "email": "bvalosek@example.com", + "createdAt": "2025-11-19T21:07:44.512Z" } ] } } } + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } }, "security": [ @@ -18027,32 +9837,32 @@ } ], "x-dfos-actions": [ - "read:memberships" + "read:profile", + "read:email" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/profile' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/profile' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } ] } }, - "/group-memberships": { + "/memberships": { "get": { - "operationId": "memberships.listGroupMemberships", - "summary": "List your group memberships", - "description": "The groups the subject currently belongs to, across every space, cursor-paginated — each with the group (including its EXACT active member count and flat `spaceId` / `spaceDid` refs), their role in it, and when the membership began. Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it. Pass `space` to scope to one space, `role` to scope to a role set. Ordered by `joinedAt` ascending with the group `id` as a stable tiebreak. Correlate `group.spaceId` with `space.id` from `GET /memberships` to reassemble the full graph — two flat walks rather than one nested page.", + "operationId": "memberships.listMemberships", + "summary": "List your space memberships", + "description": "The spaces the subject belongs to, cursor-paginated, with their role, group count, and join date. Private and unlisted spaces are included; spaces they have left are not. Ordered by `joinedAt` ascending, space `id` as tiebreak. Use `GET /membership/{space}` to check one space.", "tags": [ "Memberships" ], "parameters": [ - { - "name": "space", - "in": "query", - "schema": { - "type": "string", - "description": "Scope to the groups inside ONE space: its subdomain, its entity id (`space_…`), or its protocol DID. MATCHED against the subject's own memberships, never resolved against the platform — a space the subject is not in and a space that does not exist both yield an EMPTY page, never a 404 and never two distinguishable answers. Hold it constant while walking cursors.", - "examples": [ - "metalabel" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, { "name": "role", "in": "query", @@ -18065,7 +9875,7 @@ "member" ], "type": "string", - "description": "The subject's role in the group" + "description": "The subject's role in the space" }, { "type": "array", @@ -18076,13 +9886,13 @@ "member" ], "type": "string", - "description": "The subject's role in the group" + "description": "The subject's role in the space" } } ], - "description": "Restrict to these group roles. REPEATABLE — `?role=owner&role=admin` means either. Omit for every role. Hold it CONSTANT while walking pagination cursors.", + "description": "Restrict to these roles. Repeatable: `?role=owner&role=admin` means either. Hold it constant while walking cursors.", "examples": [ - "admin" + "owner" ] }, "allowEmptyValue": true, @@ -18095,7 +9905,7 @@ "type": "integer", "minimum": 1, "maximum": 100, - "description": "Maximum number of items per page (default: 20, max: 100)", + "description": "Items per page. Default 20, max 100.", "examples": [ 20 ] @@ -18108,355 +9918,56 @@ "in": "query", "schema": { "type": "string", - "description": "Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee).", - "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" - ] - }, - "allowEmptyValue": true, - "allowReserved": true - }, - { - "name": "before", - "in": "query", - "schema": { - "type": "string", - "description": "Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`).", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", "examples": [ - "eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9" + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" ] - }, - "allowEmptyValue": true, - "allowReserved": true - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GroupMembershipPageOutput" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + }, + "allowEmptyValue": true, + "allowReserved": true }, - "503": { - "description": "503", + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/MembershipPageOutput" } } } + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } }, "security": [ @@ -18470,28 +9981,41 @@ ], "x-dfos-actions": [ "read:memberships" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/memberships' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/memberships' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } ] } }, - "/group-membership/{group}": { + "/membership/{space}": { "get": { - "operationId": "memberships.getGroupMembership", - "summary": "Check one group membership", - "description": "Is the subject a member of this group? Returns the single group-membership entry when they are, and `404` when they are not. **The `404` is collapsed by design: \"no such group\" and \"the user is not a member\" are deliberately indistinguishable** — the identifier is matched against the subject's own group memberships rather than resolved against the platform. The symmetric gating primitive to `GET /membership/{space}`, for a relying party gating on a role inside a space rather than on the space itself. Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it.", + "operationId": "memberships.getMembership", + "summary": "Check one space membership", + "description": "Is the subject a member of this space? Returns the membership entry, or `404` when they are not — a space that does not exist and one they are not in are indistinguishable, because the identifier is matched against their own membership rows.", "tags": [ "Memberships" ], "parameters": [ { - "name": "group", + "name": "space", "in": "path", "required": true, "schema": { "type": "string", - "description": "A group: its entity id (`group_…`) or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). Groups have no subdomain.", + "description": "A space: its subdomain, its entity id (`space_…`), or its protocol DID (`did:dfos:` + 31 chars). An identifier of any other shape will not resolve.", "examples": [ - "group_79h6z77had2kc68ffdkhac", - "did:dfos:79h6z77had2kc68ffdkhac9e4tv3rf" + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] } } @@ -18502,387 +10026,31 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GroupMembershipOutput" + "$ref": "#/components/schemas/MembershipOutput" } } } }, "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ProofRequired" }, "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/Forbidden" }, "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_NOT_FOUND" - }, - "status": { - "const": 404 - }, - "message": { - "type": "string", - "default": "Not found. \"No such space or group\" and \"the granting user is not in it\" are deliberately indistinguishable — this credential discloses the user's own memberships, never the existence of anything else." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/MembershipNotFound" }, "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/PayloadTooLarge" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, - "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } }, "security": [ @@ -18896,345 +10064,297 @@ ], "x-dfos-actions": [ "read:memberships" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/membership/home' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/membership/home' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } ] } }, - "/credential": { + "/group-memberships": { "get": { - "operationId": "credential.getCredential", - "summary": "Describe the presented credential", - "description": "Introspect the credential presented on this request: who issued it, which application holds it, what it grants, how that application was resolved, and when it was issued and expires. Credential-gated like every route in this family — an `Authorization: DFOS ` header alongside `X-Credential: ` — but it requires **no particular scope**: a credential may always describe itself. Use it at startup to confirm a stored grant is still standing and to discover what it covers before calling a route that needs a scope you may not have. A revoked or expired credential does not describe itself; it is refused with `403` like anywhere else.", + "operationId": "memberships.listGroupMemberships", + "summary": "List your group memberships", + "description": "The groups the subject belongs to across every space, cursor-paginated, with their role and join date. Filter with `space` and `role`. Ordered by `joinedAt` ascending, group `id` as tiebreak. Correlate `group.spaceId` with `space.id` from `GET /memberships` for the full graph.", "tags": [ - "Credential" + "Memberships" + ], + "parameters": [ + { + "name": "space", + "in": "query", + "schema": { + "type": "string", + "description": "Scope to the groups inside one space. A space the subject is not in and a space that does not exist both yield an empty page. Hold it constant while walking cursors.", + "examples": [ + "home", + "space_vnzfk7hth9vadc3daahd48", + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "role", + "in": "query", + "schema": { + "anyOf": [ + { + "enum": [ + "owner", + "admin", + "member" + ], + "type": "string", + "description": "The subject's role in the group" + }, + { + "type": "array", + "items": { + "enum": [ + "owner", + "admin", + "member" + ], + "type": "string", + "description": "The subject's role in the group" + } + } + ], + "description": "Restrict to these group roles. Repeatable: `?role=owner&role=admin` means either. Hold it constant while walking cursors.", + "examples": [ + "admin" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page. Default 20, max 100.", + "examples": [ + 20 + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "description": "Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim.", + "examples": [ + "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + ] + }, + "allowEmptyValue": true, + "allowReserved": true + } ], - "parameters": [], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CredentialIntrospectionOutput" + "$ref": "#/components/schemas/GroupMembershipPageOutput" } } } }, "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_AUTHENTICATION_FAILED" - }, - "status": { - "const": 401 - }, - "message": { - "type": "string", - "default": "The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ProofRequired" }, "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_UNAUTHORIZED" - }, - "status": { - "const": 403 - }, - "message": { - "type": "string", - "default": "The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/Forbidden" }, "413": { - "description": "413", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 413 - }, - "message": { - "type": "string", - "default": "The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/PayloadTooLarge" }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:memberships" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/group-memberships' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/group-memberships' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + } + }, + "/group-membership/{group}": { + "get": { + "operationId": "memberships.getGroupMembership", + "summary": "Check one group membership", + "description": "Is the subject a member of this group? Returns the group-membership entry, or `404` when they are not — a group that does not exist and one they are not in are indistinguishable. The symmetric primitive to `GET /membership/{space}`.", + "tags": [ + "Memberships" + ], + "parameters": [ + { + "name": "group", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "A group: its entity id (`group_…`) or its protocol DID (`did:dfos:` + 31 chars, as emitted in `did`). Groups have no subdomain.", + "examples": [ + "group_4kt7cn9vza3fe2h8dr6ac4", + "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMembershipOutput" } } } }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/MembershipNotFound" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, "503": { - "description": "503", + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [ + { + "dfosIdentityProof": [] + }, + { + "dfosRequestProof": [], + "dfosCredential": [] + } + ], + "x-dfos-actions": [ + "read:memberships" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (identity proof)", + "source": "curl 'https://api.dfos.com/v1/group-membership/group_4kt7cn9vza3fe2h8dr6ac4' \\\n -H 'Authorization: DFOS '" + }, + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/group-membership/group_4kt7cn9vza3fe2h8dr6ac4' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } + ] + } + }, + "/credential": { + "get": { + "operationId": "credential.getCredential", + "summary": "Describe the presented credential", + "description": "Introspect the credential presented on this request: who issued it, which application holds it, what it grants, and when it was issued and expires. It requires no particular action — a credential may always describe itself. A revoked or expired one is refused with `403`.", + "tags": [ + "Credential" + ], + "parameters": [], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] + "$ref": "#/components/schemas/CredentialIntrospectionOutput" } } } + }, + "401": { + "$ref": "#/components/responses/ProofRequired" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } }, "security": [ @@ -19242,6 +10362,13 @@ "dfosRequestProof": [], "dfosCredential": [] } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (delegated)", + "source": "curl 'https://api.dfos.com/v1/credential' \\\n -H 'Authorization: DFOS ' \\\n -H 'X-Credential: '" + } ] } }, @@ -19249,7 +10376,7 @@ "post": { "operationId": "keyProof.present", "summary": "Present a key-add envelope", - "description": "Present a signed key-add envelope against an open ceremony. **Takes no authentication** — the code and the signature are the whole capability, and the client presenting them has no session by construction.\n\n**This does not add the key.** It verifies the envelope, stores it, and moves the ceremony to `presented`. The identity's owner then sees the key's fingerprint in their DFOS settings and either adopts it — which is what appends the operation — or rejects it. Poll `GET /v1/key-proof/status?code=…` to find out which.\n\n**You do not construct this URL.** Resolve the 8-character code the user carries out of the DFOS app at `GET /.well-known/dfos-key-proof?code=` on this host. That lookup answers with everything needed to sign, and everything a tool MUST show its human before signing:\n\n```json\n{\n \"present\": \"https://api.dfos.com/v1/key-proof/present\",\n \"nonce\": \"…\",\n \"audience\": \"api.dfos.com\",\n \"purpose\": \"did:dfos:key-add\",\n \"adopts\": { \"did\": \"did:dfos:…\", \"handle\": \"…\", \"displayName\": \"…\" },\n \"roleSet\": \"auth,assert\",\n \"prevCID\": \"…\",\n \"expiresAt\": \"…\",\n \"relay\": \"https://relay.dfos.com\"\n}\n```\n\n`roleSet` VARIES BY CEREMONY and the example above is one value, not a constant. It is the canonical comma-joined set the person chose when they minted the code — `auth,assert` for an ordinary signing key, `controller` for a key registered to carry the identity beyond this platform. **Sign the set the resolution handed you and render it to your human before signing; never hardcode one.** It is a signed POSITION member, so an envelope carrying a different set is refused (before anything is consumed — the code stays live), and a tool that assumed a set would be asking somebody to consent to a grant it is not describing.\n\nAn unknown code and a lapsed one both answer `404 { \"error\": \"unknown or expired code\" }`, deliberately identically. The well-known sits outside `/v1` (it is a discovery document, not a versioned resource), which is why it does not appear as an operation in this specification.\n\nThe envelope is a compact JWS with `typ` `did:dfos:key-add`, signed by the key being added. Its payload is byte-compared against the canonical serialization of exactly `{ nonce, audience, did, roleSet, prevCID, publicKeyMultibase, timestamp }` in that order, so a payload with the right values in a different order is a different signed object and is refused. `audience` is this API's own host; `did`, `roleSet` and `prevCID` are the POSITION the key is being added at, and all three come from the resolution above — an envelope is bound to one introduction on one chain at one head, and is worthless anywhere else. `timestamp` is a whole-second UTC instant within five minutes of now. The optional `description` in the request body is **not** one of those members and is not signed.\n\nOnly a bad SIGNATURE consumes the ceremony: that leaves it `failed` and a new code must be minted. Every other refusal — a malformed body, an over-long `description`, an over-cap `envelope`, a wrong audience, a stale timestamp, a `did`/`roleSet`/`prevCID` that does not match — is decided before anything is consumed, so the code stays live and the command can simply be re-run. If the chain head moved while you were signing, re-resolve the code and present again with a fresh `prevCID`: **presenting the same key twice is admitted**, and replaces the stored envelope. Presenting a DIFFERENT key against a ceremony that already has one is refused. A public key that another identity chain has already PROVED is refused at adoption — one key names one identity, or \"who signed this\" stops having an answer.", + "description": "Present a signed key-add envelope against an open ceremony; poll `GET /v1/key-proof/status` for the owner's decision. Resolve the user's code at `GET /.well-known/dfos-key-proof?code=…` first — it supplies five of the envelope's seven signed members (`nonce`, `audience`, `did`, `roleSet`, `prevCID`). The envelope's payload is byte-compared against the canonical serialization of all seven — `{ nonce, audience, did, roleSet, prevCID, publicKeyMultibase, timestamp }` — in that order. `audience` is this API's own host; `timestamp` is a whole-second UTC instant within five minutes of now. Every refusal but a bad signature leaves the code live to retry; re-presenting the same key is admitted, a different key is refused. Full member table: https://docs.dfos.com/docs/api/key-proof.", "tags": [ "Key proof" ], @@ -19262,15 +10389,15 @@ "properties": { "code": { "type": "string", - "description": "The 8-character ceremony code the user carried out of the DFOS app — the same code the well-known lookup was resolved against", + "description": "The 8-character ceremony code from the DFOS app, as resolved at the well-known lookup.", "examples": [ - "K7M2QXPA" + "K7RD4HEA" ] }, "envelope": { "type": "string", "maxLength": 4096, - "description": "The compact JWS proving possession, `typ` `did:dfos:key-add`. The header carries `alg` (`EdDSA`) and `typ` and nothing else — a `kid` is refused, because the verifying key comes from the payload. The payload carries exactly `nonce`, `audience`, `did`, `roleSet`, `prevCID`, `publicKeyMultibase` and `timestamp`, in that order: it is byte-compared against its canonical serialization, so the member order is part of the contract. At most 4096 bytes", + "description": "The compact JWS proving possession, `typ` `did:dfos:key-add`. The header carries `alg` (`EdDSA`) and `typ` only; a `kid` is refused. Max 4096 bytes.", "examples": [ "eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ..." ] @@ -19278,316 +10405,122 @@ "description": { "type": "string", "maxLength": 200, - "description": "Optional label for the key, shown to the user when they decide whether to adopt it — typically the machine it lives on. **Not part of the signed envelope**: it is unsigned platform metadata, it is not one of the payload members, and nothing about it affects whether the key is admitted. Trimmed; at most 200 characters, the same bound the settings rename enforces. Omitted, empty, or whitespace-only gets the default label `CLI signing key`. The user can rename it afterwards", + "description": "Optional label for the key, shown to its owner at adoption. Unsigned; not part of the envelope. Trimmed, max 200 characters, default `CLI signing key`.", "examples": [ "work laptop" ] } }, "required": [ - "code", - "envelope" - ], - "additionalProperties": false, - "description": "Present a key-add envelope for an open ceremony", - "examples": [ - { - "code": "K7M2QXPA", - "envelope": "eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ...", - "description": "work laptop" - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "const": "presented", - "description": "The envelope verified and is stored. **Nothing is on the identity chain yet** — the owner of the identity has to adopt it in their DFOS settings, and may instead reject it. Poll `/key-proof/status` to find out which" - }, - "adopts": { - "type": "object", - "properties": { - "did": { - "type": "string", - "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", - "description": "The protocol DID of the identity chain this key would be added to", - "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" - ] - }, - "handle": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "The identity’s username, or null if it has none", - "examples": [ - "bvalosek" - ] - }, - "displayName": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "The identity’s display name, or null", - "examples": [ - "Brandon" - ] - } - }, - "required": [ - "did", - "handle", - "displayName" - ], - "additionalProperties": false, - "description": "The identity a key-add ceremony is for" - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "description": "When the ceremony lapses. If nobody adopts or rejects by then it simply expires and nothing was added", - "examples": [ - "2026-08-28T17:10:00Z" - ] - } - }, - "required": [ - "status", - "adopts", - "expiresAt" - ], - "additionalProperties": false, - "description": "The envelope verified. The identity’s owner now decides", - "examples": [ - { - "status": "presented", - "adopts": { - "did": "did:dfos:r7z9c4kfhne2t38va6d9kn2ch7f4b6a", - "handle": "bvalosek", - "displayName": "Brandon" - }, - "expiresAt": "2026-08-28T17:10:00Z" - } - ] - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer \"this ceremony is not open\" — that is a single statement, so there is nothing to learn by varying it. Only a bad SIGNATURE consumes the ceremony; every other refusal leaves the code live, so re-resolve and try again." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } + "code", + "envelope" + ], + "additionalProperties": false, + "description": "Present a key-add envelope for an open ceremony", + "examples": [ + { + "code": "K7RD4HEA", + "envelope": "eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ...", + "description": "work laptop" + } + ] } } - }, - "503": { - "description": "503", + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "oneOf": [ - { + "type": "object", + "properties": { + "status": { + "const": "presented", + "description": "The envelope verified and is stored. Nothing is on the identity chain yet; the owner adopts or rejects it in their DFOS settings." + }, + "adopts": { "type": "object", "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { + "did": { "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." + "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", + "description": "The protocol DID of the identity this key would be added to", + "examples": [ + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + ] }, - "data": {} + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The identity’s username, or null." + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The identity’s display name, or null" + } }, "required": [ - "defined", - "code", - "status", - "message" - ] + "did", + "handle", + "displayName" + ], + "additionalProperties": false, + "description": "The identity a key-add ceremony is for" }, + "expiresAt": { + "type": "string", + "format": "date-time", + "description": "When the ceremony lapses. Nothing is added if nobody adopts or rejects by then." + } + }, + "required": [ + "status", + "adopts", + "expiresAt" + ], + "additionalProperties": false, + "description": "The stored envelope, awaiting the identity’s owner.", + "examples": [ { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} + "status": "presented", + "adopts": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "handle": "bvalosek", + "displayName": "Brandon" }, - "required": [ - "defined", - "code", - "status", - "message" - ] + "expiresAt": "2026-09-08T18:40:00.000Z" } ] } } } + }, + "400": { + "$ref": "#/components/responses/KeyProofRejected" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + }, + "503": { + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } @@ -19596,7 +10529,7 @@ "get": { "operationId": "keyProof.status", "summary": "Poll a key-add ceremony", - "description": "Where the ceremony behind a code has got to. **Takes no authentication** — the code is the capability, and this is scoped to nothing else.\n\nThis is the leg a CLI waits on after presenting: the identity's owner has to adopt or reject in their browser, and until they do the answer is `presented`.\n\nWatch `stale`. While `presented`, it turns true if another writer moves the identity's chain head — the stored envelope is bound to the head it was signed against, so it can no longer be adopted as-is. Re-resolve the code, sign a fresh envelope for the **same key** with the new `prevCID`, and present it again; the owner's approval carries across, and their browser retries the adoption on its own.\n\nOn `adopted`, `onAdopted` carries the DID, the chain-local key id, and the CID of the operation that added it — enough to fetch the chain from the relay the resolution named and file the key locally without asking anything else. **It is served only while the ceremony's ten minutes are still running.** Past `expiresAt` the answer narrows to the bare `status`: a spent code must not stay a permanent handle on somebody's identity, and a CLI polling its own ceremony has the receipt seconds after presenting — long before that matters.\n\n`rejected` is not an error: the person declined a key they did not recognize, nothing was added, and the honest thing to tell your user is exactly that.\n\nA code this deployment never issued is refused. A code it DID issue answers its real state, including after that state is terminal — which a caller needs, since the whole point of polling is to learn how the ceremony ended. So this endpoint does distinguish a real spent code from a fabricated one. That is one bit, it is deliberate, and it is what the rate limit is in front of; the 8-character code space is not a secret this endpoint is defending, the nonce and the signature are.", + "description": "Where the ceremony behind a code has got to. Poll after presenting: the answer stays `presented` until the identity's owner adopts or rejects in the DFOS app, and `rejected` is not an error. If `stale` turns true, present a fresh envelope for the same key; the owner's approval carries across. `onAdopted` is served only until `expiresAt`.", "tags": [ "Key proof" ], @@ -19609,7 +10542,7 @@ "type": "string", "description": "The ceremony code this CLI presented against", "examples": [ - "K7M2QXPA" + "K7RD4HEA" ] }, "allowEmptyValue": true, @@ -19634,11 +10567,11 @@ "expired" ], "type": "string", - "description": "`pending` = nobody has presented an envelope. `presented` = yours verified and the identity’s owner is deciding. `adopted` = they adopted it and the key is on the chain (see `onAdopted`). `rejected` = they declined; **nothing was added**, and that is not an error. `failed` = an envelope was refused at the signature; the ceremony is burned. `expired` = the ten minutes ran out" + "description": "`pending` = nothing presented; `presented` = verified, the owner is deciding; `adopted` = on the chain (see `onAdopted`); `rejected` = declined, nothing added; `failed` = bad signature, ceremony burned; `expired` = the ceremony’s ten minutes ran out." }, "stale": { "type": "boolean", - "description": "Present only while `presented`. True when the chain head has moved since your envelope was signed, so it can no longer be adopted as-is. **Re-resolve the code and present a fresh envelope for the same key** — that is admitted, and the owner’s approval carries across it" + "description": "Present only while `presented`. True when the chain head has moved and the envelope can no longer be adopted as-is. Present a fresh envelope for the same key." }, "onAdopted": { "type": "object", @@ -19648,22 +10581,16 @@ "pattern": "^did:dfos:[2346789acdefhknrtvz]{31}$", "description": "The identity chain the key was added to", "examples": [ - "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc" + "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" ] }, "keyId": { "type": "string", - "description": "The chain-local id the key was given. The DID URL a verifier sees is `#`", - "examples": [ - "key_4h2ndv79fckae3rz6t8v2d" - ] + "description": "The chain-local id the key was given. The DID URL a verifier sees is `#`" }, "chainOpCID": { "type": "string", - "description": "CID of the operation that added the key, with your envelope embedded in it — the receipt anyone can go read", - "examples": [ - "bafyreib2rxk3rhqhbvpaowvtdcnqvbtwbctvvbrqjhkgvhvcnpi7zdkeqm" - ] + "description": "CID of the operation that added the key, with the envelope embedded in it." } }, "required": [ @@ -19684,208 +10611,31 @@ { "status": "presented", "stale": false - } - ] - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_INVALID_REQUEST" - }, - "status": { - "const": 400 - }, - "message": { - "type": "string", - "default": "The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer \"this ceremony is not open\" — that is a single statement, so there is nothing to learn by varying it. Only a bad SIGNATURE consumes the ceremony; every other refusal leaves the code live, so re-resolve and try again." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] }, { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] + "status": "adopted", + "onAdopted": { + "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + "keyId": "key_4h2ndv79fckae3rz6t8v2d", + "chainOpCID": "bafyreib2rxk3rhqhbvpaowvtdcnqvbtwbctvvbrqjhkgvhvcnpi7zdkeqm" + } } ] } } } }, + "400": { + "$ref": "#/components/responses/KeyProofRejected" + }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } @@ -19894,7 +10644,7 @@ "get": { "operationId": "protocol.getProtocolInfo", "summary": "Get protocol discovery info", - "description": "Return the protocol discovery document for this API deployment: the DID method and specification, plus URL templates for the relay proof plane and indexes.", + "description": "The protocol discovery document for this deployment: DID method, spec URL, relay URL, and relay endpoint templates.", "tags": [ "Protocol" ], @@ -19910,141 +10660,13 @@ } }, "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_RATE_LIMITED" - }, - "status": { - "const": 429 - }, - "message": { - "type": "string", - "default": "Rate limit exceeded — retry after `retryAfterMs`." - }, - "data": { - "type": "object", - "properties": { - "scope": { - "type": "string", - "description": "Which per-IP budget was exhausted" - }, - "retryAfterMs": { - "type": "number", - "description": "Milliseconds to wait before retrying" - } - }, - "required": [ - "scope", - "retryAfterMs" - ] - } - }, - "required": [ - "defined", - "code", - "status", - "message", - "data" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/RateLimited" }, "503": { - "description": "503", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "properties": { - "defined": { - "const": true - }, - "code": { - "const": "E_SERVICE_UNAVAILABLE" - }, - "status": { - "const": 503 - }, - "message": { - "type": "string", - "default": "Service temporarily unavailable — the rate-limit store was unreachable (fail-closed)." - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - }, - { - "type": "object", - "properties": { - "defined": { - "const": false - }, - "code": { - "type": "string" - }, - "status": { - "type": "number" - }, - "message": { - "type": "string" - }, - "data": {} - }, - "required": [ - "defined", - "code", - "status", - "message" - ] - } - ] - } - } - } + "$ref": "#/components/responses/ServiceUnavailable" + }, + "default": { + "$ref": "#/components/responses/Error" } } } diff --git a/package.json b/package.json index 07c1006..a7df6ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metalabel/dfos-api", - "version": "0.11.0", + "version": "0.12.0", "type": "module", "description": "Typed TypeScript client for the public DFOS API — a committed openapi.json snapshot, generated types, and a thin openapi-fetch client", "license": "MIT", diff --git a/src/generated/api.ts b/src/generated/api.ts index e77a185..9c45d64 100644 --- a/src/generated/api.ts +++ b/src/generated/api.ts @@ -13,7 +13,7 @@ export interface paths { }; /** * Get a public space - * @description Fetch a space public profile + site config by its subdomain, entity id, or protocol DID. Returns 404 if the space does not exist or has no public profile (no existence leak — private and missing are indistinguishable). + * @description A space's public profile and site configuration, by subdomain, entity id, or protocol DID. Missing and non-public spaces give the same 404. */ get: operations["spaces.getSpace"]; put?: never; @@ -33,7 +33,7 @@ export interface paths { }; /** * List public spaces - * @description List public, discoverable spaces, cursor-paginated. Lean discovery-card projection per item. Optionally filter by `joinMode`, recency (`activeWithinDays`), `category`, or whether the space explicitly opted in to the directory (`optedIn`). Every filter is a request parameter, never encoded in a cursor. Ordered by recent activity. + * @description Public, discoverable spaces as lean cards, ordered by recent activity. Filter by `joinMode`, `activeWithinDays`, `category`, or `optedIn`. */ get: operations["spaces.listSpaces"]; put?: never; @@ -53,21 +53,15 @@ export interface paths { }; /** * List a space public feed - * @description List the posts in a space, newest first by default, cursor-paginated. Optionally filter by format, topic, or publication window, and select oldest-first ordering. + * @description List a space's posts, newest first. Filter by format, topic, or publication window, or sort oldest-first. * - * **Anonymously**, only posts explicitly published to the public surface are returned (a public per-post view-access override, or a post in a topic marked world-readable). **With a credential covering this space and `read:posts`** (or a bare identity proof), the feed is the one the granting user sees: every post their membership reaches, with the full body and attachments where they genuinely read it, and a `viewer` block per post. A valid credential that does NOT cover this space returns exactly the anonymous feed — a grant only ever adds. Returns 404 if the space has no public profile, for every caller alike. + * Without a proof the feed is the space's public posts. A grant covering this space and `read:posts` returns the feed the granting user sees; an uncovered grant returns the anonymous feed. */ get: operations["posts.listPosts"]; put?: never; /** * Write a post - * @description Write a post as the granting user, into a topic they can post in. - * - * Text only. `title` decides the format — with one the post is a `long-post`, without one a `short-post` — and there is deliberately no way to attach media, set a cover, announce or broadcast the post, backdate it, override its view access, or fulfil an event: those fields do not exist on this input. Body length limits, the topic's own format policy, and the `post:create` permission are all enforced by the same engine the DFOS app writes through. - * - * Requires `write:posts` covering this space under the delegated profile, or a bare identity proof. Returns 404 when the space has no public profile or the topic is one the user cannot reach — the same 404 for every caller, so the route never reports which topics exist. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Write a post as the granting user, into a topic they can post in. Text only: `title` makes it a `long-post`. Media, covers, announce, backdating, and view-access overrides are not on this input. An unreachable topic is a 404. */ post: operations["posts.createPost"]; delete?: never; @@ -85,33 +79,23 @@ export interface paths { }; /** * Get a post - * @description Fetch a single post in a space by ID. Returns a discriminated union on `state`: `eligible` (with the post content) when the caller can read it, or `gated` (with a slim space CTA, no content) when the post exists in this public space but the caller cannot. + * @description Fetch a post by ID. Returns a union on `state`: `eligible` with the post, or `gated` with a space CTA when the caller cannot read it. * - * **With a credential covering this space and `read:posts`** (or a bare identity proof), "can read" means what the granting user can read, so a members-only post comes back `eligible` with the full body, attachments, and a `viewer` block. A valid credential that does NOT cover this space returns exactly the anonymous response. Returns 404 if the space has no public profile, the post does not exist, or the post is reached through the wrong space — the same 404, for every caller alike. + * A missing post, a private space, and a post reached through the wrong space are the same 404. */ get: operations["posts.getPost"]; put?: never; post?: never; /** * Delete a post - * @description Delete one of the granting user's OWN posts. - * - * OWN CONTENT ONLY — deleting another member's post is moderation, and this tier does not reach it even for an admin. Requires `write:posts` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Delete one of the granting user's own posts. Own content only: another member's post is a 403 even for a space admin. */ delete: operations["posts.deletePost"]; options?: never; head?: never; /** * Edit a post - * @description Edit one of the granting user's OWN posts. Send at least one of `title`, `body`, or `topic`; anything you omit is left alone. - * - * OWN CONTENT ONLY, and that is narrower than the app: a space admin editing somebody else's post is a moderation act, and this tier does not reach it. Editing a post the user did not write is a 403 even when their role in the space would allow it in the DFOS app. - * - * Requires `write:posts` covering this space under the delegated profile, or a bare identity proof. A post that does not exist, sits in another space, or lives in a topic the user cannot reach is the same 404. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Edit one of the granting user's own posts. Send at least one of `title`, `body`, or `topic`; omitted fields are left alone. Own content only: editing another member's post is a 403 even for a space admin. */ patch: operations["posts.editPost"]; trace?: never; @@ -126,21 +110,13 @@ export interface paths { get?: never; /** * Upvote a post - * @description Add the granting user's upvote to a post. IDEMPOTENT: upvoting a post they have already upvoted succeeds and returns the same state, so a client may safely re-send. - * - * This writes the user's name onto a signal other members can see. Requires `write:upvotes` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Add the granting user's upvote to a post. Idempotent. */ put: operations["posts.upvotePost"]; post?: never; /** * Remove a post upvote - * @description Remove the granting user's upvote from a post. IDEMPOTENT: removing an upvote that is not there succeeds and returns the same state. - * - * Requires `write:upvotes` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Remove the granting user's upvote from a post. Idempotent. */ delete: operations["posts.removePostUpvote"]; options?: never; @@ -157,23 +133,13 @@ export interface paths { }; /** * List a post’s comments - * @description The comment thread on a post, as the granting user sees it — cursor-paginated, flat, root comments by default. - * - * Requires `read:posts` covering this space under the delegated profile, or a bare identity proof. There is NO anonymous projection: comment visibility inherits the post's everywhere in DFOS, and the conversation under a post is space interior rather than something the space published. Every refusal that is not an authentication failure collapses into the SAME 404 as an unknown post — a grant that does not reach this space, a post in another space, a post the user cannot read, and a post that does not exist are one answer. - * - * Pass `parentCommentId` to walk one comment's replies instead of the roots. + * @description The comment thread on a post, cursor-paginated and flat, root comments by default. Pass `parentCommentId` to walk one comment's replies instead. There is no anonymous projection of a comment thread: an uncovered grant, a post in another space, a post the user cannot read, and a post that does not exist are all the same 404. */ get: operations["comments.listPostComments"]; put?: never; /** * Write a comment - * @description Write a comment on a post, as the granting user. Pass `parentCommentId` to reply to an existing comment instead of commenting on the post itself — threads are one level deep, so a reply to a reply attaches to the same root. - * - * Text only: there is no way to attach media to a comment on this API. The parent post's visibility gate and the topic's `comment:create` permission are enforced by the same engine the DFOS app writes through, so a post the user cannot read is the same 404 the read routes give. - * - * Requires `write:comments` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Write a comment on a post, or pass `parentCommentId` to reply to an existing comment. Threads are one level deep: a reply to a reply attaches to the same root. Text only — a comment cannot carry media. A post the user cannot read answers the same 404 as one that does not exist. */ post: operations["comments.createComment"]; delete?: never; @@ -194,24 +160,14 @@ export interface paths { post?: never; /** * Delete a comment - * @description Delete one of the granting user's OWN comments. Replies to it are not deleted with it. - * - * OWN CONTENT ONLY — deleting another member's comment is moderation, which this tier does not reach. Requires `write:comments` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Delete one of the granting user's own comments. Replies to it are not deleted with it. Own content only: deleting another member's comment is moderation, which this tier does not reach. */ delete: operations["comments.deleteComment"]; options?: never; head?: never; /** * Edit a comment - * @description Edit one of the granting user's OWN comments. - * - * OWN CONTENT ONLY, and narrower than the app: editing another member's comment is not something this tier can do for anybody, whatever their role in the space. - * - * Requires `write:comments` covering this space under the delegated profile, or a bare identity proof. A comment that does not exist, sits in another space, or hangs off a post the user cannot read is the same 404. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Replace the body of one of the granting user's own comments. Own content only: this tier cannot edit another member's comment, whatever the caller's role in the space. A comment that does not exist, sits in another space, or hangs off a post the user cannot read is the same 404. */ patch: operations["comments.editComment"]; trace?: never; @@ -226,21 +182,13 @@ export interface paths { get?: never; /** * Upvote a comment - * @description Add the granting user's upvote to a comment. IDEMPOTENT: upvoting one they have already upvoted succeeds and returns the same state. - * - * Requires `write:upvotes` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Add the granting user's upvote to a comment. Idempotent: upvoting a comment they have already upvoted returns the same state. */ put: operations["comments.upvoteComment"]; post?: never; /** * Remove a comment upvote - * @description Remove the granting user's upvote from a comment. IDEMPOTENT: removing one that is not there succeeds and returns the same state. - * - * Requires `write:upvotes` covering this space under the delegated profile, or a bare identity proof. - * - * Every write requires a per-request `jti` in the proof (`generateJti()` in the DFOS client mints one; `createApiAuthFetch` attaches one automatically for non-GET methods). Presenting the same `jti` twice inside the freshness window is a `409` — the first attempt may have landed, so re-read state rather than retrying, and mint a NEW `jti` for a genuine retry. + * @description Remove the granting user's upvote from a comment. Idempotent: removing an upvote that is not there returns the same state. */ delete: operations["comments.removeCommentUpvote"]; options?: never; @@ -257,11 +205,9 @@ export interface paths { }; /** * The cross-space feed - * @description The granting user's own post feed across the spaces this grant reaches — newest first, cursor-paginated, one flat stream. Each item carries the space it came from. + * @description The granting user's own feed across the spaces this grant reaches — newest first, one flat stream. Each item carries its space. * - * Requires `read:posts` under the delegated profile, or a bare identity proof for a caller reading their own feed. The reach is the grant's: a credential covering all the user's spaces feeds every space they are an active member of, including ones joined later; a credential naming particular spaces feeds exactly those. A space the user has left contributes nothing and is not reported — losing a space looks like silence, not an error. A caller in no reachable space gets an empty page. - * - * Pinned posts are NOT hoisted: a pin is a fact about one space's own feed, and hoisting every space's pins onto page one would bury the chronology this route exists for. + * A grant covering all the user's spaces feeds every space they are an active member of, including ones joined later. A space the user has left contributes nothing. Pinned posts are not hoisted. */ get: operations["feed.listFeed"]; put?: never; @@ -281,7 +227,7 @@ export interface paths { }; /** * List a space public pages - * @description List a space's published pages, ordered by sort order then ID and cursor-paginated. Returns 404 if the space has no public profile. Pages are always fully public; only enabled pages appear. + * @description A space's published pages, ordered by `sortOrder` then id. Only enabled pages appear. */ get: operations["pages.listPages"]; put?: never; @@ -301,7 +247,7 @@ export interface paths { }; /** * Get a public page - * @description Fetch a single published page in a space by stable page ID or slug, with its full markdown body and resolved inline media. Returns 404 if the space has no public profile, the page does not exist, belongs to another space, or is not enabled — all byte-identical (no existence leak). + * @description One published page by id or slug, with its full body and inline media. A missing, disabled, foreign-space, or non-public page is the same 404. */ get: operations["pages.getPage"]; put?: never; @@ -321,7 +267,7 @@ export interface paths { }; /** * List a space public events - * @description List a space's PUBLIC events within a time window, earliest first, cursor-paginated. Only events the space marked public and published appear — member-only, group-only, and admin-only events are absent, and so are drafts and cancelled events (no existence leak: an absent event is indistinguishable from one that was never created). Returns 404 if the space has no public profile. Recurring events are expanded into individual occurrences within the window; a cursor walk reaches at most the first 500 occurrences of that window (narrow the window to see past them). + * @description A space's public, published events within a time window, earliest first. Non-public, draft, and cancelled events are absent. Recurring events expand into occurrences; a cursor walk reaches the first 500 in the window — narrow the window rather than paging past them. */ get: operations["events.listSpaceEvents"]; put?: never; @@ -341,7 +287,7 @@ export interface paths { }; /** * Get a public event - * @description Fetch ONE occurrence of a space's public event by its event id — the direct read behind a per-event page, and the same item shape the listings return. Defaults to the event's SOONEST UPCOMING public occurrence within the ~1-year look-ahead this API serves; pass `occ` to address a specific occurrence, including one beyond that horizon. Returns 404 if the space has no public profile, the event does not exist, belongs to another space, is a draft, is cancelled, is not public, or has no upcoming public occurrence at all — every case byte-identical (no existence leak). Resolution runs the SAME expansion as `GET /spaces/{space}/events`, so `recurrenceId` here is byte-identical to the one the listing emits for the same occurrence, and any pair you take off a listing addresses the same occurrence when you hand it back. + * @description One occurrence of a space's public event, in the shape the listings return. Defaults to the soonest upcoming occurrence; pass `occ` for a specific one, including beyond the ~1-year look-ahead. A missing, draft, cancelled, non-public, foreign-space, or fully past event is the same 404. */ get: operations["events.getSpaceEvent"]; put?: never; @@ -361,7 +307,7 @@ export interface paths { }; /** * List upcoming public events - * @description List upcoming PUBLIC events across every publicly-discoverable space, earliest first, cursor-paginated. Each item carries the space it belongs to. Same content rule as the per-space listing — public, published events only — with the space gate matching public space discovery (`GET /spaces`) rather than direct space lookup. Recurring events are expanded into individual occurrences within the window; a cursor walk reaches at most the first 500 occurrences of that window (narrow the window to see past them). + * @description Upcoming public events across every publicly-discoverable space, earliest first. Each item carries its space. Only spaces that appear in `GET /spaces` contribute. Recurring events expand into occurrences; a cursor walk reaches the first 500 in the window — narrow the window rather than paging past them. */ get: operations["events.listUpcomingEvents"]; put?: never; @@ -381,7 +327,7 @@ export interface paths { }; /** * List a space public products - * @description List the store products a space has published public purchase pages for, in the space's curated display order (`sortOrder`, lower first), cursor-paginated. A space that has never reordered its store reads newest-first. Each item is the same object the single-product route returns. Returns 404 if the space has no public profile (byte-identical to every other not-found — no existence leak). Products the seller has not published, and archived or inactive ones, never appear and are never counted in `sortOrder`; an empty list is a valid answer and reveals nothing. + * @description A space's public store products, in its display order (`sortOrder`, lowest first). Unpublished, archived, and inactive products never appear. Returns 404 when the space has no public profile. */ get: operations["products.listProducts"]; put?: never; @@ -401,7 +347,7 @@ export interface paths { }; /** * List public products across all spaces - * @description List every store product with a public purchase page, across every publicly-discoverable space, newest-created first, cursor-paginated. Each item is the same object the space-scoped routes return plus the `space` it belongs to. The space gate matches public space discovery (`GET /spaces`) rather than direct space lookup, so a space excluded from the directory is absent here even though its own product routes still serve it — and a space turning its public site off drops every one of its products from this feed by predicate. No filters beyond pagination in this version. + * @description Every store product with a public purchase page across every publicly-discoverable space, newest-created first. Each item adds the `space` it belongs to. A space absent from `GET /spaces` is absent here even though its own product routes still serve it. */ get: operations["products.listAllProducts"]; put?: never; @@ -421,7 +367,7 @@ export interface paths { }; /** * Get a public product - * @description Fetch a store product's public purchase page by its slug. Returns 404 if the space has no public profile, the slug matches no product, the product belongs to another space, the seller has not published a public page for it, or the product is archived or inactive — all byte-identical (no existence leak). + * @description Fetch a store product's public purchase page by its slug. Returns 404 when the space has no public profile or no publicly purchasable product matches the slug. */ get: operations["products.getProduct"]; put?: never; @@ -441,7 +387,7 @@ export interface paths { }; /** * List a space public releases - * @description List the releases a space has published public pages for, newest-published first (`publishedAt` descending), cursor-paginated. Returns 404 if the space has no public profile (byte-identical to every other not-found — no existence leak). Unpublished releases and releases with no slug never appear and are never counted; an empty list is a valid answer and reveals nothing. + * @description A space's published public releases, newest-published first. Returns 404 when the space has no public profile. */ get: operations["releases.listSpaceReleases"]; put?: never; @@ -461,7 +407,7 @@ export interface paths { }; /** * List public releases across all spaces - * @description List releases with a public page across every publicly-discoverable space, newest-published first (`publishedAt` descending), cursor-paginated. Each item is the same summary a space's own shelf returns plus the `space` it belongs to. The space gate matches public space discovery (`GET /spaces`) rather than direct space lookup, so a space excluded from the directory is absent here even though its own release routes still serve it. Unpublished releases and releases with no slug never appear; an empty page is a valid answer and reveals nothing. No filters beyond pagination in this version. + * @description Releases with a public page across every publicly-discoverable space, newest-published first. Each item is the space-shelf summary plus its `space`. A space absent from `GET /spaces` is absent here even though its own release routes still serve it. */ get: operations["releases.listReleases"]; put?: never; @@ -481,7 +427,7 @@ export interface paths { }; /** * Get a public release - * @description Fetch a release's public page by its slug, with its media wall, credits, composed products, and public dates. Returns 404 if the space has no public profile, the slug matches no release, the release belongs to another space, it has not been published, or it has been deleted — all byte-identical (no existence leak). + * @description Fetch a release's public page by its slug: media wall, credits, composed products, events. Returns 404 when nothing publicly visible matches. */ get: operations["releases.getSpaceRelease"]; put?: never; @@ -501,7 +447,7 @@ export interface paths { }; /** * List a space's public topics - * @description List the live, world-readable topics in a space, ordered by name then ID, cursor-paginated. Returns 404 if the space has no public profile. + * @description A space's public topics, ordered by name. Pass an `id` as the `topic` filter on the posts listing. */ get: operations["topics.listTopics"]; put?: never; @@ -521,7 +467,7 @@ export interface paths { }; /** * Get a public topic - * @description Fetch a single live, world-readable topic in a space by its stable topic ID — the direct read behind a per-topic page. Pair it with `GET /spaces/{space}/posts?topic=...` for the topic's public posts. Returns 404 if the space has no public profile, the topic does not exist, belongs to another space, is deleted, is not world-readable, or carries no public post feed — all byte-identical (no existence leak). + * @description A single public topic. Pair it with `GET /spaces/{space}/posts?topic=...` for the topic's posts. Missing, deleted, non-public, and out-of-space topics all return the same 404. */ get: operations["topics.getTopic"]; put?: never; @@ -541,7 +487,7 @@ export interface paths { }; /** * Get a public user profile - * @description Fetch a public user profile by its handle (username), its identity entity id, or its protocol DID. Handles are mutable aliases — the id and DID are canonical. Returns 404 if the user does not exist, has opted their profile out of public visibility, or is not a user identity (no existence leak — private and missing are indistinguishable). + * @description Fetch a public user profile by handle, identity entity id, or protocol DID. A missing user and one whose profile is not public both return the same 404. */ get: operations["users.getUser"]; put?: never; @@ -561,7 +507,7 @@ export interface paths { }; /** * List public user profiles - * @description List public member profiles, cursor-paginated. Profiles are public by default and owners can opt out — an opted-out profile does not appear here. Each row carries only the terse pinned-space cards; fetch the profile for the full membership shelf. Ordering is deterministic and cursor-stable but otherwise unspecified; do not depend on it. + * @description List public user profiles, cursor-paginated. An owner who opted out does not appear. Ordering is deterministic and cursor-stable but otherwise unspecified. */ get: operations["users.listUsers"]; put?: never; @@ -581,7 +527,7 @@ export interface paths { }; /** * List a user's public spaces - * @description The complete list of public spaces a user belongs to, cursor-paginated: pinned spaces first in the user's chosen order, then the rest by join recency. A space appears only if it is public, the space allows it, and the user has not hidden it — so this is a curated shelf, not a membership audit. Never includes roles, member numbers, or join dates. Returns 404 if the user does not exist, has opted their profile out of public visibility, or is not a user identity (no existence leak). A public profile with nothing to show returns an empty list, not a 404. + * @description The public spaces a user belongs to, cursor-paginated: pinned first in the user's chosen order, then the rest by join recency. Only spaces that are public and unhidden appear; a profile with nothing to show returns an empty page, not a 404. 404s exactly as `GET /users/{user}` does — opted out, missing, or not a user identity. */ get: operations["users.getUserSpaces"]; put?: never; @@ -601,7 +547,7 @@ export interface paths { }; /** * Get your own profile - * @description Read a user's own profile. Two ways to call it, per the DFOS API-AUTH specification. **Your own key:** `Authorization: DFOS ` and no `X-Credential` — the subject is the proof's `kid` DID, which is the signer itself, and the response carries every field, because you are reading your own data. **On a user's behalf:** `Authorization: DFOS ` alongside `X-Credential: ` — here the signer is the credential's AUDIENCE (your application) and the subject is the credential chain's ROOT issuer, the user who granted access; the route requires the `read:profile` **or** `read:email` action on this host, and the response is assembled from the actions the grant actually carries: the profile fields (`username`, `displayName`, `description`, `avatarUrl`, `createdAt`) under `read:profile`, `email` under `read:email`, so a `read:email`-only credential receives `{did, email}`. Under both, the presented artifact alone selects the subject: there is no path parameter and no way to name another user, and only `did` is unconditional. Presenting an identity proof together with `X-Credential` is malformed and refused. + * @description The authenticated user's own profile. Under a credential the response is assembled from the actions the grant carries: the profile fields under `read:profile`, `email` under `read:email`, and only `did` unconditionally. A field the grant does not cover is absent rather than null, so check for the key. Under a bare identity proof every field is present. */ get: operations["profile.getOwnProfile"]; put?: never; @@ -621,7 +567,7 @@ export interface paths { }; /** * List your space memberships - * @description The spaces the subject currently belongs to, cursor-paginated, each with their role, how many groups they belong to inside it, and when the membership began. Callable two ways: with your own key (`Authorization: DFOS `, no `X-Credential`), where the subject is the proof’s `kid` DID — the signer itself; or on a user’s behalf with a request proof plus a credential carrying the `read:memberships` action on this host, where the signer is the credential’s audience (your application) and the subject is the credential chain’s ROOT issuer, the user who granted access. Under both, the presented artifact alone selects the subject: there is no path parameter and no way to name another user. **Every** current membership is listed, private and unlisted spaces included — that is what this route is for, and it is what the consent line grants when a third party is the caller. Memberships the user has left, and spaces that were deleted, are not listed. Ordered by `joinedAt` ascending with the space `id` as a stable tiebreak, so a walk never skips or repeats. To check ONE space instead of walking, call `GET /membership/{space}`; for the groups themselves, walk `GET /group-memberships`. + * @description The spaces the subject belongs to, cursor-paginated, with their role, group count, and join date. Private and unlisted spaces are included; spaces they have left are not. Ordered by `joinedAt` ascending, space `id` as tiebreak. Use `GET /membership/{space}` to check one space. */ get: operations["memberships.listMemberships"]; put?: never; @@ -641,7 +587,7 @@ export interface paths { }; /** * Check one space membership - * @description Is the subject a member of this space? Returns the single membership entry when they are, and `404` when they are not. **The `404` is collapsed by design: "no such space" and "the user is not a member" are deliberately indistinguishable** — these routes disclose the subject's own memberships, never the existence of anything else, so the identifier is matched against their membership rows rather than resolved against the platform. This is the gating primitive for a relying party that only needs to ask "does this user belong to our space". Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it. + * @description Is the subject a member of this space? Returns the membership entry, or `404` when they are not — a space that does not exist and one they are not in are indistinguishable, because the identifier is matched against their own membership rows. */ get: operations["memberships.getMembership"]; put?: never; @@ -661,7 +607,7 @@ export interface paths { }; /** * List your group memberships - * @description The groups the subject currently belongs to, across every space, cursor-paginated — each with the group (including its EXACT active member count and flat `spaceId` / `spaceDid` refs), their role in it, and when the membership began. Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it. Pass `space` to scope to one space, `role` to scope to a role set. Ordered by `joinedAt` ascending with the group `id` as a stable tiebreak. Correlate `group.spaceId` with `space.id` from `GET /memberships` to reassemble the full graph — two flat walks rather than one nested page. + * @description The groups the subject belongs to across every space, cursor-paginated, with their role and join date. Filter with `space` and `role`. Ordered by `joinedAt` ascending, group `id` as tiebreak. Correlate `group.spaceId` with `space.id` from `GET /memberships` for the full graph. */ get: operations["memberships.listGroupMemberships"]; put?: never; @@ -681,7 +627,7 @@ export interface paths { }; /** * Check one group membership - * @description Is the subject a member of this group? Returns the single group-membership entry when they are, and `404` when they are not. **The `404` is collapsed by design: "no such group" and "the user is not a member" are deliberately indistinguishable** — the identifier is matched against the subject's own group memberships rather than resolved against the platform. The symmetric gating primitive to `GET /membership/{space}`, for a relying party gating on a role inside a space rather than on the space itself. Callable with your own identity proof, where the subject is the proof’s `kid` DID; or on a user’s behalf with a credential carrying the `read:memberships` action on this host, where the subject is the credential chain’s ROOT issuer rather than the application presenting it. + * @description Is the subject a member of this group? Returns the group-membership entry, or `404` when they are not — a group that does not exist and one they are not in are indistinguishable. The symmetric primitive to `GET /membership/{space}`. */ get: operations["memberships.getGroupMembership"]; put?: never; @@ -701,7 +647,7 @@ export interface paths { }; /** * Describe the presented credential - * @description Introspect the credential presented on this request: who issued it, which application holds it, what it grants, how that application was resolved, and when it was issued and expires. Credential-gated like every route in this family — an `Authorization: DFOS ` header alongside `X-Credential: ` — but it requires **no particular scope**: a credential may always describe itself. Use it at startup to confirm a stored grant is still standing and to discover what it covers before calling a route that needs a scope you may not have. A revoked or expired credential does not describe itself; it is refused with `403` like anywhere else. + * @description Introspect the credential presented on this request: who issued it, which application holds it, what it grants, and when it was issued and expires. It requires no particular action — a credential may always describe itself. A revoked or expired one is refused with `403`. */ get: operations["credential.getCredential"]; put?: never; @@ -723,33 +669,7 @@ export interface paths { put?: never; /** * Present a key-add envelope - * @description Present a signed key-add envelope against an open ceremony. **Takes no authentication** — the code and the signature are the whole capability, and the client presenting them has no session by construction. - * - * **This does not add the key.** It verifies the envelope, stores it, and moves the ceremony to `presented`. The identity's owner then sees the key's fingerprint in their DFOS settings and either adopts it — which is what appends the operation — or rejects it. Poll `GET /v1/key-proof/status?code=…` to find out which. - * - * **You do not construct this URL.** Resolve the 8-character code the user carries out of the DFOS app at `GET /.well-known/dfos-key-proof?code=` on this host. That lookup answers with everything needed to sign, and everything a tool MUST show its human before signing: - * - * ```json - * { - * "present": "https://api.dfos.com/v1/key-proof/present", - * "nonce": "…", - * "audience": "api.dfos.com", - * "purpose": "did:dfos:key-add", - * "adopts": { "did": "did:dfos:…", "handle": "…", "displayName": "…" }, - * "roleSet": "auth,assert", - * "prevCID": "…", - * "expiresAt": "…", - * "relay": "https://relay.dfos.com" - * } - * ``` - * - * `roleSet` VARIES BY CEREMONY and the example above is one value, not a constant. It is the canonical comma-joined set the person chose when they minted the code — `auth,assert` for an ordinary signing key, `controller` for a key registered to carry the identity beyond this platform. **Sign the set the resolution handed you and render it to your human before signing; never hardcode one.** It is a signed POSITION member, so an envelope carrying a different set is refused (before anything is consumed — the code stays live), and a tool that assumed a set would be asking somebody to consent to a grant it is not describing. - * - * An unknown code and a lapsed one both answer `404 { "error": "unknown or expired code" }`, deliberately identically. The well-known sits outside `/v1` (it is a discovery document, not a versioned resource), which is why it does not appear as an operation in this specification. - * - * The envelope is a compact JWS with `typ` `did:dfos:key-add`, signed by the key being added. Its payload is byte-compared against the canonical serialization of exactly `{ nonce, audience, did, roleSet, prevCID, publicKeyMultibase, timestamp }` in that order, so a payload with the right values in a different order is a different signed object and is refused. `audience` is this API's own host; `did`, `roleSet` and `prevCID` are the POSITION the key is being added at, and all three come from the resolution above — an envelope is bound to one introduction on one chain at one head, and is worthless anywhere else. `timestamp` is a whole-second UTC instant within five minutes of now. The optional `description` in the request body is **not** one of those members and is not signed. - * - * Only a bad SIGNATURE consumes the ceremony: that leaves it `failed` and a new code must be minted. Every other refusal — a malformed body, an over-long `description`, an over-cap `envelope`, a wrong audience, a stale timestamp, a `did`/`roleSet`/`prevCID` that does not match — is decided before anything is consumed, so the code stays live and the command can simply be re-run. If the chain head moved while you were signing, re-resolve the code and present again with a fresh `prevCID`: **presenting the same key twice is admitted**, and replaces the stored envelope. Presenting a DIFFERENT key against a ceremony that already has one is refused. A public key that another identity chain has already PROVED is refused at adoption — one key names one identity, or "who signed this" stops having an answer. + * @description Present a signed key-add envelope against an open ceremony; poll `GET /v1/key-proof/status` for the owner's decision. Resolve the user's code at `GET /.well-known/dfos-key-proof?code=…` first — it supplies five of the envelope's seven signed members (`nonce`, `audience`, `did`, `roleSet`, `prevCID`). The envelope's payload is byte-compared against the canonical serialization of all seven — `{ nonce, audience, did, roleSet, prevCID, publicKeyMultibase, timestamp }` — in that order. `audience` is this API's own host; `timestamp` is a whole-second UTC instant within five minutes of now. Every refusal but a bad signature leaves the code live to retry; re-presenting the same key is admitted, a different key is refused. Full member table: https://docs.dfos.com/docs/api/key-proof. */ post: operations["keyProof.present"]; delete?: never; @@ -767,17 +687,7 @@ export interface paths { }; /** * Poll a key-add ceremony - * @description Where the ceremony behind a code has got to. **Takes no authentication** — the code is the capability, and this is scoped to nothing else. - * - * This is the leg a CLI waits on after presenting: the identity's owner has to adopt or reject in their browser, and until they do the answer is `presented`. - * - * Watch `stale`. While `presented`, it turns true if another writer moves the identity's chain head — the stored envelope is bound to the head it was signed against, so it can no longer be adopted as-is. Re-resolve the code, sign a fresh envelope for the **same key** with the new `prevCID`, and present it again; the owner's approval carries across, and their browser retries the adoption on its own. - * - * On `adopted`, `onAdopted` carries the DID, the chain-local key id, and the CID of the operation that added it — enough to fetch the chain from the relay the resolution named and file the key locally without asking anything else. **It is served only while the ceremony's ten minutes are still running.** Past `expiresAt` the answer narrows to the bare `status`: a spent code must not stay a permanent handle on somebody's identity, and a CLI polling its own ceremony has the receipt seconds after presenting — long before that matters. - * - * `rejected` is not an error: the person declined a key they did not recognize, nothing was added, and the honest thing to tell your user is exactly that. - * - * A code this deployment never issued is refused. A code it DID issue answers its real state, including after that state is terminal — which a caller needs, since the whole point of polling is to learn how the ceremony ended. So this endpoint does distinguish a real spent code from a fabricated one. That is one bit, it is deliberate, and it is what the rate limit is in front of; the 8-character code space is not a secret this endpoint is defending, the nonce and the signature are. + * @description Where the ceremony behind a code has got to. Poll after presenting: the answer stays `presented` until the identity's owner adopts or rejects in the DFOS app, and `rejected` is not an error. If `stale` turns true, present a fresh envelope for the same key; the owner's approval carries across. `onAdopted` is served only until `expiresAt`. */ get: operations["keyProof.status"]; put?: never; @@ -797,7 +707,7 @@ export interface paths { }; /** * Get protocol discovery info - * @description Return the protocol discovery document for this API deployment: the DID method and specification, plus URL templates for the relay proof plane and indexes. + * @description The protocol discovery document for this deployment: DID method, spec URL, relay URL, and relay endpoint templates. */ get: operations["protocol.getProtocolInfo"]; put?: never; @@ -814,40 +724,49 @@ export interface components { schemas: { PublicPaginationInput: { /** - * @description Maximum number of items per page (default: 20, max: 100) + * @description Items per page. Default 20, max 100. * @example 20 */ limit?: number; /** - * @description Opaque cursor for forward pagination — fetch items after this cursor. Treat as an opaque token: pass back a `nextCursor`/`previousCursor` verbatim; do not parse or construct it (its encoding carries no stability guarantee). - * @example eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9 + * @description Opaque cursor for forward pagination. Pass back a `nextCursor` verbatim; never parse it. + * @example eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9 */ after?: string; /** - * @description Opaque cursor for backward pagination — fetch items before this cursor. Treat as an opaque token (see `after`). - * @example eyJpZCI6InBvc3RfNmVuY2M0YWtyemUyYWg5a250emQ5dCJ9 + * @description Opaque cursor for backward pagination. Pass back a `previousCursor` verbatim. + * @example eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9 */ before?: string; }; /** - * @description Protocol DID — `did:dfos:` + 31 chain-derived chars (40 total). The ONLY DID universe the public wire speaks; never the internal 22-char OLTP identifier. - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @description Protocol DID — `did:dfos:` plus 31 chain-derived characters (40 total). The only DID form this API speaks; a DID of any other shape will not resolve. + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ ProtocolDid: string; - /** @description A resolved media object */ + /** + * @description A resolved media object + * @example { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * } + */ PublicMediaOutput: { - /** - * @description Media object id — the value an `attachment://` inline body token references. Use it to associate a body token with its entry in `bodyMedia` (required when a body carries more than one inline media item). Media ids already appear verbatim in the body markdown, so this exposes nothing new. - * @example media_6encc4akrze2ah9kntzd9t - */ + /** @description Media object id — what an `attachment://` inline body token references. Use it to match a token to its `bodyMedia` entry. */ id: string; - /** @description Original uploaded filename of the media object */ + /** @description Original uploaded filename */ filename: string; - /** @description Resolved URL for the media object. A permanent, unsigned imgix CDN URL for public images; a time-limited SIGNED URL for private media (audio/video/files). When signed, `urlExpiresAt` is present — never persist a signed URL, re-fetch the post for a fresh one. */ + /** @description Resolved URL for the media object. Permanent for public images; a time-limited signed URL for private media, which carries `urlExpiresAt`. */ url: string; /** @description MIME type of the media object */ contentType: string; - /** @description Size of the media object in bytes (absent until upload is finalized) */ + /** @description Size in bytes, absent until the upload is finalized */ contentLength?: number; /** @description Pixel width (images/video) */ width?: number; @@ -855,60 +774,105 @@ export interface components { height?: number; /** @description Blur-hash placeholder string for progressive image loading */ blurHash?: string; - /** @description Uploader-authored caption / alt text, when present */ + /** @description Uploader-authored caption or alt text */ alt?: string; /** @description Playback length in milliseconds (audio/video only) */ durationMs?: number; - /** @description CDN URL of an extracted poster frame / cover art (audio/video only) */ + /** @description CDN URL of an extracted poster frame or cover art (audio/video only) */ posterUrl?: string; - /** @description Streamable MP4 rendition URL (audio/video only). A permanent CDN URL for public media; a time-limited SIGNED URL for private media (in which case `urlExpiresAt` is present). */ + /** @description Streamable MP4 rendition URL (audio/video only). Permanent for public media; a time-limited signed URL for private media. */ playbackUrl?: string; /** * Format: date-time - * @description When the SIGNED `url` / `playbackUrl` expire (ISO 8601 UTC). PRESENT iff those URLs are time-limited signed URLs (private media); ABSENT means they are permanent (public images). Never persist a signed URL — re-fetch the post to obtain fresh ones. + * @description When the signed `url` and `playbackUrl` expire (ISO 8601 UTC). Present only for private media. Signed URLs are ephemeral — re-fetch rather than persisting them. */ urlExpiresAt?: string; /** @description Amplitude overview for audio — up to 200 integers, each 0–100 */ waveformPeaks?: number[]; }; - /** @description Public author identity */ + /** + * @description Public author identity + * @example { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * } + */ PublicAuthorOutput: { did: components["schemas"]["ProtocolDid"]; /** @description Author display name */ displayName?: string; /** @description Author username */ username?: string; - /** @description Resolved public CDN URL for the author avatar, when present */ + /** @description Public CDN URL for the author avatar */ avatarUrl?: string; }; - /** @description A public space profile and site configuration */ + /** + * @description A public space profile and site configuration + * @example { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "headerUrl": "https://dfos.imgix.net/media/public/dv6kef3nahvh4anecth2zt-bbgg.png", + * "verifiedDomain": "dfos.com", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "protocol": { + * "headOpCid": "bafyreib4dybomqzwea3ottaeflaapuj4vlci6zpuelb54l3o3lqm4zntli", + * "profile": { + * "contentId": "e7nr6ta3ddhvnvdz6aav99anf2tna3r", + * "headOpCid": "bafyreiejihfwzybgtg5b3pgahumbzpqkzp6h4m3etedo6x27xbenjhpvbu" + * } + * }, + * "joinMode": "open", + * "siteMode": "posts", + * "subscribeEnabled": true, + * "memberCountSummary": "thousands of members", + * "privatePostCountSummary": "hundreds of private posts", + * "chatMessageCountSummary": "thousands of chat messages", + * "eventCountSummary": "about a dozen events", + * "mediaUploadCountSummary": "thousands of media uploads", + * "createdAt": "2025-12-12T02:20:55.777Z" + * } + */ SpaceOutput: { /** @description Space short ID */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical. */ + /** @description Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical. */ domain: string; /** @description Space display name */ displayName: string | null; /** @description Space description */ description: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; - /** @description Resolved public CDN URL for the space header/wallpaper image, or null */ + /** @description Public CDN URL for the space header image, or null */ headerUrl: string | null; - /** @description An external domain this space has proven control of via origin binding, or null. NOT the same thing as `domain`, which is the space's DFOS subdomain. The claim is a `DfosOrigin` entry on the space's identity chain, and the domain independently publishes the space's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the space claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction. */ + /** @description A domain this space has proven control of through origin binding, or null. Distinct from `domain`, the DFOS subdomain. */ verifiedDomain: string | null; /** @description Ordered space profile links (may be empty) */ links: components["schemas"]["PublicProfileLinkOutput"][]; - /** @description Protocol proof-plane handles for this space's identity and optional profile content chain. Present iff the identity chain exists; absent for legacy identities without a chain. Combine with `GET /protocol` to fetch and verify the chains from the relay. */ + /** @description Proof-plane handles for this space's identity chain and optional profile chain. Absent for legacy identities without one; resolve them via `GET /protocol`. */ protocol?: { /** @description The identity chain's current head operation CID */ headOpCid: string; - /** @description Protocol proof-plane handles for the space profile content chain. Present iff the space has a non-empty profile chain; absent when the profile is empty or its chain has not been created. */ + /** @description Proof-plane handles for the space profile content chain, absent when there is none. */ profile?: { - /** @description The space profile's protocol content-chain id */ + /** @description The space profile's content-chain id */ contentId: string; - /** @description The space profile content chain's current head operation CID */ + /** @description The profile content chain's current head operation CID */ headOpCid: string; }; }; @@ -918,21 +882,21 @@ export interface components { * @enum {string} */ siteMode: "join" | "posts"; - /** @description Whether the public email-subscribe form is enabled for this space */ + /** @description Whether the public email-subscribe form is enabled */ subscribeEnabled: boolean; - /** @description Worded member-count summary (e.g. "a few dozen members"). Public surfaces deliberately avoid exact counts. */ + /** @description Worded member-count summary (e.g. "a few dozen members"), never exact. */ memberCountSummary: string; - /** @description Worded summary of how many posts sit BEHIND the door — live posts this API cannot serve anonymously (e.g. "a few dozen private posts", "no private posts"). The exact complement of the space's public posts, which the posts listing enumerates precisely as `totalCount`. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent. */ + /** @description Worded summary of the posts this API does not serve anonymously (e.g. "a few dozen private posts"). Approximate scale, not an exact count; render it, never parse it. */ privatePostCountSummary?: string; - /** @description Worded summary of how many chat messages the space's channels hold (e.g. "hundreds of chat messages", "no chat messages"). Covers every channel bound to the space, including its private ones. Direct and group messages are never space-bound and never counted. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent. */ + /** @description Worded summary of chat messages across the space's channels, private ones included; direct and group messages never count. Approximate scale, not an exact count; render it, never parse it. */ chatMessageCountSummary?: string; - /** @description Worded summary of how many published events the space has scheduled (e.g. "a few events", "no events"). A recurring series counts once. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent. */ + /** @description Worded summary of the space's published events; a recurring series counts once. Approximate scale, not an exact count; render it, never parse it. */ eventCountSummary?: string; - /** @description Worded summary of how many media files have been uploaded to the space (e.g. "about a hundred media uploads", "no media uploads"). Covers ALL of the space's media, including images it serves publicly — the avatar and header on this response are themselves uploads and are counted. Approximate worded scale of the space's interior, never an exact count. An opaque display string from an open set — render it, never parse it. Viewer-independent. */ + /** @description Worded summary of the space's media uploads, publicly served ones like the avatar included. Approximate scale, not an exact count; render it, never parse it. */ mediaUploadCountSummary?: string; - /** @description The normalized discovery category the space filed itself under. ABSENT (not null) when the space has not filed one, matching the `protocol` block convention. The same token the discovery listing emits and its `category` filter accepts. */ + /** @description The discovery category the space filed itself under, absent when it has filed none. The same token the `category` filter accepts. */ category?: string; - /** @description The questions an applicant answers when joining, present ONLY for a public application-mode space (`joinMode: "application"`) that has configured at least one question. Absent for every other space. Viewer-independent — the same list for all callers. */ + /** @description The questions an applicant answers when joining. Present only for an application-mode space that configured at least one. */ applicationQuestions?: components["schemas"]["PublicApplicationQuestionOutput"][]; /** * Format: date-time @@ -940,27 +904,44 @@ export interface components { */ createdAt: string; }; - /** @description A public space application question */ + /** + * @description A public space application question + * @example { + * "question": "What are you working on, and what brings you here?", + * "isRequired": true + * } + */ PublicApplicationQuestionOutput: { /** @description The question text an applicant answers */ question: string; - /** @description Whether answering this question is required to apply */ + /** @description Whether an answer is required to apply */ isRequired: boolean; }; - /** @description Compact space context for a post response (header + join/sign-in CTA) */ + /** + * @description Compact space context for a post response (header + join/sign-in CTA) + * @example { + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "domain": "home", + * "joinMode": "open", + * "subscribeEnabled": true, + * "memberCountSummary": "thousands of members" + * } + */ PublicSpaceCtaOutput: { /** @description Space display name */ displayName: string | null; /** @description Space description */ description: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias. */ + /** @description Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias. */ domain: string; joinMode: components["schemas"]["PublicSpaceJoinMode"]; - /** @description Whether the public email-subscribe form is enabled for this space */ + /** @description Whether the public email-subscribe form is enabled */ subscribeEnabled: boolean; - /** @description Worded member-count summary (e.g. "a few dozen members"). Public surfaces deliberately avoid exact counts. */ + /** @description Worded member-count summary (e.g. "a few dozen members"), never exact. */ memberCountSummary: string; }; /** @@ -968,76 +949,157 @@ export interface components { * @enum {string} */ PublicSpaceJoinMode: "closed" | "open" | "application" | "allowlist"; - /** @description A public space in the discovery listing */ + /** + * @description A public space in the discovery listing + * @example { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "verifiedDomain": "dfos.com", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "memberCountSummary": "thousands of members" + * } + */ SpaceDiscoveryItemOutput: { /** @description Space short ID */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical. */ + /** @description Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical. */ domain: string; /** @description Space display name */ displayName: string | null; /** @description Space description */ description: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; - /** @description An external domain this space has proven control of via origin binding, or null. NOT the same thing as `domain`, which is the space's DFOS subdomain. The claim is a `DfosOrigin` entry on the space's identity chain, and the domain independently publishes the space's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the space claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction. */ + /** @description A domain this space has proven control of through origin binding, or null. Distinct from `domain`, the DFOS subdomain. */ verifiedDomain: string | null; /** @description Ordered space profile links (may be empty) */ links: components["schemas"]["PublicProfileLinkOutput"][]; - /** @description Worded member-count summary (e.g. "a few dozen members"). Public surfaces deliberately avoid exact counts. */ + /** @description Worded member-count summary (e.g. "a few dozen members"), never exact. */ memberCountSummary: string; - /** @description The normalized discovery category the space filed itself under. ABSENT (not null) when the space has not filed one, matching the `protocol` block convention. A storage token, not display copy — pass it back verbatim as the `category` filter. */ + /** @description The discovery category the space filed itself under, absent when it has filed none. Pass it back verbatim as the `category` filter. */ category?: string; }; - /** @description A cursor-paginated page of public spaces */ + /** + * @description A cursor-paginated page of public spaces + * @example { + * "items": [ + * { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "verifiedDomain": "dfos.com", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "memberCountSummary": "thousands of members" + * }, + * { + * "id": "space_z94a849d9kdftfvv3n9hn7", + * "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + * "domain": "rakowwwski", + * "displayName": "POPULAR", + * "description": "Popular is a space we share our creative process and research practices. We get to know our work to get to know ourselves.", + * "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + * "verifiedDomain": null, + * "links": [], + * "memberCountSummary": "about a dozen members" + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null, + * "totalCount": null + * } + */ SpaceDiscoveryPageOutput: { /** @description Page of public spaces */ items: components["schemas"]["SpaceDiscoveryItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A post in a space public feed */ + /** + * @description A post in a space public feed + * @example { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "isPinned": false, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z", + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * } + * } + */ PublicPostListItemOutput: { - /** - * @description Post ID (use as `{postId}` on the single-post route) - * @example post_6encc4akrze2ah9kntzd9t - */ + /** @description Post ID. Use as `{postId}` on the single-post route. */ id: string; - /** - * @description Server-generated URL slug for the post - * @example building-a-more-generous-internet - */ + /** @description URL slug for the post */ slug: string; format: components["schemas"]["PublicPostFormat"]; - /** - * @description Post title - * @example Building a more generous internet - */ + /** @description Post title */ title: string | null; - /** - * @description Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content. - * @example Building a more generous internet - */ + /** @description The title, else a short markdown-stripped excerpt of the above-fold body, else null. */ displayTitle: string | null; - /** - * @description Truncated plain-text preview of the post body (markdown-stripped) - * @example A field guide to shared infrastructure for creative communities. - */ + /** @description Plain-text preview of the body, markdown-stripped */ excerpt: string | null; /** @description Post author */ author: components["schemas"]["PublicAuthorOutput"] | null; /** @description Post cover image, when present */ cover?: components["schemas"]["PublicMediaOutput"]; - /** @description Number of upvotes on the post */ + /** @description Number of upvotes */ upvoteCount: number; - /** @description Number of comments on the post */ + /** @description Number of comments */ commentCount: number; - /** @description Whether the space has pinned this post. The feed is ordered purely by recency (newest first); a client may use this flag to surface pinned posts itself. */ + /** @description Whether the space has pinned this post. The feed is ordered by recency alone. */ isPinned: boolean; /** * Format: date-time @@ -1049,9 +1111,9 @@ export interface components { * @description When the post was last updated (ISO 8601 UTC) */ updatedAt: string; - /** @description Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path. Same value as on the single-post response. */ + /** @description Canonical public web permalink for the post */ canonicalUri?: string; - /** @description Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay. */ + /** @description Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only. */ protocol?: { /** @description The post's protocol content-chain id */ contentId: string; @@ -1061,43 +1123,86 @@ export interface components { viewer?: components["schemas"]["PublicPostViewerOutput"]; }; /** - * @description Post format: `short-post` (a short, typically untitled note) or `long-post` (a titled article). Open enum — treat an unrecognized value as a generic post. + * @description `short-post` (untitled note) or `long-post` (titled article). Open enum — render an unknown value as a generic post. * @example long-post * @enum {string} */ PublicPostFormat: "short-post" | "long-post"; - /** @description Full public post content */ + /** + * @description Full public post content + * @example { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "body": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\n\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.", + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "attachments": [], + * "bodyMedia": [ + * { + * "id": "media_rn2926ahdrnra6t4erc862", + * "filename": "2026-09-04-siwd-consent-composite-equal-height.png", + * "url": "https://dfos.imgix.net/media/public/rn2926ahdrnra6t4erc862-2026-09-04-siwd-consent-composite-equal-height.png", + * "contentType": "image/png", + * "contentLength": 273173, + * "width": 2116, + * "height": 1160, + * "blurHash": "eRS6Pl%Mt7%May~qWBWBt7WBxuayWBofj[%Mxut7Rjj[-;WBWBt7ay" + * } + * ], + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "topics": [ + * { + * "id": "topic_6c2efd472dvt8rf9k4ftcc", + * "name": "DFOS Blog" + * } + * ], + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z" + * } + */ PublicPostOutput: { /** @description Post ID */ id: string; - /** - * @description Server-generated URL slug for the post - * @example building-a-more-generous-internet - */ + /** @description URL slug for the post */ slug: string; format: components["schemas"]["PublicPostFormat"]; - /** - * @description Post title - * @example Building a more generous internet - */ + /** @description Post title */ title: string | null; - /** - * @description Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content. - * @example Building a more generous internet - */ + /** @description The title, else a short markdown-stripped excerpt of the above-fold body, else null. */ displayTitle: string | null; - /** - * @description Truncated plain-text preview of the post body (markdown-stripped) - * @example A field guide to shared infrastructure for creative communities. - */ + /** @description Plain-text preview of the body, markdown-stripped */ excerpt: string | null; /** @description Post content body (markdown) */ body: string | null; /** @description Post cover image */ cover?: components["schemas"]["PublicMediaOutput"]; - /** @description Post attachments. Includes private media (audio/video/files), which carry time-limited signed `url` / `playbackUrl` values (see `urlExpiresAt`). */ + /** @description Post attachments. Private media carries time-limited signed `url` / `playbackUrl` values; see `urlExpiresAt`. */ attachments: components["schemas"]["PublicMediaOutput"][]; - /** @description Media objects referenced inline in the post body markdown. The body carries `attachment://` tokens; resolve each against this array by its `id`. Always present (empty array when none). */ + /** @description Media referenced inline in the body via `attachment://` tokens; resolve each by `id`. Empty when none. */ bodyMedia: components["schemas"]["PublicMediaOutput"][]; /** @description Post author */ author: components["schemas"]["PublicAuthorOutput"] | null; @@ -1108,55 +1213,52 @@ export interface components { /** @description Topic name */ name: string; }[]; - /** @description Link cards extracted from URLs in the post body, in appearance order. Absent when the post has no links. */ + /** @description Link cards extracted from the body, in appearance order. Absent when there are none. */ links?: { /** - * @description The original shared URL represented by this link card - * @example https://example.com/field-notes + * @description The shared URL + * @example https://dfos.com/ */ url: string; /** - * @description Resolved page title, or null when unavailable - * @example Field Notes + * @description Resolved page title, or null + * @example Dark Forest OS */ title: string | null; - /** @description Resolved page description, or null when unavailable */ + /** @description Resolved page description, or null */ description: string | null; - /** - * @description Resolved site name, or null when unavailable - * @example Example - */ + /** @description Resolved site name, or null */ siteName: string | null; - /** @description Permanent public CDN URL for the cached link preview image, when available */ + /** @description Permanent CDN URL for the cached preview image, when available */ imageUrl?: string; - /** @description Permanent public CDN URL for the cached site favicon, when available */ + /** @description Permanent CDN URL for the cached site favicon, when available */ faviconUrl?: string; /** @description Zero-based order of first appearance in the post body */ sortOrder: number; }[]; - /** @description Internal entity links derived from the served (folded) body. Viewer-independent; post and space labels resolve at the anonymous/public floor, while events must be published and space-visible in a space whose public profile is enabled. Omitted when no internal links resolve. */ + /** @description Internal entity links resolved from the served body. Absent when none resolve. */ entityLinks?: { - /** @description Normalized absolute internal URL as it appears in the served post body */ + /** @description Absolute internal URL as it appears in the served body */ url: string; - /** @description Viewer-independent display label for the entity */ + /** @description Display label for the entity */ label: string; - /** @description Entity kind. Current values: `post`, `space`, or `event`. Open enum — clients must render unknown values as ordinary links. */ + /** @description Entity kind: `post`, `space`, or `event`. Open enum — render an unknown value as an ordinary link. */ kind: string; }[]; - /** @description True when the body was truncated at a fold marker — only the above-fold teaser is present. An anonymous read is always the non-reader projection, so a folded post is always truncated for it; a member projection carries the full body when the caller genuinely reads the post. Absent when the post has no fold. */ + /** @description True when only the above-fold teaser is present. Absent when the post has no fold. */ folded?: boolean; - /** @description Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path. */ + /** @description Canonical public web permalink: the space public host plus `/post/{slug}-{id}`. */ canonicalUri?: string; - /** @description Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay. */ + /** @description Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only. */ protocol?: { /** @description The post's protocol content-chain id */ contentId: string; /** @description The post content chain's current head operation CID */ headOpCid: string; }; - /** @description Number of upvotes on the post */ + /** @description Number of upvotes */ upvoteCount: number; - /** @description Number of comments on the post */ + /** @description Number of comments */ commentCount: number; /** * Format: date-time @@ -1170,71 +1272,188 @@ export interface components { updatedAt: string; viewer?: components["schemas"]["PublicPostViewerOutput"]; }; - /** @description A cursor-paginated page of public posts */ + /** + * @description A cursor-paginated page of public posts + * @example { + * "items": [ + * { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "isPinned": false, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z", + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * } + * }, + * { + * "id": "post_n7hhz3hv6n4h8rde8c2ed9", + * "slug": "the-dangers-of-self-expression", + * "format": "long-post", + * "title": "The dangers of self-expression", + * "displayTitle": "The dangers of self-expression", + * "excerpt": "The thread running through this week’s issue: what happens when more and more of life becomes something to optimize, perform, and monetize? This is why we’re making DFOS: a different world becomes...", + * "author": { + * "did": "did:dfos:27z77a722tacvrne3k7f6k6rzd9evkc", + * "displayName": "Yancey", + * "username": "ystrickler", + * "avatarUrl": "https://dfos.imgix.net/media/public/v3rr6d9z7f2dad4hhde8cd-yancey-headshot.jpeg" + * }, + * "cover": { + * "id": "media_eet4k4rhdt3nt4n4kervvr", + * "filename": "cover.png", + * "url": "https://dfos.imgix.net/media/public/eet4k4rhdt3nt4n4kervvr-cover.png", + * "contentType": "image/png", + * "contentLength": 261229, + * "width": 1051, + * "height": 720, + * "blurHash": "evQ,z6tQ}x%3A8xuj[oMj[ax=ij[EvWnw6%3j[bFj[afozj[Rjayt7" + * }, + * "upvoteCount": 20, + * "commentCount": 16, + * "isPinned": false, + * "publishedAt": "2026-09-02T14:54:48.551Z", + * "updatedAt": "2026-09-02T14:54:48.725Z", + * "canonicalUri": "https://home.dfos.com/post/the-dangers-of-self-expression-n7hhz3hv6n4h8rde8c2ed9", + * "protocol": { + * "contentId": "en8ne3nzft9rrte866v2423rn26r3er", + * "headOpCid": "bafyreicmvn5tzg6vo5oey2itz3pacvmeemfggwecc62rdbnercnb6lggrm" + * } + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "totalCount": 10 + * } + */ PublicPostPageOutput: { /** @description Page of public posts */ items: components["schemas"]["PublicPostListItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description The authenticated caller's own relationship to this post. Present ONLY on a member projection — a request that presented a credential (or identity proof) reaching this space. Absent on every anonymous response. */ + /** + * @description The caller's own relationship to this post. Present only on a member projection. + * @example { + * "upvoted": true + * } + */ PublicPostViewerOutput: { /** @description Whether the authenticated caller has upvoted this post. */ upvoted: boolean; }; - /** @description The space a feed item belongs to */ + /** + * @description The space a feed item belongs to + * @example { + * "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "name": "DFOS", + * "url": "https://home.dfos.com" + * } + */ FeedSpaceRefOutput: { /** - * @description The space's protocol DID — the canonical, stable identifier. Pass it back as `{space}` on any space-addressed route. - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @description The space's protocol DID. Pass it back as `{space}` on any space-addressed route. + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ id: string; - /** @description Space display name, or null when it has none */ + /** @description Space display name, or null */ name: string | null; - /** @description The space's public web address (custom domain, else the `space-{id}` subdomain). Present for every space; a space with no public profile still has a canonical address, which is simply not anonymously reachable. */ + /** @description The space's public web address: its custom domain, else the `space-{id}` subdomain. Present for private spaces too, where the address is real but does not resolve anonymously. */ url: string; }; - /** @description A post in the cross-space feed, with its space */ + /** + * @description A post in the cross-space feed, with its space + * @example { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "isPinned": false, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z", + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * }, + * "viewer": { + * "upvoted": true + * }, + * "space": { + * "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "name": "DFOS", + * "url": "https://home.dfos.com" + * } + * } + */ FeedItemOutput: { - /** - * @description Post ID (use as `{postId}` on the single-post route) - * @example post_6encc4akrze2ah9kntzd9t - */ + /** @description Post ID. Use as `{postId}` on the single-post route. */ id: string; - /** - * @description Server-generated URL slug for the post - * @example building-a-more-generous-internet - */ + /** @description URL slug for the post */ slug: string; format: components["schemas"]["PublicPostFormat"]; - /** - * @description Post title - * @example Building a more generous internet - */ + /** @description Post title */ title: string | null; - /** - * @description Server-derived display label: the title when present, else a short markdown-stripped excerpt of the (above-fold) body, else null. Standardizes the untitled-post fallback; never derived from below-fold content. - * @example Building a more generous internet - */ + /** @description The title, else a short markdown-stripped excerpt of the above-fold body, else null. */ displayTitle: string | null; - /** - * @description Truncated plain-text preview of the post body (markdown-stripped) - * @example A field guide to shared infrastructure for creative communities. - */ + /** @description Plain-text preview of the body, markdown-stripped */ excerpt: string | null; /** @description Post author */ author: components["schemas"]["PublicAuthorOutput"] | null; /** @description Post cover image, when present */ cover?: components["schemas"]["PublicMediaOutput"]; - /** @description Number of upvotes on the post */ + /** @description Number of upvotes */ upvoteCount: number; - /** @description Number of comments on the post */ + /** @description Number of comments */ commentCount: number; - /** @description Whether the space has pinned this post. The feed is ordered purely by recency (newest first); a client may use this flag to surface pinned posts itself. */ + /** @description Whether the space has pinned this post. The feed is ordered by recency alone. */ isPinned: boolean; /** * Format: date-time @@ -1246,9 +1465,9 @@ export interface components { * @description When the post was last updated (ISO 8601 UTC) */ updatedAt: string; - /** @description Canonical public web permalink for the post — the space public host (custom domain, else the `space-{id}` subdomain) plus the `/post/{slug}-{id}` path. Same value as on the single-post response. */ + /** @description Canonical public web permalink for the post */ canonicalUri?: string; - /** @description Protocol proof-plane handles for this post's content chain. Present iff the post's chain exists; absent for posts created in the last ~30 seconds (creation settlement) and legacy/ineligible posts. Combine with `GET /protocol` to fetch and verify the chain from the relay. */ + /** @description Proof-plane handles for the post's content chain. Absent briefly after creation, for legacy posts, and for members-only posts in spaces where protocol broadcast is public-only. */ protocol?: { /** @description The post's protocol content-chain id */ contentId: string; @@ -1258,31 +1477,124 @@ export interface components { viewer?: components["schemas"]["PublicPostViewerOutput"]; space: components["schemas"]["FeedSpaceRefOutput"]; }; - /** @description A cursor-paginated page of the cross-space feed */ + /** + * @description A cursor-paginated page of the cross-space feed + * @example { + * "items": [ + * { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "isPinned": false, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z", + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * }, + * "viewer": { + * "upvoted": true + * }, + * "space": { + * "id": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "name": "DFOS", + * "url": "https://home.dfos.com" + * } + * }, + * { + * "id": "post_4vfa2h8ekt3rc9n7zd6a2f", + * "slug": "notes-from-the-studio-floor", + * "format": "short-post", + * "title": null, + * "displayTitle": "Working in public means the process is the artifact.", + * "excerpt": "Working in public means the process is the artifact. This week: three false starts, one that held, and the notes we kept along the way.", + * "upvoteCount": 5, + * "commentCount": 2, + * "isPinned": false, + * "publishedAt": "2026-09-03T09:41:12.000Z", + * "updatedAt": "2026-09-03T09:41:12.000Z", + * "author": { + * "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + * "displayName": "Aron", + * "username": "aron", + * "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + * }, + * "viewer": { + * "upvoted": false + * }, + * "space": { + * "id": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + * "name": "POPULAR", + * "url": "https://rakowwwski.dfos.com" + * } + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9" + * } + */ FeedPageOutput: { /** @description Page of cross-space feed items */ items: components["schemas"]["FeedItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; /** - * @description Thread ordering. `newest` (default) — most recently active threads first, where a thread's activity is the latest of its root and its replies. `oldest` — the reverse. `top` — most upvoted first. Open enum; only send values supported by the current contract. + * @description Thread ordering. `newest` (default) sorts by a thread's latest activity, `oldest` reverses it, and `top` sorts by upvotes. Open enum. * @example newest * @enum {string} */ PublicCommentSort: "newest" | "oldest" | "top"; - /** @description A comment on a post */ + /** + * @description A comment on a post + * @example { + * "id": "comment_9rze4tk2vdc7fa38nhe6c2", + * "postId": "post_ze2kh2d47tzerkhet8348c", + * "author": { + * "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + * "displayName": "Aron", + * "username": "aron", + * "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + * }, + * "body": "The exit-key framing is the part that clicks for me: the door exists before anyone needs it.", + * "publishedAt": "2026-09-04T19:12:40.000Z", + * "activityAt": "2026-09-04T20:01:05.000Z", + * "upvoteCount": 3, + * "replyCount": 1, + * "viewer": { + * "upvoted": true + * } + * } + */ PublicCommentOutput: { - /** - * @description Comment ID. Pass it back as `parentCommentId` to walk its replies. - * @example post_6encc4akrze2ah9kntzd9t - */ + /** @description Comment ID. Pass it back as `parentCommentId` to walk its replies. */ id: string; - /** @description The ROOT post this comment belongs to — the same id the route was called with. */ + /** @description The root post this comment belongs to. */ postId: string; /** @description The comment this one replies to. Absent on a root comment. */ parentCommentId?: string; @@ -1297,38 +1609,82 @@ export interface components { publishedAt: string; /** * Format: date-time - * @description The instant this row is ORDERED by, and the value the cursor carries. On a root comment it is the thread's last activity — the later of the comment's own publication and its most recent reply — so an active thread sorts ahead of an older one under `newest`. On a reply it is the reply's own `publishedAt`. + * @description The ordering key the cursor carries. On a root comment, the later of its own publication and its most recent reply; on a reply, its own `publishedAt`. */ activityAt: string; /** @description Number of upvotes on the comment */ upvoteCount: number; /** @description Number of replies to this comment. Always 0 on a reply. */ replyCount: number; - /** @description The authenticated caller's own relationship to this comment. This route is always authenticated, so the block is always present. */ + /** @description The caller's own relationship to this comment. Always present on this route. */ viewer?: { /** @description Whether the authenticated caller has upvoted this comment. */ upvoted: boolean; }; }; - /** @description A cursor-paginated page of post comments */ + /** + * @description A cursor-paginated page of post comments + * @example { + * "items": [ + * { + * "id": "comment_9rze4tk2vdc7fa38nhe6c2", + * "postId": "post_ze2kh2d47tzerkhet8348c", + * "author": { + * "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + * "displayName": "Aron", + * "username": "aron", + * "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg" + * }, + * "body": "The exit-key framing is the part that clicks for me: the door exists before anyone needs it.", + * "publishedAt": "2026-09-04T19:12:40.000Z", + * "activityAt": "2026-09-04T20:01:05.000Z", + * "upvoteCount": 3, + * "replyCount": 1, + * "viewer": { + * "upvoted": true + * } + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 5 + * } + */ PublicCommentPageOutput: { /** @description Page of comments */ items: components["schemas"]["PublicCommentOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A comment, as returned by a write */ + /** + * @description A comment, as returned by a write + * @example { + * "id": "comment_d6ah3f9rkt2ez48vc7n4rc", + * "postId": "post_ze2kh2d47tzerkhet8348c", + * "parentCommentId": "comment_9rze4tk2vdc7fa38nhe6c2", + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "body": "Same. That and origin binding running in both directions.", + * "publishedAt": "2026-09-04T20:01:05.000Z", + * "upvoteCount": 0, + * "replyCount": 0, + * "viewer": { + * "upvoted": false + * } + * } + */ PublicCommentWriteOutput: { - /** - * @description Comment ID. Pass it back as `parentCommentId` to walk its replies. - * @example post_6encc4akrze2ah9kntzd9t - */ + /** @description Comment ID. Pass it back as `parentCommentId` to walk its replies. */ id: string; - /** @description The ROOT post this comment belongs to — the same id the route was called with. */ + /** @description The root post this comment belongs to. */ postId: string; /** @description The comment this one replies to. Absent on a root comment. */ parentCommentId?: string; @@ -1345,165 +1701,208 @@ export interface components { upvoteCount: number; /** @description Number of replies to this comment. Always 0 on a reply. */ replyCount: number; - /** @description The authenticated caller's own relationship to this comment. This route is always authenticated, so the block is always present. */ + /** @description The caller's own relationship to this comment. Always present on this route. */ viewer?: { /** @description Whether the authenticated caller has upvoted this comment. */ upvoted: boolean; }; }; - /** @description Upvote state after a toggle */ + /** + * @description Upvote state after a toggle + * @example { + * "upvoted": true, + * "upvoteCount": 15 + * } + */ PublicUpvoteStateOutput: { - /** @description Whether the granting user's upvote is on AFTER this call. `PUT` answers `true` and `DELETE` answers `false`, including when the call changed nothing. */ + /** @description Whether the caller's upvote is on after this call. `PUT` answers `true` and `DELETE` `false`, including when nothing changed. */ upvoted: boolean; - /** @description Upvote count on the post or comment after this call */ + /** @description Upvote count after this call */ upvoteCount: number; }; - /** @description Confirmation that the content was deleted */ + /** + * @description Confirmation that the content was deleted + * @example { + * "deleted": true + * } + */ PublicDeletedOutput: { /** - * @description Always `true`. A failed delete is an error status, never this body. + * @description Always `true`; a failed delete returns an error status. * @constant */ deleted: true; }; - /** @description A page in a space public page list */ + /** + * @description A page in a space's page list + * @example { + * "id": "page_c7dnfhc3zn2eh7frrc3nhc", + * "slug": "about", + * "title": "About", + * "sortOrder": 0, + * "updatedAt": "2026-07-22T13:12:02.748Z" + * } + */ PublicPageSummaryOutput: { - /** - * @description Stable page entity id. Store this identifier; the slug may change. - * @example page_6encc4akrze2ah9kntzd9t - */ + /** @description Stable page id. Prefer it to the slug. */ id: string; - /** - * @description Mutable page slug alias (accepted as `{page}` on the single-page route) - * @example about - */ + /** @description Mutable slug alias. */ slug: string; - /** - * @description Page title - * @example About this space - */ + /** @description Page title. */ title: string; - /** @description Flat ordering within the space (ascending); the list is pre-sorted */ + /** @description Ascending sort position. */ sortOrder: number; /** * Format: date-time - * @description When the page was last updated (ISO 8601 UTC) + * @description Last update (ISO 8601 UTC). */ updatedAt: string; }; - /** @description Full public page content */ + /** + * @description Full page content + * @example { + * "id": "page_c7dnfhc3zn2eh7frrc3nhc", + * "slug": "about", + * "title": "About", + * "body": "![dfos](attachment://media_9tnfvccf6t372vdnv43ftv)DFOS is a tool for making worlds of your own.\n\nA DFOS can be a community, publication, studio, institution, business, archive, scene, private internet, or something that doesn’t have a name yet.\n\nIt can be public or private. Free or paid.", + * "bodyMedia": [ + * { + * "id": "media_9tnfvccf6t372vdnv43ftv", + * "filename": "dfos.png", + * "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + * "contentType": "image/png", + * "contentLength": 7045192, + * "width": 2729, + * "height": 2129, + * "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + * } + * ], + * "sortOrder": 0, + * "updatedAt": "2026-07-22T13:12:02.748Z" + * } + */ PublicPageOutput: { - /** - * @description Stable page entity id. Store this identifier; the slug may change. - * @example page_6encc4akrze2ah9kntzd9t - */ + /** @description Stable page id. */ id: string; - /** - * @description Mutable page slug alias - * @example about - */ + /** @description Mutable slug alias. */ slug: string; - /** - * @description Page title - * @example About this space - */ + /** @description Page title. */ title: string; - /** @description Page content body (markdown). Inline media appears as `attachment://` tokens — resolve each against `bodyMedia` by id. */ + /** @description Markdown body, or null. Inline media appears as `attachment://`; resolve against `bodyMedia`. */ body: string | null; - /** @description Page cover image, when present */ + /** @description Cover image. */ cover?: components["schemas"]["PublicMediaOutput"]; - /** @description Media objects referenced inline in the page body markdown */ + /** @description Media referenced inline in `body`. */ bodyMedia?: components["schemas"]["PublicMediaOutput"][]; - /** @description Flat ordering within the space (ascending) */ + /** @description Ascending sort position. */ sortOrder: number; /** * Format: date-time - * @description When the page was last updated (ISO 8601 UTC) + * @description Last update (ISO 8601 UTC). */ updatedAt: string; }; - /** @description A cursor-paginated page of public pages */ + /** + * @description A cursor-paginated list of pages + * @example { + * "items": [ + * { + * "id": "page_c7dnfhc3zn2eh7frrc3nhc", + * "slug": "about", + * "title": "About", + * "sortOrder": 0, + * "updatedAt": "2026-07-22T13:12:02.748Z" + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 1 + * } + */ PublicPagePageOutput: { /** @description Page of public pages */ items: components["schemas"]["PublicPageSummaryOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A public topic */ + /** + * @description A public topic + * @example { + * "id": "topic_6c2efd472dvt8rf9k4ftcc", + * "name": "DFOS Blog", + * "description": "Announcements, product notes, and essays from the DFOS team." + * } + */ PublicTopicOutput: { - /** - * @description Topic ID (use as the `topic` filter on the posts listing) - * @example topic_6encc4akrze2ah9kntzd9t - */ + /** @description Topic ID — pass it as the `topic` filter on the posts listing */ id: string; - /** - * @description Topic name - * @example Announcements - */ + /** @description Topic name */ name: string; - /** - * @description Topic description - * @example News and updates from the community. - */ + /** @description Topic description */ description: string | null; }; /** - * @description What the event is: `attend` (people show up — a session, a call, a party) or `update` (the space will publish something on this date). Open enum — treat an unrecognized value as a generic event. + * @description `attend` (people show up) or `update` (the space publishes something that day). Open enum. * @example attend * @enum {string} */ PublicEventType: "update" | "attend"; /** - * @description How an `attend` event happens: `call` (video/audio call), `place` (a physical location), or `chat` (in the space itself). Presentational only, and null on `update` events. Open enum — treat an unrecognized value as unspecified. + * @description How an `attend` event happens: `call` (video or audio), `place` (a physical location), or `chat` (in the space itself). Presentational only, and null on `update` events. Open enum. * @example place * @enum {string} */ PublicEventMode: "call" | "place" | "chat"; - /** @description The space an item belongs to */ + /** + * @description The space an item belongs to + * @example { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + */ PublicSpaceRefOutput: { - /** - * @description Space short ID — canonical and stable. - * @example space_6encc4akrze2ah9kntzd9t - */ + /** @description Space short ID — canonical and stable. */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `id` and `did` are canonical. */ + /** @description Effective subdomain (custom domain if set, else `space-{id}`). A mutable alias; `id` and `did` are canonical. */ domain: string; /** @description Space display name */ displayName: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; }; - /** @description A purchasable price tier for a public product */ + /** + * @description A purchasable price tier for a public product + * @example { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + */ PublicProductPriceOutput: { - /** - * @description Stable price-tier id. Identifies which tier a purchase is for when a product offers more than one. - * @example pprice_6encc4akrze2ah9kntzd9t - */ + /** @description Stable price-tier id. Names which tier a purchase is for. */ id: string; /** - * @description Pricing model. `one_time` is a fixed price; `pwyw` lets the buyer choose an amount at or above the floor; `recurring` bills on the stated `interval`. Open enum — tolerate unrecognized values. + * @description Pricing model. `one_time` is a fixed price, `pwyw` a buyer-chosen amount at or above the floor, `recurring` bills on `interval`. Open enum. * @enum {string} */ type: "one_time" | "pwyw" | "recurring"; - /** - * @description For `one_time`, the price. For `pwyw`, the MINIMUM the buyer may pay (0 means a free floor). - * @example 1000 - */ + /** @description For `one_time`, the price. For `pwyw`, the minimum the buyer may pay (0 is a free floor). */ amountCents: number; /** - * @description Suggested amount for a `pwyw` price, to prefill a buyer amount input. Absent when there is no suggestion, and always absent for `one_time`. + * @description Amount to prefill for a `pwyw` price. Absent for other price types and when there is no suggestion. * @example 1500 */ suggestedCents?: number; - /** - * @description ISO 4217 currency code, lowercase - * @example usd - */ + /** @description ISO 4217 currency code, lowercase */ currency: string; /** * @description Billing cadence for a `recurring` price. Absent for `one_time` and `pwyw`. @@ -1511,349 +1910,634 @@ export interface components { */ interval?: "month" | "year"; /** - * @description Number of `interval` units between billings (1 = every month/year). Always present alongside `interval`; absent for `one_time` and `pwyw`. + * @description Number of `interval` units between billings. Present with `interval`, absent otherwise. * @example 1 */ intervalCount?: number; }; - /** @description A ticket on sale for a public event */ + /** + * @description A ticket on sale + * @example { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "isSoldOut": false, + * "purchasable": true + * } + */ PublicEventTicketOutput: { - /** - * @description Store product id — the same `id` the products routes return. Stable; store this rather than the slug. - * @example sprod_6encc4akrze2ah9kntzd9t - */ + /** @description Store product id. Prefer it to the slug. */ id: string; /** - * @description The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example softr-ticket + * @description The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet-digital */ slug: string; - /** - * @description Ticket name - * @example General admission - */ + /** @description Ticket name. */ name: string; - /** @description ACTIVE price tiers for this ticket — the identical shape and the identical selection the products routes emit for the same product (archived tiers never appear). One-time and pay-what-you-want only; a ticket never carries a recurring price. */ + /** @description Active price tiers, as the products routes emit them. One-time and pay-what-you-want only. */ prices: components["schemas"]["PublicProductPriceOutput"][]; - /** @description True when this ticket had limited capacity and it is exhausted. Advisory — availability is re-checked at purchase. */ + /** @description True when limited capacity is exhausted. Re-checked at purchase. */ isSoldOut: boolean; - /** @description Whether a purchase can be STARTED right now — false when the platform payment rail is paused or the ticket has no completable price tier. Render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message. Same meaning, and the same derivation, as `purchasable` on the products routes. */ + /** @description Whether a purchase can be started now, independent of `isSoldOut`. False when the payment rail is paused or no price tier is completable. */ purchasable: boolean; }; - /** @description One occurrence of a public event */ + /** + * @description One occurrence of a public event + * @example { + * "id": "evt_ehrrkh8thkkdh6c383zkf9", + * "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + * "recurrenceId": "2026-09-09", + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * }, + * "title": "DFOS product weekly", + * "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + * "location": null, + * "type": "update", + * "mode": null, + * "url": null, + * "isAllDay": true, + * "startsAt": null, + * "endsAt": null, + * "startDate": "2026-09-09", + * "endDate": null, + * "localStart": null, + * "timeZone": null, + * "interestCount": 0, + * "requiresTicket": false, + * "tickets": [], + * "cover": { + * "id": "media_r22avnnrfch9ee62nhzkcn", + * "filename": "cover.png", + * "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + * "contentType": "image/png", + * "contentLength": 114085, + * "width": 3000, + * "height": 2100, + * "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + * } + * } + */ PublicEventOutput: { - /** - * @description Event ID. Identifies the EVENT; a recurring event yields one item per occurrence, so use `(seriesId, recurrenceId)` to address a specific one. - * @example evt_6encc4akrze2ah9kntzd9t - */ + /** @description Event id. A recurring event yields one item per occurrence; address one with `(seriesId, recurrenceId)`. */ id: string; - /** - * @description The series this occurrence belongs to — the same value as `id` today. Half of the occurrence identity pair. - * @example evt_6encc4akrze2ah9kntzd9t - */ + /** @description The series this occurrence belongs to; same value as `id` today. */ seriesId: string; - /** - * @description RFC 5545 RECURRENCE-ID: this occurrence's stable identity within its series — its local wall-time (`YYYY-MM-DDTHH:MM:SS`) or, for an all-day event, its date (`YYYY-MM-DD`). Stable across DST; never identify an occurrence by its resolved UTC instant. - * @example 2026-08-15T19:00:00 - */ + /** @description This occurrence's identity within its series (RFC 5545 RECURRENCE-ID): local wall-time `YYYY-MM-DDTHH:MM:SS`, or `YYYY-MM-DD` when all-day. Stable across DST. */ recurrenceId: string; space: components["schemas"]["PublicSpaceRefOutput"]; - /** - * @description Event title - * @example Listening session - */ + /** @description Event title. */ title: string; - /** @description Event description (plain text), or null */ + /** @description Plain-text description, or null. */ description: string | null; - /** @description Free-text location as the organizer wrote it, or null */ + /** @description Location as the organizer wrote it, or null. */ location: string | null; type: components["schemas"]["PublicEventType"]; - /** @description Attend-event flavor; null when unspecified or on an `update` event. */ + /** @description Attend-event flavor. Null when unspecified or on an `update` event. */ mode: components["schemas"]["PublicEventMode"] | null; - /** @description Waypoint URL the organizer attached (a call link, a ticket page), or null. Arbitrary organizer-supplied content — treat it as untrusted. */ + /** @description URL the organizer attached (a call link, a ticket page), or null. Treat it as untrusted. */ url: string | null; - /** @description True for a date-based event: `startDate`/`endDate` carry the dates and every instant field is null. False for a timed event, where the reverse holds. */ + /** @description True for a date-based event: `startDate`/`endDate` are set and the instant fields are null. */ isAllDay: boolean; /** - * @description This occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event. + * @description This occurrence's start instant (ISO 8601 UTC). Null when all-day. * @example 2026-08-16T00:00:00.000Z */ startsAt: string | null; - /** @description This occurrence's resolved end instant (ISO 8601 UTC). Null for an all-day event or an open-ended one; equal to `startsAt` for a point-in-time event. */ + /** @description This occurrence's end instant (ISO 8601 UTC). Null when all-day or open-ended; equal to `startsAt` for a point-in-time event. */ endsAt: string | null; - /** - * @description All-day only: this occurrence's start date (`YYYY-MM-DD`). Null for a timed event. - * @example 2026-08-15 - */ + /** @description All-day only: this occurrence's start date (`YYYY-MM-DD`). Null on a timed event. */ startDate: string | null; - /** @description All-day only: EXCLUSIVE end date (`YYYY-MM-DD`) — null means a single day. Null for a timed event. */ + /** @description All-day only: exclusive end date (`YYYY-MM-DD`). Null for a single day or a timed event. */ endDate: string | null; - /** @description Timed only: this occurrence's start as local wall-clock time in `timeZone` (`YYYY-MM-DDTHH:MM:SS`) — the time the organizer actually authored. Equal to `recurrenceId` for a timed occurrence. Null for an all-day event. */ + /** @description Timed only: this occurrence's start as wall-clock time in `timeZone`. Equal to `recurrenceId`. Null when all-day. */ localStart: string | null; /** - * @description Timed only: the IANA zone the event was authored in — the zone `localStart` is expressed in. Null for an all-day event. + * @description Timed only: the IANA zone `localStart` is expressed in. Null when all-day. * @example America/Chicago */ timeZone: string | null; - /** @description How many members have marked interest in the event (its RSVP count). Members only — this API cannot RSVP. */ + /** @description How many members marked interest. */ interestCount: number; - /** @description Whether attending requires buying a ticket. Always false on an `update` event. Independent of `tickets`: an event can require a ticket and still list none here, when the seller has not published a public purchase page for it. */ + /** @description Whether attending requires a ticket. Always false on an `update` event. An event can require one and still list none in `tickets`. */ requiresTicket: boolean; - /** @description Tickets on sale for this event that an anonymous caller can buy, in the order the organizer attached them. Empty when the event sells no tickets, or sells them only through pages that are not public. Viewer-independent, like everything else here — it says what is on sale, never what you already hold. */ + /** @description Tickets on sale, in the order the organizer attached them. */ tickets: components["schemas"]["PublicEventTicketOutput"][]; - /** @description The cover image to render for THIS occurrence, when present: the occurrence's own cover art if the organizer set one, otherwise the event's. Always a public image with a PERMANENT unsigned URL — no `urlExpiresAt`, safe to hotlink while the event references it. */ + /** @description This occurrence's cover image, falling back to the event's. Permanent unsigned URL, safe to hotlink. */ cover?: components["schemas"]["PublicMediaOutput"]; }; - /** @description A cursor-paginated page of public event occurrences */ + /** + * @description A cursor-paginated list of occurrences + * @example { + * "items": [ + * { + * "id": "evt_ehrrkh8thkkdh6c383zkf9", + * "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + * "recurrenceId": "2026-09-09", + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * }, + * "title": "DFOS product weekly", + * "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + * "location": null, + * "type": "update", + * "mode": null, + * "url": null, + * "isAllDay": true, + * "startsAt": null, + * "endsAt": null, + * "startDate": "2026-09-09", + * "endDate": null, + * "localStart": null, + * "timeZone": null, + * "interestCount": 0, + * "requiresTicket": false, + * "tickets": [], + * "cover": { + * "id": "media_r22avnnrfch9ee62nhzkcn", + * "filename": "cover.png", + * "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + * "contentType": "image/png", + * "contentLength": 114085, + * "width": 3000, + * "height": 2100, + * "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + * } + * }, + * { + * "id": "evt_ehrrkh8thkkdh6c383zkf9", + * "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + * "recurrenceId": "2026-09-16", + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * }, + * "title": "DFOS product weekly", + * "description": "A weekly peak of the inner forest of DFOS. What we're working on, what we're thinking about, what's next.", + * "location": null, + * "type": "update", + * "mode": null, + * "url": null, + * "isAllDay": true, + * "startsAt": null, + * "endsAt": null, + * "startDate": "2026-09-16", + * "endDate": null, + * "localStart": null, + * "timeZone": null, + * "interestCount": 0, + * "requiresTicket": false, + * "tickets": [], + * "cover": { + * "id": "media_r22avnnrfch9ee62nhzkcn", + * "filename": "cover.png", + * "url": "https://dfos.imgix.net/media/public/r22avnnrfch9ee62nhzkcn-cover.png", + * "contentType": "image/png", + * "contentLength": 114085, + * "width": 3000, + * "height": 2100, + * "blurHash": "et5^}ckUX|kUYsX|fji~fQj[P1a{nTayoyt%j[W-j[V[t7j[ayfja3" + * } + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null + * } + */ PublicEventPageOutput: { /** @description Page of public event occurrences */ items: components["schemas"]["PublicEventOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A cursor-paginated page of public topics */ + /** + * @description A cursor-paginated page of public topics + * @example { + * "items": [ + * { + * "id": "topic_6c2efd472dvt8rf9k4ftcc", + * "name": "DFOS Blog", + * "description": "Announcements, product notes, and essays from the DFOS team." + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 1 + * } + */ PublicTopicPageOutput: { /** @description Page of public topics */ items: components["schemas"]["PublicTopicOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description The release a product belongs to */ + /** + * @description The release a product belongs to + * @example { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * } + */ PublicProductReleaseRefOutput: { - /** - * @description The release's public page slug — hand it to `GET /spaces/{space}/releases/{slug}` for the full page. Mutable, like every slug here. - * @example blue-record - */ + /** @description The release's public page slug. Pass it to `GET /spaces/{space}/releases/{slug}`. */ slug: string; - /** - * @description Release name - * @example Blue Record - */ + /** @description Release name */ name: string; }; - /** @description A store product with a public purchase page */ + /** + * @description A store product with a public purchase page + * @example { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "kind": "digital", + * "image": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "purchaseGrantsMembership": true, + * "available": null, + * "isSoldOut": false, + * "purchasable": true, + * "sortOrder": 0, + * "release": { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * }, + * "createdAt": "2026-08-08T13:24:36.433Z" + * } + */ PublicProductOutput: { - /** - * @description Stable store-product id. Store this identifier; the slug may change. - * @example sprod_6encc4akrze2ah9kntzd9t - */ + /** @description Stable store-product id. */ id: string; /** - * @description The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example softr-ticket + * @description The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet-digital */ slug: string; - /** - * @description Product name - * @example SOFTR Ticket - */ + /** @description Product name */ name: string; - /** @description Seller-authored plain-text description. Newlines are significant; render with preserved whitespace. */ + /** @description Plain-text description. Newlines are significant. */ description: string | null; /** - * @description What the product delivers. Open enum — treat an unrecognized value as an opaque string. + * @description What the product delivers. Open enum. * @enum {string} */ kind: "digital" | "physical"; - /** @description Hero image, when the product has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL. */ + /** @description Hero image, when the product has one. Always a permanent public CDN URL. */ image?: components["schemas"]["PublicMediaOutput"]; - /** @description Every purchasable price tier, one-time and recurring alike. Multiple tiers are normal (e.g. a one-time price alongside a monthly subscription). */ + /** @description Every purchasable price tier. */ prices: components["schemas"]["PublicProductPriceOutput"][]; - /** @description Whether buying this product makes the buyer a member of the space. A PRODUCT fact, identical for every caller — it says what the purchase does, not what any particular viewer would get from it (a caller who is already a member gains nothing new, and this field does not know or say so). */ + /** @description Whether buying this product makes the buyer a member of the space. */ purchaseGrantsMembership: boolean; - /** @description Remaining stock, or null when the product has unlimited stock. Advisory only — availability is re-checked at purchase. */ + /** @description Remaining stock, or null when unlimited. Advisory; stock is re-checked at purchase. */ available: number | null; /** @description True when limited stock is exhausted (`available` <= 0). */ isSoldOut: boolean; - /** @description Whether a purchase can be STARTED right now. False when the platform payment rail is paused or the product has no completable price tier — render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message. */ + /** @description Whether a purchase can be started right now. False when the payment rail is paused or no price tier is completable. */ purchasable: boolean; - /** - * @description This product's position in the space's public product list, counting from 0 — LOWER SORTS FIRST. Dense and contiguous across the whole list (0, 1, 2, …) and meaningful only within one space; products with no public page are not counted, so this reveals nothing about a space's unpublished catalog. The index is ordered by this field. It is a POSITION, not a stable identifier — publishing, unpublishing, or reordering shifts it, so never store it as a key. - * @example 0 - */ + /** @description Position in the space's public product list, from 0, lowest first. Dense over publicly-visible products only, and not a stable key. */ sortOrder: number; - /** @description The next upcoming PUBLIC event occurrence this product admits, when one exists — a ticket product’s backlink to what it sells entry to. SINGULAR BY DESIGN: a product may admit several events (a season pass), and this names the SOONEST upcoming occurrence across all of them — the one date a purchase page should send a buyer to — never a claim that only one event is admitted. Read the events namespace for the full relation. Absent when the product is not a ticket, when its ticket binding has been detached, when the event it admits is not publicly visible, and when no occurrence falls inside the window this API serves (the same ~1 year the events routes look ahead). */ + /** @description The soonest upcoming public event occurrence this product admits. A product may admit several; read the events namespace for the full relation. Absent when the product is not a ticket, its binding is detached, the event is not public, or no occurrence falls in the coming year. */ ticketFor?: { - /** - * @description The event series this product admits — the same `seriesId` the events namespace emits. - * @example evt_6encc4akrze2ah9kntzd9t - */ + /** @description The event series this product admits. */ seriesId: string; - /** - * @description RFC 5545 RECURRENCE-ID of the NEXT upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence. - * @example 2026-08-15T19:00:00 - */ + /** @description RFC 5545 RECURRENCE-ID of the next upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. */ recurrenceId: string; - /** - * @description That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event. - * @example 2026-08-16T00:00:00.000Z - */ + /** @description The occurrence's start instant (ISO 8601 UTC). Null for an all-day event. */ startsAt: string | null; - /** - * @description All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event. - * @example 2026-08-15 - */ + /** @description All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event. */ startDate: string | null; }; - /** @description The release this product belongs to, when a publicly-visible one holds it — a one-line context chip, not a projection of the release. SINGULAR: a product composed into several releases carries the EARLIEST-published public one (ties broken by release id). Absent when the product is in no release, and equally absent when every release holding it is unpublished — the two are deliberately indistinguishable. Read `GET /spaces/{space}/releases/{slug}` for the release itself. */ + /** @description The release this product belongs to, when a public one holds it. A product in several releases carries the earliest-published public one. */ release?: components["schemas"]["PublicProductReleaseRefOutput"]; /** * Format: date-time - * @description When the product was created (ISO 8601 UTC). The cross-space index `GET /products` is ordered by this value, newest first. It is the row's own creation stamp — NOT "when it was published", which this surface does not record, and not a position in any catalog. - * @example 2026-09-01T17:00:00.000Z + * @description When the product was created (ISO 8601 UTC). `GET /products` orders by this value, newest first. */ createdAt: string; }; - /** @description A cursor-paginated page of public products */ + /** + * @description A page of public products + * @example { + * "items": [ + * { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "kind": "digital", + * "image": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "purchaseGrantsMembership": true, + * "available": null, + * "isSoldOut": false, + * "purchasable": true, + * "sortOrder": 0, + * "release": { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * }, + * "createdAt": "2026-08-08T13:24:36.433Z" + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null, + * "totalCount": 2 + * } + */ PublicProductPageOutput: { /** @description Page of public products */ items: components["schemas"]["PublicProductOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A public product with the space it belongs to */ + /** + * @description A public product with the space it belongs to + * @example { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "kind": "digital", + * "image": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "purchaseGrantsMembership": true, + * "available": null, + * "isSoldOut": false, + * "purchasable": true, + * "sortOrder": 0, + * "release": { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * }, + * "createdAt": "2026-08-08T13:24:36.433Z", + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * } + */ PublicProductFeedItemOutput: { - /** - * @description Stable store-product id. Store this identifier; the slug may change. - * @example sprod_6encc4akrze2ah9kntzd9t - */ + /** @description Stable store-product id. */ id: string; /** - * @description The product's public purchase-page slug (the `/p/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example softr-ticket + * @description The product's public purchase-page slug (`/p/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet-digital */ slug: string; - /** - * @description Product name - * @example SOFTR Ticket - */ + /** @description Product name */ name: string; - /** @description Seller-authored plain-text description. Newlines are significant; render with preserved whitespace. */ + /** @description Plain-text description. Newlines are significant. */ description: string | null; /** - * @description What the product delivers. Open enum — treat an unrecognized value as an opaque string. + * @description What the product delivers. Open enum. * @enum {string} */ kind: "digital" | "physical"; - /** @description Hero image, when the product has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL. */ + /** @description Hero image, when the product has one. Always a permanent public CDN URL. */ image?: components["schemas"]["PublicMediaOutput"]; - /** @description Every purchasable price tier, one-time and recurring alike. Multiple tiers are normal (e.g. a one-time price alongside a monthly subscription). */ + /** @description Every purchasable price tier. */ prices: components["schemas"]["PublicProductPriceOutput"][]; - /** @description Whether buying this product makes the buyer a member of the space. A PRODUCT fact, identical for every caller — it says what the purchase does, not what any particular viewer would get from it (a caller who is already a member gains nothing new, and this field does not know or say so). */ + /** @description Whether buying this product makes the buyer a member of the space. */ purchaseGrantsMembership: boolean; - /** @description Remaining stock, or null when the product has unlimited stock. Advisory only — availability is re-checked at purchase. */ + /** @description Remaining stock, or null when unlimited. Advisory; stock is re-checked at purchase. */ available: number | null; /** @description True when limited stock is exhausted (`available` <= 0). */ isSoldOut: boolean; - /** @description Whether a purchase can be STARTED right now. False when the platform payment rail is paused or the product has no completable price tier — render an unavailable state rather than a dead buy button. Independent of `isSoldOut`, which has its own message. */ + /** @description Whether a purchase can be started right now. False when the payment rail is paused or no price tier is completable. */ purchasable: boolean; - /** - * @description This product's position in the space's public product list, counting from 0 — LOWER SORTS FIRST. Dense and contiguous across the whole list (0, 1, 2, …) and meaningful only within one space; products with no public page are not counted, so this reveals nothing about a space's unpublished catalog. The index is ordered by this field. It is a POSITION, not a stable identifier — publishing, unpublishing, or reordering shifts it, so never store it as a key. - * @example 0 - */ + /** @description Position in the space's public product list, from 0, lowest first. Dense over publicly-visible products only, and not a stable key. */ sortOrder: number; - /** @description The next upcoming PUBLIC event occurrence this product admits, when one exists — a ticket product’s backlink to what it sells entry to. SINGULAR BY DESIGN: a product may admit several events (a season pass), and this names the SOONEST upcoming occurrence across all of them — the one date a purchase page should send a buyer to — never a claim that only one event is admitted. Read the events namespace for the full relation. Absent when the product is not a ticket, when its ticket binding has been detached, when the event it admits is not publicly visible, and when no occurrence falls inside the window this API serves (the same ~1 year the events routes look ahead). */ + /** @description The soonest upcoming public event occurrence this product admits. A product may admit several; read the events namespace for the full relation. Absent when the product is not a ticket, its binding is detached, the event is not public, or no occurrence falls in the coming year. */ ticketFor?: { - /** - * @description The event series this product admits — the same `seriesId` the events namespace emits. - * @example evt_6encc4akrze2ah9kntzd9t - */ + /** @description The event series this product admits. */ seriesId: string; - /** - * @description RFC 5545 RECURRENCE-ID of the NEXT upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence. - * @example 2026-08-15T19:00:00 - */ + /** @description RFC 5545 RECURRENCE-ID of the next upcoming occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. */ recurrenceId: string; - /** - * @description That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event. - * @example 2026-08-16T00:00:00.000Z - */ + /** @description The occurrence's start instant (ISO 8601 UTC). Null for an all-day event. */ startsAt: string | null; - /** - * @description All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event. - * @example 2026-08-15 - */ + /** @description All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event. */ startDate: string | null; }; - /** @description The release this product belongs to, when a publicly-visible one holds it — a one-line context chip, not a projection of the release. SINGULAR: a product composed into several releases carries the EARLIEST-published public one (ties broken by release id). Absent when the product is in no release, and equally absent when every release holding it is unpublished — the two are deliberately indistinguishable. Read `GET /spaces/{space}/releases/{slug}` for the release itself. */ + /** @description The release this product belongs to, when a public one holds it. A product in several releases carries the earliest-published public one. */ release?: components["schemas"]["PublicProductReleaseRefOutput"]; /** * Format: date-time - * @description When the product was created (ISO 8601 UTC). The cross-space index `GET /products` is ordered by this value, newest first. It is the row's own creation stamp — NOT "when it was published", which this surface does not record, and not a position in any catalog. - * @example 2026-09-01T17:00:00.000Z + * @description When the product was created (ISO 8601 UTC). `GET /products` orders by this value, newest first. */ createdAt: string; space: components["schemas"]["PublicSpaceRefOutput"]; }; - /** @description A cursor-paginated page of cross-space public products */ + /** + * @description A page of cross-space public products + * @example { + * "items": [ + * { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "kind": "digital", + * "image": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "purchaseGrantsMembership": true, + * "available": null, + * "isSoldOut": false, + * "purchasable": true, + * "sortOrder": 0, + * "release": { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * }, + * "createdAt": "2026-08-08T13:24:36.433Z", + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null + * } + */ PublicProductFeedPageOutput: { /** @description Page of public products across every publicly-discoverable space */ items: components["schemas"]["PublicProductFeedItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; /** - * @description Where the release is in time, DERIVED from its dates rather than stored. `upcoming` = published, but `releasesAt` is still in the future; `live` = out; `ended` = `closesAt` has passed and the page is archival. Open enum — treat an unrecognized value as an opaque string. + * @description Where the release is in time: `upcoming` before `releasesAt`, `live` once out, `ended` past `closesAt`. Open enum. * @enum {string} */ PublicReleaseState: "upcoming" | "live" | "ended"; - /** @description A credit on a release */ - PublicReleaseCreditOutput: { - /** - * @description Stable credit id. - * @example relcred_6encc4akrze2ah9kntzd9t - */ + /** + * @description A credit on a release + * @example { + * "id": "relcred_aha44ncvfnfk8t23ct3v7r", + * "displayName": "Yancey", + * "role": "editor" + * } + */ + PublicReleaseCreditOutput: { + /** @description Stable credit id. */ id: string; - /** - * @description The credited name, exactly as the release renders it. - * @example Lena Ortiz - */ + /** @description The credited name. */ displayName: string; - /** - * @description What they did. Free text authored by the runner, never an enum — "mastering", "cover photograph", "with thanks to". - * @example mastering - */ + /** @description The credit role, free text ("mastering"). Null when unset. */ role: string | null; }; - /** @description An entry on a release media wall */ + /** + * @description An entry on a release media wall + * @example { + * "id": "relmed_h4zdvrtz9nr73nc7v3n99e", + * "caption": null, + * "media": { + * "id": "media_9tnfvccf6t372vdnv43ftv", + * "filename": "dfos.png", + * "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + * "contentType": "image/png", + * "contentLength": 7045192, + * "width": 2729, + * "height": 2129, + * "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + * } + * } + */ PublicReleaseMediaOutput: { - /** - * @description Stable wall-entry id (not the media id — that is `media.id`). - * @example relmed_6encc4akrze2ah9kntzd9t - */ + /** @description Stable wall-entry id (not `media.id`). */ id: string; - /** @description Runner-authored caption for this entry, when there is one. */ + /** @description Caption for this entry, when there is one. */ caption: string | null; - /** @description The asset. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL, so a wall entry whose asset is private media is omitted from the wall entirely rather than served with an expiring link. */ + /** + * @description The asset. Always a permanent public CDN URL. + * @example { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * } + */ media: { - /** - * @description Media object id — the value an `attachment://` inline body token references. Use it to associate a body token with its entry in `bodyMedia` (required when a body carries more than one inline media item). Media ids already appear verbatim in the body markdown, so this exposes nothing new. - * @example media_6encc4akrze2ah9kntzd9t - */ + /** @description Media object id — what an `attachment://` inline body token references. Use it to match a token to its `bodyMedia` entry. */ id: string; - /** @description Original uploaded filename of the media object */ + /** @description Original uploaded filename */ filename: string; - /** @description Resolved URL for the media object. A permanent, unsigned imgix CDN URL for public images; a time-limited SIGNED URL for private media (audio/video/files). When signed, `urlExpiresAt` is present — never persist a signed URL, re-fetch the post for a fresh one. */ + /** @description Resolved URL for the media object. Permanent for public images; a time-limited signed URL for private media, which carries `urlExpiresAt`. */ url: string; /** @description MIME type of the media object */ contentType: string; - /** @description Size of the media object in bytes (absent until upload is finalized) */ + /** @description Size in bytes, absent until the upload is finalized */ contentLength?: number; /** @description Pixel width (images/video) */ width?: number; @@ -1861,254 +2545,434 @@ export interface components { height?: number; /** @description Blur-hash placeholder string for progressive image loading */ blurHash?: string; - /** @description Uploader-authored caption / alt text, when present */ + /** @description Uploader-authored caption or alt text */ alt?: string; /** @description Playback length in milliseconds (audio/video only) */ durationMs?: number; - /** @description CDN URL of an extracted poster frame / cover art (audio/video only) */ + /** @description CDN URL of an extracted poster frame or cover art (audio/video only) */ posterUrl?: string; - /** @description Streamable MP4 rendition URL (audio/video only). A permanent CDN URL for public media; a time-limited SIGNED URL for private media (in which case `urlExpiresAt` is present). */ + /** @description Streamable MP4 rendition URL (audio/video only). Permanent for public media; a time-limited signed URL for private media. */ playbackUrl?: string; /** * Format: date-time - * @description When the SIGNED `url` / `playbackUrl` expire (ISO 8601 UTC). PRESENT iff those URLs are time-limited signed URLs (private media); ABSENT means they are permanent (public images). Never persist a signed URL — re-fetch the post to obtain fresh ones. + * @description When the signed `url` and `playbackUrl` expire (ISO 8601 UTC). Present only for private media. Signed URLs are ephemeral — re-fetch rather than persisting them. */ urlExpiresAt?: string; /** @description Amplitude overview for audio — up to 200 integers, each 0–100 */ waveformPeaks?: number[]; }; }; - /** @description A public calendar event a release points at */ + /** + * @description A public event a release points at. Address it in the events namespace as `(seriesId, recurrenceId)`. + * @example { + * "seriesId": "evt_ehrrkh8thkkdh6c383zkf9", + * "recurrenceId": "2026-09-09", + * "title": "DFOS product weekly", + * "isAllDay": true, + * "timeZone": null, + * "startsAt": null, + * "startDate": "2026-09-09" + * } + */ PublicReleaseEventOutput: { - /** - * @description The event series — the same `seriesId` the events namespace emits. - * @example evt_6encc4akrze2ah9kntzd9t - */ + /** @description The event series id. */ seriesId: string; - /** - * @description RFC 5545 RECURRENCE-ID of the occurrence: its local wall-time (`YYYY-MM-DDTHH:MM:SS`), or its date (`YYYY-MM-DD`) for an all-day event. Byte-identical to the events namespace's value for the same occurrence. - * @example 2026-09-12T19:00:00 - */ + /** @description RFC 5545 RECURRENCE-ID of the occurrence: local wall-time (`YYYY-MM-DDTHH:MM:SS`), or a date (`YYYY-MM-DD`) when all-day. */ recurrenceId: string; - /** - * @description Event title - * @example Listening party - */ + /** @description Event title */ title: string; - /** @description True for a date-based occurrence: `startDate` carries the date and `startsAt` is null. False for a timed one, where the reverse holds. */ + /** @description True for a date-based occurrence: `startDate` is set, `startsAt` is null. */ isAllDay: boolean; /** - * @description Timed only: the IANA zone the event was authored in — the zone `recurrenceId` is expressed in. Null for an all-day event. + * @description Timed only: the IANA zone `recurrenceId` is expressed in. Null for an all-day event. * @example America/New_York */ timeZone: string | null; - /** - * @description That occurrence's resolved start instant (ISO 8601 UTC). Null for an all-day event. - * @example 2026-09-13T00:00:00.000Z - */ + /** @description The occurrence's start instant (ISO 8601 UTC). Null for an all-day event. */ startsAt: string | null; - /** - * @description All-day only: that occurrence's start date (`YYYY-MM-DD`). Null for a timed event. - * @example 2026-09-12 - */ + /** @description All-day only: the occurrence's start date (`YYYY-MM-DD`). Null for a timed event. */ startDate: string | null; }; - /** @description A release in a space public release shelf */ + /** + * @description A release in a space's public shelf + * @example { + * "id": "rel_vdeaff8zac8cr49fa624dh", + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet", + * "state": "live", + * "shortDescription": "The digital edition, readable anywhere, out now.", + * "cover": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "catalogNumber": null, + * "location": null, + * "publishedAt": "2026-08-08T13:24:36.433Z", + * "releasesAt": "2026-08-08T13:24:36.433Z", + * "closesAt": null, + * "productCount": 1 + * } + */ PublicReleaseSummaryOutput: { - /** - * @description Stable release id. Store this identifier; the slug may change. - * @example rel_6encc4akrze2ah9kntzd9t - */ + /** @description Stable release id. */ id: string; /** - * @description The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example blue-record + * @description The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet */ slug: string; - /** - * @description Release name - * @example Blue Record - */ + /** @description Release name */ name: string; state: components["schemas"]["PublicReleaseState"]; - /** @description One-line blurb. Newlines are not expected here; render as a single line of text. */ + /** @description One-line blurb. */ shortDescription: string | null; - /** @description Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL. */ + /** @description Cover image, when the release has one. Always a permanent public CDN URL. */ cover?: components["schemas"]["PublicMediaOutput"]; /** - * @description The space's own catalog expression ("MTL-001", "no. 4"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it. + * @description The space's own catalog expression. Free text, not unique, never a key. * @example MTL-001 */ catalogNumber: string | null; /** - * @description Freeform place string, purely presentational. Never geocoded, never a key. + * @description Freeform place string. Never geocoded, never a key. * @example Austin, TX */ location: string | null; /** * Format: date-time - * @description When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts. - * @example 2026-09-01T17:00:00.000Z + * @description When this release was published (ISO 8601 UTC). Release lists order by this value, newest first. */ publishedAt: string; - /** - * @description When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case. - * @example 2026-09-12T17:00:00.000Z - */ + /** @description When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set. */ releasesAt: string | null; /** - * @description When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date. + * @description When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set. * @example 2026-10-12T17:00:00.000Z */ closesAt: string | null; - /** - * @description How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published. - * @example 2 - */ + /** @description How many of the composed products are publicly purchasable. */ productCount: number; }; - /** @description A release public page */ + /** + * @description A release public page + * @example { + * "id": "rel_vdeaff8zac8cr49fa624dh", + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet", + * "state": "live", + * "shortDescription": "The digital edition, readable anywhere, out now.", + * "cover": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "catalogNumber": null, + * "location": null, + * "publishedAt": "2026-08-08T13:24:36.433Z", + * "releasesAt": "2026-08-08T13:24:36.433Z", + * "closesAt": null, + * "productCount": 1, + * "longDescription": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "media": [ + * { + * "id": "relmed_h4zdvrtz9nr73nc7v3n99e", + * "caption": null, + * "media": { + * "id": "media_9tnfvccf6t372vdnv43ftv", + * "filename": "dfos.png", + * "url": "https://dfos.imgix.net/media/public/9tnfvccf6t372vdnv43ftv-dfos.png", + * "contentType": "image/png", + * "contentLength": 7045192, + * "width": 2729, + * "height": 2129, + * "blurHash": "eIHLC@.7~q%MM|?bsmS5xu%MMwS24TE1oz.8xat7W=M{%Naxt7j]RP" + * } + * } + * ], + * "credits": [ + * { + * "id": "relcred_aha44ncvfnfk8t23ct3v7r", + * "displayName": "Yancey", + * "role": "editor" + * } + * ], + * "products": [ + * { + * "id": "sprod_ra73keekz9f22k7889ren7", + * "slug": "the-dark-forest-anthology-of-the-internet-digital", + * "name": "The Dark Forest Anthology of the Internet (digital)", + * "description": "A book about how to survive on the internet. It’s about the cozy web, the dark web, the dark forest, the clear net, the dark net, and a new social world emerging around us. This is the Dark Forest Anthology of the Internet.", + * "kind": "digital", + * "image": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "prices": [ + * { + * "id": "pprice_97hfv7rcea93267z8t677d", + * "type": "one_time", + * "amountCents": 1000, + * "currency": "usd" + * } + * ], + * "purchaseGrantsMembership": true, + * "available": null, + * "isSoldOut": false, + * "purchasable": true, + * "sortOrder": 0, + * "release": { + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet" + * }, + * "createdAt": "2026-08-08T13:24:36.433Z" + * } + * ], + * "events": [] + * } + */ PublicReleaseOutput: { - /** - * @description Stable release id. Store this identifier; the slug may change. - * @example rel_6encc4akrze2ah9kntzd9t - */ + /** @description Stable release id. */ id: string; /** - * @description The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example blue-record + * @description The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet */ slug: string; - /** - * @description Release name - * @example Blue Record - */ + /** @description Release name */ name: string; state: components["schemas"]["PublicReleaseState"]; - /** @description One-line blurb. Newlines are not expected here; render as a single line of text. */ + /** @description One-line blurb. */ shortDescription: string | null; - /** @description Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL. */ + /** @description Cover image, when the release has one. Always a permanent public CDN URL. */ cover?: components["schemas"]["PublicMediaOutput"]; /** - * @description The space's own catalog expression ("MTL-001", "no. 4"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it. + * @description The space's own catalog expression. Free text, not unique, never a key. * @example MTL-001 */ catalogNumber: string | null; /** - * @description Freeform place string, purely presentational. Never geocoded, never a key. + * @description Freeform place string. Never geocoded, never a key. * @example Austin, TX */ location: string | null; /** * Format: date-time - * @description When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts. - * @example 2026-09-01T17:00:00.000Z + * @description When this release was published (ISO 8601 UTC). Release lists order by this value, newest first. */ publishedAt: string; - /** - * @description When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case. - * @example 2026-09-12T17:00:00.000Z - */ + /** @description When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set. */ releasesAt: string | null; /** - * @description When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date. + * @description When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set. * @example 2026-10-12T17:00:00.000Z */ closesAt: string | null; - /** - * @description How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published. - * @example 2 - */ + /** @description How many of the composed products are publicly purchasable. */ productCount: number; - /** @description Long-form release copy. Newlines are significant; render with preserved whitespace. */ + /** @description Long-form release copy. Newlines are significant. */ longDescription: string | null; - /** @description The media wall, in the runner's wall order. Entries whose asset is private media are omitted (this surface emits no signed URLs), so an empty wall is a valid answer. */ + /** @description The media wall, in wall order. Entries backed by private media are omitted. */ media: components["schemas"]["PublicReleaseMediaOutput"][]; - /** @description Credits, in credit order. Display text only — a name and an optional role. If credits ever carry a subject, only accepted ones appear, and a pending invitation is indistinguishable from a credit that was never offered. */ + /** @description Credits, in credit order. A name and an optional role. */ credits: components["schemas"]["PublicReleaseCreditOutput"][]; - /** @description The composed products, in the release's curated order — the SAME objects `products.getProduct` returns, filtered by the store's own public-visibility rule. A product with no public purchase page is absent from this list while the release page renders normally, and is not counted in `productCount`. */ + /** @description The composed products, in the release's curated order. Products with no public page are absent and uncounted. */ products: components["schemas"]["PublicProductOutput"][]; - /** @description Public calendar events attached to this release, soonest first. Each is filtered by the EVENTS visibility rules; an event an anonymous caller may not see, or one with no occurrence in the window this API serves, is simply absent. */ + /** @description Public events attached to this release, soonest first. Events an anonymous caller may not see are absent. */ events: components["schemas"]["PublicReleaseEventOutput"][]; }; - /** @description A cursor-paginated page of public releases */ + /** + * @description A page of public releases + * @example { + * "items": [ + * { + * "id": "rel_vdeaff8zac8cr49fa624dh", + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet", + * "state": "live", + * "shortDescription": "The digital edition, readable anywhere, out now.", + * "cover": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "catalogNumber": null, + * "location": null, + * "publishedAt": "2026-08-08T13:24:36.433Z", + * "releasesAt": "2026-08-08T13:24:36.433Z", + * "closesAt": null, + * "productCount": 1 + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 1 + * } + */ PublicReleasePageOutput: { /** @description Page of public releases */ items: components["schemas"]["PublicReleaseSummaryOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A public release with the space it belongs to */ + /** + * @description A public release with the space it belongs to + * @example { + * "id": "rel_vdeaff8zac8cr49fa624dh", + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet", + * "state": "live", + * "shortDescription": "The digital edition, readable anywhere, out now.", + * "cover": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "catalogNumber": null, + * "location": null, + * "publishedAt": "2026-08-08T13:24:36.433Z", + * "releasesAt": "2026-08-08T13:24:36.433Z", + * "closesAt": null, + * "productCount": 1, + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * } + */ PublicReleaseFeedItemOutput: { - /** - * @description Stable release id. Store this identifier; the slug may change. - * @example rel_6encc4akrze2ah9kntzd9t - */ + /** @description Stable release id. */ id: string; /** - * @description The release's public page slug (the `/r/{slug}` segment on the space site). Mutable — changing it breaks old links with no redirect; store the `id`. - * @example blue-record + * @description The release's public page slug (`/r/{slug}` on the space site). A mutable alias; `id` is canonical. + * @example the-dark-forest-anthology-of-the-internet */ slug: string; - /** - * @description Release name - * @example Blue Record - */ + /** @description Release name */ name: string; state: components["schemas"]["PublicReleaseState"]; - /** @description One-line blurb. Newlines are not expected here; render as a single line of text. */ + /** @description One-line blurb. */ shortDescription: string | null; - /** @description Cover image, when the release has one. Always a PUBLIC, permanently-hosted CDN image — this endpoint never emits a signed or expiring URL. */ + /** @description Cover image, when the release has one. Always a permanent public CDN URL. */ cover?: components["schemas"]["PublicMediaOutput"]; /** - * @description The space's own catalog expression ("MTL-001", "no. 4"). NOT A KEY: free text, deliberately un-unique — runners reuse it, re-number, and leave gaps. Render it; never index on it. + * @description The space's own catalog expression. Free text, not unique, never a key. * @example MTL-001 */ catalogNumber: string | null; /** - * @description Freeform place string, purely presentational. Never geocoded, never a key. + * @description Freeform place string. Never geocoded, never a key. * @example Austin, TX */ location: string | null; /** * Format: date-time - * @description When this release was published (ISO 8601 UTC). The index is ordered by this value, newest first. A runner who unpublishes and re-publishes moves it, and the shelf re-sorts. - * @example 2026-09-01T17:00:00.000Z + * @description When this release was published (ISO 8601 UTC). Release lists order by this value, newest first. */ publishedAt: string; - /** - * @description When the release OPENS, when the runner named a date (ISO 8601 UTC). While it is in the future the `state` is `upcoming`. Null when the release has no opening date, which is the common case. - * @example 2026-09-12T17:00:00.000Z - */ + /** @description When the release opens (ISO 8601 UTC). Before it, `state` is `upcoming`. Null when no opening date is set. */ releasesAt: string | null; /** - * @description When the release CLOSES, when the runner named a date (ISO 8601 UTC). Once it has passed the `state` is `ended` and the page stays readable as an archival record. Null when the release has no closing date. + * @description When the release closes (ISO 8601 UTC). Past it, `state` is `ended` and the page stays readable. Null when no closing date is set. * @example 2026-10-12T17:00:00.000Z */ closesAt: string | null; - /** - * @description How many products in the composition are PUBLICLY purchasable — the same set the release page lists, counted. Never the raw composition size, which would disclose how much of a release a space has not published. - * @example 2 - */ + /** @description How many of the composed products are publicly purchasable. */ productCount: number; space: components["schemas"]["PublicSpaceRefOutput"]; }; - /** @description A cursor-paginated page of cross-space public releases */ + /** + * @description A page of cross-space public releases + * @example { + * "items": [ + * { + * "id": "rel_vdeaff8zac8cr49fa624dh", + * "slug": "the-dark-forest-anthology-of-the-internet", + * "name": "The Dark Forest Anthology of the Internet", + * "state": "live", + * "shortDescription": "The digital edition, readable anywhere, out now.", + * "cover": { + * "id": "media_a9e9r767ca9frz7dvrfvna", + * "filename": "GpE09OWasAAw1BX.jpeg", + * "url": "https://dfos.imgix.net/media/public/a9e9r767ca9frz7dvrfvna-gpe09owasaaw1bx.jpeg", + * "contentType": "image/jpeg", + * "contentLength": 2468014, + * "width": 3200, + * "height": 3200, + * "blurHash": "e$P43Mxs|Fw]K4r^oeaLaeba,;n*JTf6snr?j?kCoLayv~f6OEj]n%" + * }, + * "catalogNumber": null, + * "location": null, + * "publishedAt": "2026-08-08T13:24:36.433Z", + * "releasesAt": "2026-08-08T13:24:36.433Z", + * "closesAt": null, + * "productCount": 1, + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null + * } + */ PublicReleaseFeedPageOutput: { /** @description Page of public releases across every publicly-discoverable space */ items: components["schemas"]["PublicReleaseFeedItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A link on a public user profile */ + /** + * @description A link on a public user profile + * @example { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + */ PublicProfileLinkOutput: { /** @description The link URL as the profile owner entered it */ url: string; @@ -2118,31 +2982,82 @@ export interface components { title: string | null; /** @description Owner-curated short blurb for the link, or null */ description: string | null; - /** @description Resolved public CDN URL for the cached OG preview image, or null */ + /** @description Public CDN URL for the cached OG preview image, or null */ imageUrl: string | null; }; - /** @description A public user profile */ + /** + * @description A public user profile + * @example { + * "id": "identity_3drf9nfk93na7vfdk723r6", + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "username": "bvalosek", + * "displayName": "Brandon", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + * "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + * "verifiedDomain": "bvalosek.com", + * "links": [ + * { + * "url": "https://bvalosek.com", + * "label": null, + * "title": null, + * "description": null, + * "imageUrl": null + * } + * ], + * "protocol": { + * "headOpCid": "bafyreig6d4y67d5jmux475unozzu7tr5g7fqhjbegozwepumcy3pen3woy", + * "profile": { + * "contentId": "f4nrnnv7zv89t7nazdnk8t2zn7chvcf", + * "headOpCid": "bafyreiasbpcbkct4mcqdopnz7vbk6yhdd4e35eotfnxsmg5vuxsw32ybx4" + * } + * }, + * "spaces": [ + * { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "memberCountSummary": "thousands of members", + * "pinned": true, + * "pinMessage": "Where I write about what we are building.", + * "pinnedAt": "2026-08-14T16:41:07.882Z" + * }, + * { + * "id": "space_z94a849d9kdftfvv3n9hn7", + * "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + * "domain": "rakowwwski", + * "displayName": "POPULAR", + * "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + * "memberCountSummary": "about a dozen members", + * "pinned": false, + * "pinMessage": null, + * "pinnedAt": null + * } + * ] + * } + */ PublicUserOutput: { - /** @description The user's identity entity id (`identity_…`) — a stable, URL-safe identifier, accepted anywhere `{user}` is. The canonical profile-URL identifier when the user has no handle. */ + /** @description The user's identity entity id (`identity_…`). Stable, and accepted anywhere `{user}` is. */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description The user's handle, or null if unset. A MUTABLE alias — a handle can change or be released; the `id` and `did` are the canonical, stable identifiers to store. */ + /** @description The user's handle, or null if unset. A mutable alias; the `id` and `did` are canonical. */ username: string | null; /** @description Display name, or null */ displayName: string | null; - /** @description Resolved public CDN URL for the avatar, or null */ + /** @description Public CDN URL for the avatar, or null */ avatarUrl: string | null; /** @description Profile bio / description, or null */ bio: string | null; - /** @description A domain this user has proven control of via origin binding, or null. The claim is a `DfosOrigin` entry on the user's identity chain, and the domain independently publishes the user's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the user claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction. */ + /** @description A domain this user has proven control of through origin binding, or null. Null when no claim is proven, or when the domain now attests a different identity. */ verifiedDomain: string | null; /** @description Ordered profile links (may be empty) */ links: components["schemas"]["PublicProfileLinkOutput"][]; - /** @description Protocol proof-plane handles for this user's identity and optional profile content chain. Present iff the identity chain exists; absent for legacy identities without a chain. Combine with `GET /protocol` to fetch and verify the chains from the relay. */ + /** @description Protocol proof-plane handles for this user's identity chain. Absent for identities without one. Resolve them through `GET /protocol`. */ protocol?: { /** @description The identity chain's current head operation CID */ headOpCid: string; - /** @description Protocol proof-plane handles for the identity profile content chain. Present iff the identity has a non-empty profile chain; absent when the profile is empty or its chain has not been created. */ + /** @description Protocol handles for the identity profile content chain. Absent when there is no profile chain. */ profile?: { /** @description The identity profile's protocol content-chain id */ contentId: string; @@ -2150,85 +3065,291 @@ export interface components { headOpCid: string; }; }; - /** @description The user's public space memberships: every pinned space (in slot order) followed by up to 20 more, most recently joined first. May be empty. Use `GET /users/{user}/spaces` for the complete, paginated list. */ + /** @description Every pinned space, then up to 20 more of the user's public spaces, most recently joined first. Use `GET /users/{user}/spaces` for the full list. */ spaces: components["schemas"]["PublicUserSpaceOutput"][]; }; - /** @description A public user profile in the directory listing */ + /** + * @description A public user profile in the directory listing + * @example { + * "id": "identity_3drf9nfk93na7vfdk723r6", + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "username": "bvalosek", + * "displayName": "Brandon", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + * "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + * "verifiedDomain": "bvalosek.com", + * "links": [ + * { + * "url": "https://bvalosek.com", + * "label": null, + * "title": null, + * "description": null, + * "imageUrl": null + * } + * ], + * "pinnedSpaces": [ + * { + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * ] + * } + */ PublicUserListItemOutput: { - /** @description The user's identity entity id (`identity_…`) — a stable, URL-safe identifier, accepted anywhere `{user}` is. The canonical profile-URL identifier when the user has no handle. */ + /** @description The user's identity entity id (`identity_…`). Stable, and accepted anywhere `{user}` is. */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description The user's handle, or null if unset. A MUTABLE alias — a handle can change or be released; the `id` and `did` are the canonical, stable identifiers to store. */ + /** @description The user's handle, or null if unset. A mutable alias; the `id` and `did` are canonical. */ username: string | null; /** @description Display name, or null */ displayName: string | null; - /** @description Resolved public CDN URL for the avatar, or null */ + /** @description Public CDN URL for the avatar, or null */ avatarUrl: string | null; /** @description Profile bio / description, or null */ bio: string | null; - /** @description A domain this user has proven control of via origin binding, or null. The claim is a `DfosOrigin` entry on the user's identity chain, and the domain independently publishes the user's DID back — so a third party can verify both halves without trusting this API (see `GET /protocol`). Null when the user claims no domain, has not yet proven a claim, or the domain now attests a different identity. A domain whose attestation has merely gone quiet keeps its value: silence alone is not a contradiction. But a domain that DID contradict this identity and has not attested it since reads null even once it falls silent — going offline does not retract a contradiction. */ + /** @description A domain this user has proven control of through origin binding, or null. Null when no claim is proven, or when the domain now attests a different identity. */ verifiedDomain: string | null; /** @description Ordered profile links (may be empty) */ links: components["schemas"]["PublicProfileLinkOutput"][]; - /** @description The user's pinned spaces in slot order (may be empty). Terse cards — fetch the profile for the full membership shelf. */ + /** @description The user's pinned spaces in slot order (may be empty). */ pinnedSpaces: components["schemas"]["PublicUserSpaceCardOutput"][]; }; - /** @description A cursor-paginated page of public user profiles */ + /** + * @description A cursor-paginated page of public user profiles + * @example { + * "items": [ + * { + * "id": "identity_3drf9nfk93na7vfdk723r6", + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "username": "bvalosek", + * "displayName": "Brandon", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png", + * "bio": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ", + * "verifiedDomain": "bvalosek.com", + * "links": [ + * { + * "url": "https://bvalosek.com", + * "label": null, + * "title": null, + * "description": null, + * "imageUrl": null + * } + * ], + * "pinnedSpaces": [ + * { + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + * ] + * }, + * { + * "id": "identity_349f9ka72nrefnf8kvrnhh", + * "did": "did:dfos:2228ka2thkre4ft44d73r232nfvdf2t", + * "username": "aron", + * "displayName": "Aron", + * "avatarUrl": "https://dfos.imgix.net/media/public/24934ta9c7z8dahz7aed7r-img-5016.jpeg", + * "bio": "Designing and navigating various contextual arrangements w/ an enigmatic sense of hope.", + * "verifiedDomain": null, + * "links": [], + * "pinnedSpaces": [] + * } + * ], + * "nextCursor": "eyJpZCI6InBvc3RfemUya2gyZDQ3dHplcmtoZXQ4MzQ4YyJ9", + * "previousCursor": null, + * "totalCount": null + * } + */ PublicUserPageOutput: { /** @description Page of public user profiles */ items: components["schemas"]["PublicUserListItemOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A space on a user's public profile */ + /** + * @description A space on a user's public profile + * @example { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "memberCountSummary": "thousands of members", + * "pinned": true, + * "pinMessage": "Where I write about what we are building.", + * "pinnedAt": "2026-08-14T16:41:07.882Z" + * } + */ PublicUserSpaceOutput: { - /** @description The space's entity id (`space_…`). Already public as the permanent `space-{id}` subdomain form; a stable join key for consumers that also see the space through other surfaces. */ + /** @description The space's entity id (`space_…`). Canonical and stable. */ id: string; did: components["schemas"]["ProtocolDid"]; /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias; the `did` is canonical. */ domain: string; /** @description Space display name, or null */ displayName: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; - /** @description Worded member-count summary (e.g. "a few dozen members"). Public surfaces deliberately avoid exact counts. */ + /** @description Worded member-count summary, e.g. "a few dozen members". Spaces do not publish exact counts. */ memberCountSummary: string; - /** @description Whether the user pinned this space to the top of their profile. Pinned spaces come first in the array. */ + /** @description Whether the user pinned this space. Pinned spaces come first. */ pinned: boolean; - /** @description The user's own short note about this space, or null. Only ever set when pinned. */ + /** @description The user's note about this space, or null. Set only when pinned. */ pinMessage: string | null; /** @description When the user pinned this space (ISO 8601 UTC), or null when not pinned. */ pinnedAt: string | null; }; - /** @description A terse pinned-space card on a directory listing row */ + /** + * @description A terse pinned-space card on a directory listing row + * @example { + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png" + * } + */ PublicUserSpaceCardOutput: { did: components["schemas"]["ProtocolDid"]; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A mutable alias. */ + /** @description Effective subdomain — custom domain if set, otherwise the `space-{id}` form. A mutable alias. */ domain: string; /** @description Space display name, or null */ displayName: string | null; - /** @description Resolved public CDN URL for the space avatar, or null */ + /** @description Public CDN URL for the space avatar, or null */ avatarUrl: string | null; }; - /** @description A cursor-paginated page of a user's public spaces */ + /** + * @description A cursor-paginated page of a user's public spaces + * @example { + * "items": [ + * { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "memberCountSummary": "thousands of members", + * "pinned": true, + * "pinMessage": "Where I write about what we are building.", + * "pinnedAt": "2026-08-14T16:41:07.882Z" + * }, + * { + * "id": "space_z94a849d9kdftfvv3n9hn7", + * "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + * "domain": "rakowwwski", + * "displayName": "POPULAR", + * "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + * "memberCountSummary": "about a dozen members", + * "pinned": false, + * "pinMessage": null, + * "pinnedAt": null + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 2 + * } + */ PublicUserSpacePageOutput: { /** @description Page of a user's public spaces */ items: components["schemas"]["PublicUserSpaceOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description A public post, or a gated space CTA when it is not anonymously readable */ + /** + * @description A readable post, or a space CTA when it is gated + * @example { + * "state": "eligible", + * "post": { + * "id": "post_ze2kh2d47tzerkhet8348c", + * "slug": "dfos-beyond-dfos-sign-in-domains-and-your-own-keys", + * "format": "long-post", + * "title": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "displayTitle": "DFOS beyond DFOS: sign-in, domains, and your own keys", + * "excerpt": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is...", + * "body": "Platforms today make your identity real by hosting it. Your @ is real because it appears on their domain. Your work is real because their page says it is.\n\nDFOS is built around a different split: the platform is where convenience lives, and the protocol is where continuity lives.", + * "cover": { + * "id": "media_efrvdarnc6zd3nhv389cf2", + * "filename": "p04.jpg", + * "url": "https://dfos.imgix.net/media/public/efrvdarnc6zd3nhv389cf2-p04.jpg", + * "contentType": "image/jpeg", + * "contentLength": 2890442, + * "width": 2752, + * "height": 1536, + * "blurHash": "eJ7ekWt%8{RQ.7tQoyWBfRoy8yVu?atQIBM{WBt7j[Rj%Lf8MyV[s;" + * }, + * "attachments": [], + * "bodyMedia": [ + * { + * "id": "media_rn2926ahdrnra6t4erc862", + * "filename": "2026-09-04-siwd-consent-composite-equal-height.png", + * "url": "https://dfos.imgix.net/media/public/rn2926ahdrnra6t4erc862-2026-09-04-siwd-consent-composite-equal-height.png", + * "contentType": "image/png", + * "contentLength": 273173, + * "width": 2116, + * "height": 1160, + * "blurHash": "eRS6Pl%Mt7%May~qWBWBt7WBxuayWBofj[%Mxut7Rjj[-;WBWBt7ay" + * } + * ], + * "author": { + * "did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "displayName": "Brandon", + * "username": "bvalosek", + * "avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png" + * }, + * "topics": [ + * { + * "id": "topic_6c2efd472dvt8rf9k4ftcc", + * "name": "DFOS Blog" + * } + * ], + * "canonicalUri": "https://home.dfos.com/post/dfos-beyond-dfos-sign-in-domains-and-your-own-keys-ze2kh2d47tzerkhet8348c", + * "protocol": { + * "contentId": "d4743469vf6heca8t466ckknvzknha2", + * "headOpCid": "bafyreiaztsa2wm76vu2t7nchqnpg3nl6jkydxolikj62ywwl3geccbbeby" + * }, + * "upvoteCount": 14, + * "commentCount": 6, + * "publishedAt": "2026-09-04T18:05:33.531Z", + * "updatedAt": "2026-09-04T18:06:01.020Z" + * }, + * "space": { + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "domain": "home", + * "joinMode": "open", + * "subscribeEnabled": true, + * "memberCountSummary": "thousands of members" + * } + * } + * @example { + * "state": "gated", + * "reason": "spaceMembers", + * "space": { + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "domain": "home", + * "joinMode": "open", + * "subscribeEnabled": true, + * "memberCountSummary": "thousands of members" + * } + * } + */ PublicGetPostOutput: { /** - * @description The post is anonymously readable — `post` carries the full public content. + * @description The caller can read the post; `post` carries its content. * @constant */ state: "eligible"; @@ -2236,35 +3357,50 @@ export interface components { space: components["schemas"]["PublicSpaceCtaOutput"]; } | { /** - * @description The post exists in this public space but is not anonymously readable — no content is returned, only a slim space CTA. + * @description The post exists in this space but the caller cannot read it; only a space CTA is returned. * @constant */ state: "gated"; /** - * @description Why the post is gated. `signIn` — open to any signed-in visitor, so signing in reveals it (a sign-in wall, e.g. a `non-members` override). `spaceMembers` — plain space membership is sufficient to read, so joining the space grants access. `restricted` — a finer gate applies (a pass, a group, a private topic tier, etc.); joining the space is NOT enough on its own — visit the space to see how to gain access. Treat this as an open enum: finer reason values may be ADDED later, so handle an unrecognized value as `restricted`. + * @description Why the post is gated: `signIn` (sign in to read it), `spaceMembers` (join the space), or `restricted` (a pass, group, or private topic). Open enum; unknown means `restricted`. * @enum {string} */ reason: "signIn" | "spaceMembers" | "restricted"; space: components["schemas"]["PublicSpaceCtaOutput"]; }; - /** @description DFOS protocol discovery info */ + /** + * @description DFOS protocol discovery info + * @example { + * "relayUrl": "https://relay.dfos.com", + * "didMethod": "did:dfos", + * "specUrl": "https://protocol.dfos.com/spec", + * "endpoints": { + * "wellKnown": "https://relay.dfos.com/.well-known/dfos-relay", + * "identity": "https://relay.dfos.com/proof/v1/identities/{did}", + * "identityLog": "https://relay.dfos.com/proof/v1/identities/{did}/log", + * "content": "https://relay.dfos.com/proof/v1/content/{contentId}", + * "contentLog": "https://relay.dfos.com/proof/v1/content/{contentId}/log", + * "operation": "https://relay.dfos.com/proof/v1/operations/{cid}", + * "blob": "https://relay.dfos.com/content/{contentId}/blob", + * "indexContent": "https://relay.dfos.com/index/v0/content", + * "indexIdentities": "https://relay.dfos.com/index/v0/identities" + * } + * } + */ PublicProtocolInfoOutput: { - /** - * @description Base URL of the read-only DFOS relay serving the identity and content chains for the protocol DIDs this API emits (as `did` on spaces, authors, and future entities). Combine with a `did` to resolve its DID document / identity-chain head (e.g. `{relayUrl}/proof/v1/identities/{did}`). One relay per deployment stage. - * @example https://relay.dfos.com - */ + /** @description Base URL of the read-only relay serving the chains for the DIDs this API emits. Combine it with a `did` (e.g. `{relayUrl}/proof/v1/identities/{did}`). */ relayUrl: string; /** - * @description The DID method used by DFOS protocol identities + * @description The DID method DFOS identities use * @constant */ didMethod: "did:dfos"; /** - * @description Canonical DFOS protocol specification URL + * @description The DFOS protocol specification URL * @constant */ specUrl: "https://protocol.dfos.com/spec"; - /** @description Templates for the relay's proof-plane and index endpoints; substitute the placeholders with values from `protocol` blocks on posts, users, and spaces. */ + /** @description URL templates for the relay proof plane and indexes. Substitute placeholders with values from the `protocol` blocks on posts, users, and spaces. */ endpoints: { /** @description Relay discovery document URL */ wellKnown: string; @@ -2286,15 +3422,32 @@ export interface components { indexIdentities: string; }; }; - /** @description A space the credential subject belongs to */ + /** + * @description A space the subject belongs to + * @example { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "memberCountSummary": "thousands of members" + * } + */ MembershipSpaceOutput: { - /** - * @description The space's entity id (`space_…`) — canonical and stable. - * @example space_6encc4akrze2ah9kntzd9t - */ + /** @description The space's entity id (`space_…`) — canonical and stable. */ id: string; did: components["schemas"]["ProtocolDid"]; - /** @description Effective subdomain — custom domain if set, otherwise the normalized `space-{id}` form. A MUTABLE alias, and for a private space it may not resolve publicly at all; the `id` and `did` are canonical. */ + /** @description Effective subdomain, or the normalized `space-{id}` form. A mutable alias that may not resolve publicly for a private space; `id` and `did` are canonical. */ domain: string; /** @description Space display name, or null */ displayName: string | null; @@ -2304,15 +3457,25 @@ export interface components { avatarUrl: string | null; /** @description Ordered space profile links (may be empty) */ links: components["schemas"]["PublicProfileLinkOutput"][]; - /** @description Worded member-count summary (e.g. "a few dozen members"). DFOS surfaces deliberately avoid exact counts. */ + /** @description Worded member-count summary, e.g. "a few dozen members". Spaces do not publish exact counts. */ memberCountSummary: string; }; - /** @description A group the credential subject belongs to */ + /** + * @description A group the subject belongs to + * @example { + * "id": "group_4kt7cn9vza3fe2h8dr6ac4", + * "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + * "name": "Developers", + * "description": "Building on the DFOS API and protocol.", + * "avatarUrl": null, + * "color": "cyan", + * "memberCount": 42, + * "spaceId": "space_vnzfk7hth9vadc3daahd48", + * "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + * } + */ MembershipGroupOutput: { - /** - * @description The group's entity id (`group_…`) — canonical and stable. - * @example group_79h6z77had2kc68ffdkhac - */ + /** @description The group's entity id (`group_…`) — canonical and stable. */ id: string; did: components["schemas"]["ProtocolDid"]; /** @description Group name */ @@ -2323,23 +3486,42 @@ export interface components { avatarUrl: string | null; /** @description The group's named palette color, or null when unset */ color: ("red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose") | null; - /** - * @description EXACT count of the group's active members — not a worded bucket. Spaces publish a worded `memberCountSummary` because a room's population is ambient; a group is an operational unit (the editors, the moderators, a paid tier) whose size has a real answer its own members already know. Reading it requires the `read:memberships` grant that opens this endpoint. - * @example 7 - */ + /** @description Exact count of the group's active members. Groups publish an exact count; spaces publish the worded `memberCountSummary` instead. */ memberCount: number; - /** - * @description Entity id of the space this group belongs to. Correlate with `space.id` on `GET /memberships` to reassemble the full graph. - * @example space_6encc4akrze2ah9kntzd9t - */ + /** @description Entity id of the space this group belongs to. Correlate with `space.id` on `GET /memberships`. */ spaceId: string; /** * @description Protocol DID of the space this group belongs to — the space's own `did`. - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ spaceDid: string; }; - /** @description One of the credential subject's space memberships */ + /** + * @description One of the subject's space memberships + * @example { + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "memberCountSummary": "thousands of members" + * }, + * "role": "member", + * "groupCount": 1, + * "joinedAt": "2026-01-14T19:02:11.000Z" + * } + */ MembershipOutput: { space: components["schemas"]["MembershipSpaceOutput"]; /** @@ -2347,10 +3529,7 @@ export interface components { * @enum {string} */ role: "owner" | "admin" | "member"; - /** - * @description How many groups the subject belongs to inside this space (ACTIVE group memberships only). Often 0 — most members belong to no group. Walk `GET /group-memberships?space=…` for the groups themselves. - * @example 2 - */ + /** @description How many groups the subject belongs to inside this space. Walk `GET /group-memberships?space=…` for the groups. */ groupCount: number; /** * Format: date-time @@ -2358,18 +3537,82 @@ export interface components { */ joinedAt: string; }; - /** @description A cursor-paginated page of the credential subject's space memberships */ + /** + * @description A cursor-paginated page of the subject's space memberships + * @example { + * "items": [ + * { + * "space": { + * "id": "space_vnzfk7hth9vadc3daahd48", + * "did": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "domain": "home", + * "displayName": "DFOS", + * "description": "The official DFOS of DFOS", + * "avatarUrl": "https://dfos.imgix.net/media/public/8e87h8c37f2vz3v6h7h3at-frame-143726546.png", + * "links": [ + * { + * "url": "https://dfos.com/", + * "label": null, + * "title": "Dark Forest OS", + * "description": null, + * "imageUrl": "https://dfos.imgix.net/media/public/ne979cake8har94728rne7-dfos-private-internet.png" + * } + * ], + * "memberCountSummary": "thousands of members" + * }, + * "role": "member", + * "groupCount": 1, + * "joinedAt": "2026-01-14T19:02:11.000Z" + * }, + * { + * "space": { + * "id": "space_z94a849d9kdftfvv3n9hn7", + * "did": "did:dfos:f3a4ncdta66627c6e2cnhhndan7k882", + * "domain": "rakowwwski", + * "displayName": "POPULAR", + * "description": "Popular is a space we share our creative process and research practices. We get to know our work to get to know ourselves.", + * "avatarUrl": "https://dfos.imgix.net/media/public/e2nfc34a369da2tfatcvf4-32351ed8-9995-48b3-ad72-1ac2fda11f47-1024x1024-2-.jpg", + * "links": [], + * "memberCountSummary": "about a dozen members" + * }, + * "role": "member", + * "groupCount": 0, + * "joinedAt": "2026-04-02T13:35:52.000Z" + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 2 + * } + */ MembershipPageOutput: { /** @description Page of the subject's space memberships */ items: components["schemas"]["MembershipOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; - /** @description One of the credential subject's group memberships */ + /** + * @description One of the subject's group memberships + * @example { + * "group": { + * "id": "group_4kt7cn9vza3fe2h8dr6ac4", + * "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + * "name": "Developers", + * "description": "Building on the DFOS API and protocol.", + * "avatarUrl": null, + * "color": "cyan", + * "memberCount": 42, + * "spaceId": "space_vnzfk7hth9vadc3daahd48", + * "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + * }, + * "role": "admin", + * "joinedAt": "2026-01-14T19:02:11.000Z" + * } + */ GroupMembershipOutput: { group: components["schemas"]["MembershipGroupOutput"]; /** @@ -2383,68 +3626,96 @@ export interface components { */ joinedAt: string; }; - /** @description A cursor-paginated page of the credential subject's group memberships */ + /** + * @description A cursor-paginated page of the subject's group memberships + * @example { + * "items": [ + * { + * "group": { + * "id": "group_4kt7cn9vza3fe2h8dr6ac4", + * "did": "did:dfos:7ha4dr2kc9vt3fe8n6zad4hcv7t2en9", + * "name": "Developers", + * "description": "Building on the DFOS API and protocol.", + * "avatarUrl": null, + * "color": "cyan", + * "memberCount": 42, + * "spaceId": "space_vnzfk7hth9vadc3daahd48", + * "spaceDid": "did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k" + * }, + * "role": "admin", + * "joinedAt": "2026-01-14T19:02:11.000Z" + * } + * ], + * "nextCursor": null, + * "previousCursor": null, + * "totalCount": 1 + * } + */ GroupMembershipPageOutput: { /** @description Page of the subject's group memberships */ items: components["schemas"]["GroupMembershipOutput"][]; - /** @description Opaque cursor for the next page (null if no more results forward). Pass back verbatim as `after`; do not parse. */ + /** @description Cursor for the next page, or null at the end. Pass back verbatim as `after`. */ nextCursor: string | null; - /** @description Opaque cursor for the previous page (null if at the beginning). Optional — may be omitted on responses that do not support backward paging. Pass back verbatim as `before`; do not parse. */ + /** @description Cursor for the previous page, or null at the start. Pass back verbatim as `before`. Omitted where backward paging is unsupported. */ previousCursor?: string | null; - /** @description Total count of matching items (null if not computed). Optional — may be omitted entirely; clients must not depend on its presence. */ + /** @description Total matching items, or null when not computed. May be omitted; do not depend on its presence. */ totalCount?: number | null; }; /** - * @description How the application was resolved when this credential was issued. `jit` = resolved live from the app's `/.well-known/dfos-app.json`, which is how every domain-backed app resolves — there is no registry and no approval gate. `loopback` = the key-proven local tier: a client on somebody's machine that proved it holds the key the grant is addressed to. An open enum like every enum on this API: a member named `approved` existed in an earlier revision and was removed with the registry that backed it; no credential was ever issued under it. + * @description How the application was resolved at consent time. `jit` = live from its `/.well-known/dfos-app.json`; `loopback` = a local client that proved its key. Open enum. * @enum {string} */ PublicCredentialTier: "jit" | "loopback"; - /** @description The presented credential, as the DFOS issuance ledger records it */ + /** + * @description The presented credential. + * @example { + * "subjectDid": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", + * "clientDid": "did:dfos:7ken2dr4ahc9vtfz3n8ce6ad2kr7h9t", + * "scopes": [ + * "read:profile", + * "read:email", + * "read:memberships", + * "read:posts", + * "write:posts" + * ], + * "attenuation": [ + * { + * "resource": "api:api.dfos.com", + * "action": "read:profile,read:email,read:memberships" + * }, + * { + * "resource": "api:api.dfos.com/spaces/9ctvrdn9vedda7efetrhcdakfh4cr2k", + * "action": "read:posts,write:posts" + * } + * ], + * "tier": "jit", + * "domain": "fieldnotes.example", + * "issuedAt": "2026-09-01T16:20:00.000Z", + * "expiresAt": "2026-11-30T16:20:00.000Z" + * } + */ CredentialIntrospectionOutput: { /** - * @description Protocol DID of the user who ISSUED this credential — the subject every gated route serves. The same `did` `GET /profile` returns. - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @description Protocol DID of the user who issued this credential — the subject every gated route serves. + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ subjectDid: string; /** - * @description Protocol DID of the application this credential was issued TO — the audience. A credential is inert without this identity’s key; it is not a bearer token. - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @description Protocol DID of the application this credential was issued to. Not a bearer token: inert without that identity’s key. + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ clientDid: string; - /** - * @description The actions this grant carries, e.g. `["read:profile","read:memberships"]`, in DFOS’s canonical order. This is the authoritative list — a route refuses with `403` when the action it needs is absent here. Treat unrecognized members as opaque strings. - * @example [ - * "read:profile", - * "read:memberships" - * ] - */ + /** @description The actions this grant carries, e.g. `["read:profile","read:memberships"]`. A route refuses with `403` when the action it needs is absent. Unrecognized members are opaque strings. */ scopes: string[]; - /** - * @description THE GRANT ITSELF, exactly as it was signed — one entry per resource. `scopes` above says WHAT this credential carries; this says what it carries WHERE, and it is the authoritative answer to both. A credential minted before grants had places carries a single `api:` entry; one narrowed to specific spaces carries one child entry each, and may carry no `api:` entry at all. Treat unrecognized resource forms as opaque strings and do not infer coverage from their shape — the grant you hold is the authority, not a rule you derived from it. - * @example [ - * { - * "resource": "api:api.dfos.com", - * "action": "read:profile,read:email" - * } - * ] - */ + /** @description The grant as signed, one entry per resource: `scopes` says what this credential carries, this says where. Treat unrecognized resource forms as opaque strings. */ attenuation: { - /** - * @description The resource this entry grants over. `api:` is the whole API — for a space-level action that means EVERY space the user belongs to, including ones they join later. `api:/spaces/<31-char space id>` is one named space. - * @example api:api.dfos.com - */ + /** @description The resource this entry grants over. `api:` is the whole API, including spaces the user joins later; `api:/spaces/` is one space. */ resource: string; - /** - * @description The comma-separated SET of action tokens this entry carries, in DFOS’s canonical order. - * @example read:profile,read:email - */ + /** @description Comma-separated set of action tokens this entry carries, in canonical order. */ action: string; }[]; tier: components["schemas"]["PublicCredentialTier"]; - /** - * @description Bare hostname the grant was issued to, or `null`. **A null domain means a LOCAL application** — the `loopback` tier has no domain because a local client proved a key rather than an origin, so there is no hostname that would be true to show. Fall back to `clientDid` rather than inventing one. - * @example example.com - */ + /** @description Bare hostname the grant was issued to, or `null`. Null on the `loopback` tier, which proved a key rather than an origin; identify the holder by `clientDid`. */ domain: string | null; /** * Format: date-time @@ -2453,12 +3724,285 @@ export interface components { issuedAt: string; /** * Format: date-time - * @description When this credential lapses on its own (ISO 8601 UTC). Expiry is the backstop, not the lever: a user revoking a grant ends it on the app’s very next request, long before this. + * @description When this credential lapses on its own (ISO 8601 UTC). A revoked grant ends sooner, on the app’s next request. */ expiresAt: string; }; + /** @description The one error envelope every non-2xx response carries. Additional members are permitted and must be tolerated: the wire also carries a transport marker `defined`, which is an oRPC client concern rather than part of this contract. */ + ErrorEnvelope: { + /** @description A stable machine-readable error code, e.g. `E_NOT_FOUND`. This and `status` are what a client branches on. */ + code: string; + /** @description The HTTP status, repeated in the body. */ + status: number; + /** @description A human-readable explanation, safe to log and to show a developer. NOT part of the contract — the wording of any given refusal can change, so never branch on it. */ + message: string; + /** @description Machine-readable detail; shape depends on `code` */ + data?: unknown; + }; + }; + responses: { + /** @description Not found. A missing space and a non-public one return the same 404, so the two are indistinguishable. */ + NotFound: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_NOT_FOUND", + * "status": 404, + * "message": "Cannot resolve space -- not found" + * } + */ + "application/json": { + /** @constant */ + code: "E_NOT_FOUND"; + /** @constant */ + status: 404; + message: string; + }; + }; + }; + /** @description Not found. "No such space or group" and "the granting user is not in it" are the same answer — this credential discloses the user's own memberships and nothing else. */ + MembershipNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_NOT_FOUND", + * "status": 404, + * "message": "Not found" + * } + */ + "application/json": { + /** @constant */ + code: "E_NOT_FOUND"; + /** @constant */ + status: 404; + message: string; + }; + }; + }; + /** @description The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve. Request bodies are closed — an unknown member is refused, not ignored. */ + BadRequest: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_INVALID_REQUEST", + * "status": 400, + * "message": "Nothing to edit" + * } + */ + "application/json": { + /** @constant */ + code: "E_INVALID_REQUEST"; + /** @constant */ + status: 400; + message: string; + }; + }; + }; + /** @description The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer "this ceremony is not open"; only a bad signature consumes the ceremony, so any other refusal leaves the code live to re-resolve and retry. */ + KeyProofRejected: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_INVALID_REQUEST", + * "status": 400, + * "message": "this ceremony is not open" + * } + */ + "application/json": { + /** @constant */ + code: "E_INVALID_REQUEST"; + /** @constant */ + status: 400; + message: string; + }; + }; + }; + /** @description The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body; `WWW-Authenticate` is best-effort — infrastructure between your client and this API can rename or drop it — so branch on the status and the error body. */ + ProofRequired: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_AUTHENTICATION_FAILED", + * "status": 401, + * "message": "a DFOS request proof is required for this endpoint" + * } + */ + "application/json": { + /** @constant */ + code: "E_AUTHENTICATION_FAILED"; + /** @constant */ + status: 401; + message: string; + }; + }; + }; + /** @description The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ + Forbidden: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_UNAUTHORIZED", + * "status": 403, + * "message": "credential has been revoked" + * } + */ + "application/json": { + /** @constant */ + code: "E_UNAUTHORIZED"; + /** @constant */ + status: 403; + message: string; + }; + }; + }; + /** @description This `jti` was already accepted, so the first attempt may have succeeded. Re-read state before retrying, and retry with a new `jti`. */ + Replayed: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_CONFLICT", + * "status": 409, + * "message": "request already seen" + * } + */ + "application/json": { + /** @constant */ + code: "E_CONFLICT"; + /** @constant */ + status: 409; + message: string; + }; + }; + }; + /** @description The request body exceeds the maximum size this endpoint will authenticate. */ + PayloadTooLarge: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_INVALID_REQUEST", + * "status": 413, + * "message": "Request body exceeds the maximum size this endpoint will authenticate." + * } + */ + "application/json": { + /** @constant */ + code: "E_INVALID_REQUEST"; + /** @constant */ + status: 413; + message: string; + }; + }; + }; + /** @description A request body must be `application/json` (optionally `; charset=utf-8`) and must not carry a `Content-Encoding` other than `identity`. */ + UnsupportedMediaType: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_INVALID_REQUEST", + * "status": 415, + * "message": "Request bodies on this API must be `application/json` (optionally `; charset=utf-8`). Exactly one media type is served so that the octets a proof binds have exactly one parse." + * } + */ + "application/json": { + /** @constant */ + code: "E_INVALID_REQUEST"; + /** @constant */ + status: 415; + message: string; + }; + }; + }; + /** @description Rate limit exceeded — retry after `retryAfterMs`. */ + RateLimited: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_RATE_LIMITED", + * "status": 429, + * "message": "Rate limit exceeded", + * "data": { + * "scope": "global", + * "retryAfterMs": 1200 + * } + * } + */ + "application/json": { + /** @constant */ + code: "E_RATE_LIMITED"; + /** @constant */ + status: 429; + message: string; + data: { + /** @description Which per-IP budget was exhausted */ + scope: string; + /** @description Milliseconds to wait before retrying */ + retryAfterMs: number; + }; + }; + }; + }; + /** @description Service temporarily unavailable — this request could not be checked, so it was refused rather than allowed. Retry shortly. */ + ServiceUnavailable: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "code": "E_SERVICE_UNAVAILABLE", + * "status": 503, + * "message": "Rate limit service temporarily unavailable" + * } + */ + "application/json": { + /** @constant */ + code: "E_SERVICE_UNAVAILABLE"; + /** @constant */ + status: 503; + message: string; + }; + }; + }; + /** @description Any other error the API itself produces. Branch on `code` and `status`; the enumerated statuses on each operation are what this contract promises. An intermediary may answer with a non-JSON 5xx body — check `Content-Type` before parsing. */ + Error: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorEnvelope"]; + }; + }; }; - responses: never; parameters: never; requestBodies: never; headers: never; @@ -2486,89 +4030,10 @@ export interface operations { "application/json": components["schemas"]["SpaceOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "spaces.listSpaces": { @@ -2597,63 +4062,9 @@ export interface operations { "application/json": components["schemas"]["SpaceDiscoveryPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.listPosts": { @@ -2685,89 +4096,10 @@ export interface operations { "application/json": components["schemas"]["PublicPostPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.createPost": { @@ -2783,13 +4115,13 @@ export interface operations { content: { "application/json": { /** - * @description Topic ID to post into, from `GET /spaces/{space}/topics`. Required — this API never picks a topic for you, because which room a post lands in is a decision the writer makes. - * @example topic_6encc4akrze2ah9kntzd9t + * @description Topic ID to post into. `GET /spaces/{space}/topics` enumerates world-readable topics only, so it supplies ids for public spaces; for a member or private topic, take the id from `post.topics[]` on a credentialed read. + * @example topic_6c2efd472dvt8rf9k4ftcc */ topic: string; /** - * @description Post title. Its presence is what makes this a `long-post`; omit it for a `short-post`. - * @example Building a more generous internet + * @description Post title. Its presence makes the post a `long-post`. + * @example DFOS beyond DFOS: sign-in, domains, and your own keys */ title?: string; /** @description Post body (markdown) */ @@ -2798,7 +4130,7 @@ export interface operations { }; }; responses: { - /** @description OK */ + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -2807,245 +4139,16 @@ export interface operations { "application/json": components["schemas"]["PublicPostOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.getPost": { @@ -3069,89 +4172,10 @@ export interface operations { "application/json": components["schemas"]["PublicGetPostOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.deletePost": { @@ -3175,245 +4199,16 @@ export interface operations { "application/json": components["schemas"]["PublicDeletedOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.editPost": { @@ -3435,7 +4230,7 @@ export interface operations { body?: string; /** * @description Move the post to this topic ID, within the same space. - * @example topic_6encc4akrze2ah9kntzd9t + * @example topic_6c2efd472dvt8rf9k4ftcc */ topic?: string; }; @@ -3451,507 +4246,49 @@ export interface operations { "application/json": components["schemas"]["PublicPostOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; + }; + }; + "posts.upvotePost": { + parameters: { + query?: never; + header?: never; + path: { + space: string; + postId: string; }; - /** @description 403 */ - 403: { + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - }; - }; - "posts.upvotePost": { - parameters: { - query?: never; - header?: never; - path: { - space: string; - postId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PublicUpvoteStateOutput"]; - }; - }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; + "application/json": components["schemas"]["PublicUpvoteStateOutput"]; }; }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "posts.removePostUpvote": { @@ -3975,245 +4312,16 @@ export interface operations { "application/json": components["schemas"]["PublicUpvoteStateOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "comments.listPostComments": { @@ -4243,89 +4351,10 @@ export interface operations { "application/json": components["schemas"]["PublicCommentPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "comments.createComment": { @@ -4345,14 +4374,14 @@ export interface operations { body: string; /** * @description Reply to this comment rather than to the post. Must be a comment on the same root post. - * @example post_6encc4akrze2ah9kntzd9t + * @example comment_9rze4tk2vdc7fa38nhe6c2 */ parentCommentId?: string; }; }; }; responses: { - /** @description OK */ + /** @description Created */ 201: { headers: { [name: string]: unknown; @@ -4361,245 +4390,16 @@ export interface operations { "application/json": components["schemas"]["PublicCommentWriteOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "comments.deleteComment": { @@ -4623,245 +4423,16 @@ export interface operations { "application/json": components["schemas"]["PublicDeletedOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "comments.editComment": { @@ -4892,245 +4463,16 @@ export interface operations { "application/json": components["schemas"]["PublicCommentWriteOutput"]; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "comments.upvoteComment": { @@ -5151,510 +4493,52 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PublicUpvoteStateOutput"]; - }; - }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - }; - }; - "comments.removeCommentUpvote": { - parameters: { - query?: never; - header?: never; - path: { - space: string; - commentId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PublicUpvoteStateOutput"]; - }; - }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request could not be acted on as sent: an edit that names nothing to change, a body that fails validation, or a field this API does not serve (request bodies are closed, so an unknown member is refused rather than ignored). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 409 */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_CONFLICT"; - /** @constant */ - status: 409; - /** @default This request was already seen — its `jti` was spent inside the proof freshness window. The earlier attempt may have succeeded, so re-read state instead of retrying. A genuine retry must carry a NEW `jti`. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 415 */ - 415: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 415; - /** @default Unsupported media type. A request body must be `application/json` (optionally `; charset=utf-8`), and must not carry a `Content-Encoding` other than `identity` — a proof binds the raw request octets, and this API does not reverse an encoding before checking that binding. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; + "application/json": components["schemas"]["PublicUpvoteStateOutput"]; }; }; - /** @description 503 */ - 503: { + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; + }; + }; + "comments.removeCommentUpvote": { + parameters: { + query?: never; + header?: never; + path: { + space: string; + commentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; + "application/json": components["schemas"]["PublicUpvoteStateOutput"]; }; }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + 409: components["responses"]["Replayed"]; + 413: components["responses"]["PayloadTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "feed.listFeed": { @@ -5679,63 +4563,9 @@ export interface operations { "application/json": components["schemas"]["FeedPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "pages.listPages": { @@ -5762,89 +4592,10 @@ export interface operations { "application/json": components["schemas"]["PublicPagePageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "pages.getPage": { @@ -5868,89 +4619,10 @@ export interface operations { "application/json": components["schemas"]["PublicPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "events.listSpaceEvents": { @@ -5979,89 +4651,10 @@ export interface operations { "application/json": components["schemas"]["PublicEventPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "events.getSpaceEvent": { @@ -6087,89 +4680,10 @@ export interface operations { "application/json": components["schemas"]["PublicEventOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "events.listUpcomingEvents": { @@ -6196,63 +4710,9 @@ export interface operations { "application/json": components["schemas"]["PublicEventPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "products.listProducts": { @@ -6279,89 +4739,10 @@ export interface operations { "application/json": components["schemas"]["PublicProductPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "products.listAllProducts": { @@ -6386,63 +4767,9 @@ export interface operations { "application/json": components["schemas"]["PublicProductFeedPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "products.getProduct": { @@ -6466,89 +4793,10 @@ export interface operations { "application/json": components["schemas"]["PublicProductOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "releases.listSpaceReleases": { @@ -6556,108 +4804,29 @@ export interface operations { query?: { limit?: number; after?: string; - before?: string; - }; - header?: never; - path: { - space: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PublicReleasePageOutput"]; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; + before?: string; + }; + header?: never; + path: { + space: string; }; - /** @description 503 */ - 503: { + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; + "application/json": components["schemas"]["PublicReleasePageOutput"]; }; }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "releases.listReleases": { @@ -6682,63 +4851,9 @@ export interface operations { "application/json": components["schemas"]["PublicReleaseFeedPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "releases.getSpaceRelease": { @@ -6762,89 +4877,10 @@ export interface operations { "application/json": components["schemas"]["PublicReleaseOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "topics.listTopics": { @@ -6871,89 +4907,10 @@ export interface operations { "application/json": components["schemas"]["PublicTopicPageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "topics.getTopic": { @@ -6977,89 +4934,10 @@ export interface operations { "application/json": components["schemas"]["PublicTopicOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "users.getUser": { @@ -7082,89 +4960,10 @@ export interface operations { "application/json": components["schemas"]["PublicUserOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "users.listUsers": { @@ -7189,63 +4988,9 @@ export interface operations { "application/json": components["schemas"]["PublicUserPageOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "users.getUserSpaces": { @@ -7272,89 +5017,10 @@ export interface operations { "application/json": components["schemas"]["PublicUserSpacePageOutput"]; }; }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. A missing space and a non-public (private) one return a byte-identical 404 by design — the two are deliberately indistinguishable (no existence leak). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "profile.getOwnProfile": { @@ -7374,159 +5040,30 @@ export interface operations { content: { "application/json": { did: components["schemas"]["ProtocolDid"]; - /** @description The user's handle, or null if unset. A MUTABLE alias — the `did` is the canonical, stable identifier to store. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof. */ + /** @description The user's handle, or null if unset. A mutable alias; the `did` is canonical. Absent without `read:profile`. */ username?: string | null; - /** @description Display name, or null. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof. */ + /** @description Display name, or null. Absent without `read:profile`. */ displayName?: string | null; - /** @description Profile bio / description, or null. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof. */ + /** @description Profile bio, or null. Absent without `read:profile`. */ description?: string | null; - /** @description Permanent public CDN URL for the profile avatar, or null when they have none. NOT a signed URL — an avatar is public media, so this link is stable while the media is referenced. ABSENT (not null) under a credential that does not carry `read:profile`; always present under an identity proof. */ + /** @description Permanent public CDN URL for the profile avatar, or null. Not a signed URL. Absent without `read:profile`. */ avatarUrl?: string | null; - /** @description The account email of the authenticated subject. PRIVATE — it is served only under a verified proof: to the subject themselves under an identity proof, and to a third party only when the credential the subject issued carries `read:email`. ABSENT (not null) otherwise. Revoking a credential ends that app’s access immediately; it does not un-share what was already read. */ + /** @description The subject's account email, served only under a verified proof. Absent without `read:email`. */ email?: string; /** * Format: date-time - * @description When the user joined DFOS (ISO 8601 UTC). ABSENT under a credential that does not carry `read:profile`; always present under an identity proof. + * @description When the user joined DFOS (ISO 8601 UTC). Absent without `read:profile`. */ createdAt?: string; }; }; }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "memberships.listMemberships": { @@ -7549,144 +5086,15 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MembershipPageOutput"]; - }; - }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; + "application/json": components["schemas"]["MembershipPageOutput"]; }; }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "memberships.getMembership": { @@ -7709,167 +5117,13 @@ export interface operations { "application/json": components["schemas"]["MembershipOutput"]; }; }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. "No such space or group" and "the granting user is not in it" are deliberately indistinguishable — this credential discloses the user's own memberships, never the existence of anything else. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["MembershipNotFound"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "memberships.listGroupMemberships": { @@ -7896,141 +5150,12 @@ export interface operations { "application/json": components["schemas"]["GroupMembershipPageOutput"]; }; }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "memberships.getGroupMembership": { @@ -8053,167 +5178,13 @@ export interface operations { "application/json": components["schemas"]["GroupMembershipOutput"]; }; }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 404 */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_NOT_FOUND"; - /** @constant */ - status: 404; - /** @default Not found. "No such space or group" and "the granting user is not in it" are deliberately indistinguishable — this credential discloses the user's own memberships, never the existence of anything else. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["MembershipNotFound"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "credential.getCredential": { @@ -8234,141 +5205,12 @@ export interface operations { "application/json": components["schemas"]["CredentialIntrospectionOutput"]; }; }; - /** @description 401 */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_AUTHENTICATION_FAILED"; - /** @constant */ - status: 401; - /** @default The DFOS request proof was missing, malformed, stale, or did not verify. Sign a fresh proof over this exact method, host, path, and body. Branch on this status and body: responses normally also carry a `WWW-Authenticate: DFOS` challenge header, but that header is best-effort and may arrive remapped, so it must not be the thing a client keys on. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 403 */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_UNAUTHORIZED"; - /** @constant */ - status: 403; - /** @default The request proof verified but the credential does not authorize this request — it is expired, revoked, not issued by this platform, or its attenuation does not cover this action on this host. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 413 */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 413; - /** @default The request body exceeds the maximum this endpoint will authenticate. A proof binds the body it was signed over, so an unhashable body cannot be authenticated at any size — the cap is refused before the signature is checked, not after. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 401: components["responses"]["ProofRequired"]; + 403: components["responses"]["Forbidden"]; + 413: components["responses"]["PayloadTooLarge"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "keyProof.present": { @@ -8382,17 +5224,17 @@ export interface operations { content: { "application/json": { /** - * @description The 8-character ceremony code the user carried out of the DFOS app — the same code the well-known lookup was resolved against - * @example K7M2QXPA + * @description The 8-character ceremony code from the DFOS app, as resolved at the well-known lookup. + * @example K7RD4HEA */ code: string; /** - * @description The compact JWS proving possession, `typ` `did:dfos:key-add`. The header carries `alg` (`EdDSA`) and `typ` and nothing else — a `kid` is refused, because the verifying key comes from the payload. The payload carries exactly `nonce`, `audience`, `did`, `roleSet`, `prevCID`, `publicKeyMultibase` and `timestamp`, in that order: it is byte-compared against its canonical serialization, so the member order is part of the contract. At most 4096 bytes + * @description The compact JWS proving possession, `typ` `did:dfos:key-add`. The header carries `alg` (`EdDSA`) and `typ` only; a `kid` is refused. Max 4096 bytes. * @example eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmtleS1hZGQifQ... */ envelope: string; /** - * @description Optional label for the key, shown to the user when they decide whether to adopt it — typically the machine it lives on. **Not part of the signed envelope**: it is unsigned platform metadata, it is not one of the payload members, and nothing about it affects whether the key is admitted. Trimmed; at most 200 characters, the same bound the settings rename enforces. Omitted, empty, or whitespace-only gets the default label `CLI signing key`. The user can rename it afterwards + * @description Optional label for the key, shown to its owner at adoption. Unsigned; not part of the envelope. Trimmed, max 200 characters, default `CLI signing key`. * @example work laptop */ description?: string; @@ -8408,120 +5250,34 @@ export interface operations { content: { "application/json": { /** - * @description The envelope verified and is stored. **Nothing is on the identity chain yet** — the owner of the identity has to adopt it in their DFOS settings, and may instead reject it. Poll `/key-proof/status` to find out which + * @description The envelope verified and is stored. Nothing is on the identity chain yet; the owner adopts or rejects it in their DFOS settings. * @constant */ status: "presented"; /** @description The identity a key-add ceremony is for */ adopts: { /** - * @description The protocol DID of the identity chain this key would be added to - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @description The protocol DID of the identity this key would be added to + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ did: string; - /** - * @description The identity’s username, or null if it has none - * @example bvalosek - */ + /** @description The identity’s username, or null. */ handle: string | null; - /** - * @description The identity’s display name, or null - * @example Brandon - */ + /** @description The identity’s display name, or null */ displayName: string | null; }; /** * Format: date-time - * @description When the ceremony lapses. If nobody adopts or rejects by then it simply expires and nothing was added - * @example 2026-08-28T17:10:00Z + * @description When the ceremony lapses. Nothing is added if nobody adopts or rejects by then. */ expiresAt: string; }; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer "this ceremony is not open" — that is a single statement, so there is nothing to learn by varying it. Only a bad SIGNATURE consumes the ceremony; every other refusal leaves the code live, so re-resolve and try again. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["KeyProofRejected"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "keyProof.status": { @@ -8543,116 +5299,31 @@ export interface operations { content: { "application/json": { /** - * @description `pending` = nobody has presented an envelope. `presented` = yours verified and the identity’s owner is deciding. `adopted` = they adopted it and the key is on the chain (see `onAdopted`). `rejected` = they declined; **nothing was added**, and that is not an error. `failed` = an envelope was refused at the signature; the ceremony is burned. `expired` = the ten minutes ran out + * @description `pending` = nothing presented; `presented` = verified, the owner is deciding; `adopted` = on the chain (see `onAdopted`); `rejected` = declined, nothing added; `failed` = bad signature, ceremony burned; `expired` = the ceremony’s ten minutes ran out. * @enum {string} */ status: "pending" | "presented" | "adopted" | "rejected" | "failed" | "expired"; - /** @description Present only while `presented`. True when the chain head has moved since your envelope was signed, so it can no longer be adopted as-is. **Re-resolve the code and present a fresh envelope for the same key** — that is admitted, and the owner’s approval carries across it */ + /** @description Present only while `presented`. True when the chain head has moved and the envelope can no longer be adopted as-is. Present a fresh envelope for the same key. */ stale?: boolean; /** @description Present only on `adopted` */ onAdopted?: { /** * @description The identity chain the key was added to - * @example did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc + * @example did:dfos:9ctvrdn9vedda7efetrhcdakfh4cr2k */ did: string; - /** - * @description The chain-local id the key was given. The DID URL a verifier sees is `#` - * @example key_4h2ndv79fckae3rz6t8v2d - */ + /** @description The chain-local id the key was given. The DID URL a verifier sees is `#` */ keyId: string; - /** - * @description CID of the operation that added the key, with your envelope embedded in it — the receipt anyone can go read - * @example bafyreib2rxk3rhqhbvpaowvtdcnqvbtwbctvvbrqjhkgvhvcnpi7zdkeqm - */ + /** @description CID of the operation that added the key, with the envelope embedded in it. */ chainOpCID: string; }; }; }; }; - /** @description 400 */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_INVALID_REQUEST"; - /** @constant */ - status: 400; - /** @default The request was refused. Unknown, expired, spent and wrong-nonce ceremonies all answer "this ceremony is not open" — that is a single statement, so there is nothing to learn by varying it. Only a bad SIGNATURE consumes the ceremony; every other refusal leaves the code live, so re-resolve and try again. */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 400: components["responses"]["KeyProofRejected"]; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; "protocol.getProtocolInfo": { @@ -8673,63 +5344,9 @@ export interface operations { "application/json": components["schemas"]["PublicProtocolInfoOutput"]; }; }; - /** @description 429 */ - 429: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_RATE_LIMITED"; - /** @constant */ - status: 429; - /** @default Rate limit exceeded — retry after `retryAfterMs`. */ - message: string; - data: { - /** @description Which per-IP budget was exhausted */ - scope: string; - /** @description Milliseconds to wait before retrying */ - retryAfterMs: number; - }; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; - /** @description 503 */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @constant */ - defined: true; - /** @constant */ - code: "E_SERVICE_UNAVAILABLE"; - /** @constant */ - status: 503; - /** @default Service temporarily unavailable — the rate-limit store was unreachable (fail-closed). */ - message: string; - data?: unknown; - } | { - /** @constant */ - defined: false; - code: string; - status: number; - message: string; - data?: unknown; - }; - }; - }; + 429: components["responses"]["RateLimited"]; + 503: components["responses"]["ServiceUnavailable"]; + default: components["responses"]["Error"]; }; }; } diff --git a/tests/client.spec.ts b/tests/client.spec.ts index 69ba016..4a9a783 100644 --- a/tests/client.spec.ts +++ b/tests/client.spec.ts @@ -122,7 +122,12 @@ describe('createDfosApi', () => { it('surfaces a non-2xx body as error, not data', async () => { const { fetch } = stubFetch( - { defined: true, code: 'E_RATE_LIMITED', status: 429, message: 'Rate limit exceeded' }, + { + code: 'E_RATE_LIMITED', + status: 429, + message: 'Rate limit exceeded', + data: { scope: 'ip', retryAfterMs: 1000 }, + }, { status: 429 }, ); const api = createDfosApi({ fetch });