diff --git a/.agents/skills/hypercerts-api/SKILL.md b/.agents/skills/hypercerts-api/SKILL.md index 4ab77df..ac375f2 100644 --- a/.agents/skills/hypercerts-api/SKILL.md +++ b/.agents/skills/hypercerts-api/SKILL.md @@ -20,7 +20,7 @@ Read the relevant reference, then inspect the exact operation in the [bundled Op ## Bundled API contract -- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 59 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately. +- [`references/openapi.json`](references/openapi.json) is a self-contained snapshot of all 61 query operations, including parameters, response schemas, declared XRPC errors, and their referenced schema definitions. The included contract makes the skill self-contained when installed separately. - To inspect an exact contract, find `/xrpc/` in the snapshot, read its operation, and follow any `#/components/schemas/...` references by schema name. Each operation and schema is stored on its own line for focused lookup. - The snapshot describes declared behavior, not endpoint availability or data freshness on a particular host. Its OpenAPI `info.version` is not the deployed server's version. - Use the API directly when a task needs live indexed results. Do not send extra diagnostic probes just to validate deployment; a deployment-specific availability check requires explicit approval for the target. diff --git a/.agents/skills/hypercerts-api/references/endpoint-map.md b/.agents/skills/hypercerts-api/references/endpoint-map.md index de40fe2..2b03e00 100644 --- a/.agents/skills/hypercerts-api/references/endpoint-map.md +++ b/.agents/skills/hypercerts-api/references/endpoint-map.md @@ -1,6 +1,6 @@ # Endpoint map: choose by consumer task -This task index lists the 59 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment. +This task index lists the 61 public query NSIDs in the bundled [OpenAPI contract](openapi.json). It helps choose an operation but does not replace its exact contract: inspect that operation and its referenced schemas for required parameters, types, defaults, limits, filter semantics, response fields, and declared errors. The static snapshot describes the contract, not availability or freshness on a selected deployment. ## Fast selection rules @@ -8,7 +8,7 @@ This task index lists the 59 public query NSIDs in the bundled [OpenAPI contract - **Known actor DIDs in a batch:** prefer `getProfiles` or `getOrganizations`; batch results may have nullable per-actor values. Their batch inputs are DIDs, not handles. - **Browse or filter without free text:** choose `list...` in the relevant domain. - **Text discovery:** use `search...` only where listed below. A search query is not interchangeable with a list filter, and search semantics vary by endpoint. -- **Relationship checks:** use `get...Follow` for a single pair; list queries answer who follows whom or which entities an actor follows. +- **Relationship checks:** use `get...Follow` for a single pair, or `getFollows` / `getEntityFollows` to check repeated target values for one actor. List queries answer who follows whom or which entities an actor follows. - **Feed reactions:** use `getLikes` or `getReposts` for distinct actors reacting to a subject URI, or `getActorLikes` / `getActorReposts` for an actor's own raw representative reactions. Inspect the exact operation in the bundled OpenAPI contract before constructing a request: names such as `authors`, `actors`, `uri`, and `collection` have endpoint-specific meanings. Do not infer that a filter, search operation, output array name, or error exists just because a related endpoint has one. @@ -69,14 +69,16 @@ Choose `listCollectionItems` when the caller needs the referenced item versions, An `unanswered` award is a successful indexed award result, not a not-found response. See [the badge recipe](recipes.md#read-badge-award-response-status). -## Follow relationships (7) +## Follow relationships (9) | User task | Query NSID | Practical choice | |---|---|---| | Check one actor-to-actor follow | `app.certified.graph.getFollow` | Supply actor and subject DIDs; the representative `follow` may be null when none is indexed. | +| Check one actor against many subject DIDs | `app.certified.graph.getFollows` | Supply 1–100 repeated `subjects` values; each `{subject, follow}` result preserves input order and duplicates. | | List an actor's followers | `app.certified.graph.listActorFollowers` | `actor` is the followed actor DID. | | List actors an actor follows | `app.certified.graph.listActorFollowing` | `actor` is the follower DID. | | Check one actor-to-entity follow | `app.certified.graph.getEntityFollow` | Supply actor DID and entity AT-URI; check its nullable/error contract. | +| Check one actor against many entity AT-URIs | `app.certified.graph.getEntityFollows` | Supply 1–100 repeated `entities` values; matching is by URI, each `{entity, follow}` preserves input order and duplicates, and the encoded GET request-target is capped at 7,000 bytes. | | List actors following an entity | `app.certified.graph.listEntityFollowers` | `entity` is an entity AT-URI. | | List entities an actor follows | `app.certified.graph.listEntityFollowing` | `actor` is an actor DID; targets are hydrated from indexed records. | | Browse recent raw follow records | `app.certified.graph.listRecentFollows` | Beta feed across publishers; `before` and pagination have distinct feed semantics. Not a per-actor relationship check. | diff --git a/.agents/skills/hypercerts-api/references/openapi.json b/.agents/skills/hypercerts-api/references/openapi.json index 8b4da9d..53fbc86 100644 --- a/.agents/skills/hypercerts-api/references/openapi.json +++ b/.agents/skills/hypercerts-api/references/openapi.json @@ -1,68 +1,70 @@ { "openapi": "3.1.0", "info": {"title":"Hypercerts API","version":"contract-snapshot","description":"Public, read-only Hypercerts XRPC query endpoints. This static contract snapshot describes declared behavior, not availability on a particular deployment."}, - "servers": [{"url":"https://api.hypercerts.dev","description":"Production API"},{"url":"https://api.staging.hypercerts.dev","description":"Staging API"}], + "servers": [{"url":"https://api.test.hypercerts.dev","description":"API test server (default)"}], "tags": [{"name":"app.certified.actor"},{"name":"app.certified.badge"},{"name":"app.certified.feed"},{"name":"app.certified.graph"},{"name":"app.certified.link"},{"name":"app.certified.location"},{"name":"org.hypercerts.claim"},{"name":"org.hypercerts.collection"},{"name":"org.hypercerts.context"},{"name":"org.hypercerts.entity"},{"name":"org.hypercerts.funding"},{"name":"org.hypercerts.vocab"},{"name":"org.hypercerts.workscope"}], "paths": { - "/xrpc/app.certified.actor.getOrganization": {"get":{"operationId":"app_certified_actor_getOrganization","tags":["app.certified.actor"],"description":"Gets an actor's indexed organization sidecar and associated profile by DID. Authentication is not required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor to look up.","schema":{"type":"string","format":"did"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getOrganization.output"}}}}},"x-lexicon-id":"app.certified.actor.getOrganization","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID is invalid or an unsupported parameter was supplied."},{"name":"RecordNotFound","description":"No indexed organization sidecar exists for this DID."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, - "/xrpc/app.certified.actor.getOrganizations": {"get":{"operationId":"app_certified_actor_getOrganizations","tags":["app.certified.actor"],"description":"Returns an indexed organization sidecar and associated profile for each supplied actor DID. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":true,"description":"Actor DIDs to look up, supplied as repeated unbracketed query values.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.getOrganizations","x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID or query parameter is invalid."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecars or associated profiles could not be queried."}]}}, - "/xrpc/app.certified.actor.getProfile": {"get":{"operationId":"app_certified_actor_getProfile","tags":["app.certified.actor"],"description":"Gets an indexed Certified profile by DID or handle. Handles resolve through the configured resolver's com.atproto.identity.resolveHandle endpoint; no DID document is fetched for independent verification. Authentication is not required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID or handle of the profile actor.","schema":{"type":"string","format":"at-identifier"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getProfile.output"}}}}},"x-lexicon-id":"app.certified.actor.getProfile","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor is not a valid DID or handle, or an unsupported parameter was supplied."},{"name":"HandleResolverConfigError","description":"HYPERCERTS_HANDLE_RESOLVER_URL is missing or invalid."},{"name":"HandleResolutionFailed","description":"The handle could not be resolved to a valid DID."},{"name":"RecordNotFound","description":"The actor was identified, but no indexed Certified profile exists."},{"name":"ProfileQueryFailed","description":"The indexed profile could not be queried."}]}}, - "/xrpc/app.certified.actor.getProfiles": {"get":{"operationId":"app_certified_actor_getProfiles","tags":["app.certified.actor"],"description":"Returns an indexed Certified profile for each supplied actor DID. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":true,"description":"Actor DIDs to look up, supplied as repeated unbracketed query values.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.getProfiles","x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID or query parameter is invalid."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, - "/xrpc/app.certified.actor.listOrganizations": {"get":{"operationId":"app_certified_actor_listOrganizations","tags":["app.certified.actor"],"description":"Lists actors with indexed organization sidecars matching the supplied filters. Filters combine with AND, and values within each array filter are ORed. Authentication is not required.","parameters":[{"name":"organizationTypes","in":"query","required":false,"description":"Exact organizationType values; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":128}},"style":"form","explode":true},{"name":"visibility","in":"query","required":false,"description":"Exact visibility match; omission is unrestricted, but records with no visibility value do not match.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.listOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.listOrganizations","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or scalar parameter is invalid or repeated, or an unsupported parameter was supplied."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, - "/xrpc/app.certified.actor.listProfiles": {"get":{"operationId":"app_certified_actor_listProfiles","tags":["app.certified.actor"],"description":"Lists indexed Certified profiles with pagination. Authentication is not required.","parameters":[{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep parameters unchanged between pages. Pages are not a snapshot.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.listProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.listProfiles","x-lexicon-errors":[{"name":"InvalidRequest","description":"A pagination parameter is invalid, repeated where scalar, or unsupported."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, - "/xrpc/app.certified.actor.searchOrganizations": {"get":{"operationId":"app_certified_actor_searchOrganizations","tags":["app.certified.actor"],"description":"Searches organizations by associated profile text and optional filters. Filters combine with AND, and values within each array filter are ORed. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Trimmed text is matched literally and case-insensitively as a substring in either displayName or description. Blank text is unrestricted; nonblank text requires an associated profile.","schema":{"type":"string"}},{"name":"actors","in":"query","required":false,"description":"Actor DIDs to include; values are ORed and handles are not resolved.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"organizationTypes","in":"query","required":false,"description":"Exact organizationType values; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":128}},"style":"form","explode":true},{"name":"visibility","in":"query","required":false,"description":"Exact visibility match; omission is unrestricted, but records with no visibility value do not match.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.searchOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.searchOrganizations","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or scalar parameter is invalid or repeated, or an unsupported parameter was supplied."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, - "/xrpc/app.certified.actor.searchProfiles": {"get":{"operationId":"app_certified_actor_searchProfiles","tags":["app.certified.actor"],"description":"Searches indexed Certified profiles by text, optionally restricted to actor DIDs. Filters combine with AND; values in the actor array are ORed. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Trimmed text is matched literally and case-insensitively as a substring in displayName or description. Blank text is unrestricted.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"actors","in":"query","required":false,"description":"Actor DIDs to include; values are ORed and handles are not resolved.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep parameters unchanged between pages. Pages are not a snapshot.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.searchProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.searchProfiles","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or pagination parameter is invalid, repeated where scalar, or unsupported."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, - "/xrpc/app.certified.badge.getBadgeAward": {"get":{"operationId":"app_certified_badge_getBadgeAward","tags":["app.certified.badge"],"description":"Gets a badge award and its recipient response by AT-URI without authentication.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the badge-award record.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeAward.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeAward","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a badge-award AT-URI."},{"name":"RecordNotFound","description":"No indexed badge award exists at this AT-URI."},{"name":"BadgeAwardQueryFailed","description":"The indexed badge award could not be queried."}]}}, - "/xrpc/app.certified.badge.getBadgeDefinition": {"get":{"operationId":"app_certified_badge_getBadgeDefinition","tags":["app.certified.badge"],"description":"Returns an indexed badge definition and its publisher actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of a badge definition with a DID authority; lookup matches the URI, not a record CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeDefinition.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeDefinition","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify a badge-definition AT-URI with a DID authority."},{"name":"RecordNotFound","description":"No indexed badge definition matches this AT-URI."}]}}, - "/xrpc/app.certified.badge.getBadgeResponse": {"get":{"operationId":"app_certified_badge_getBadgeResponse","tags":["app.certified.badge"],"description":"Gets one badge response by AT-URI without authentication.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the badge-response record.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeResponse.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeResponse","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a badge-response AT-URI."},{"name":"RecordNotFound","description":"No indexed badge response exists at this AT-URI."},{"name":"BadgeResponseQueryFailed","description":"The indexed badge response could not be queried."}]}}, - "/xrpc/app.certified.badge.listBadgeAwards": {"get":{"operationId":"app_certified_badge_listBadgeAwards","tags":["app.certified.badge"],"description":"Lists public badge awards with recipient response status and pagination without authentication.","parameters":[{"name":"authors","in":"query","required":false,"description":"DIDs of repositories that published the awards.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeUris","in":"query","required":false,"description":"Badge-definition AT-URIs.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType values from the referenced definition version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"DIDs or full record AT-URIs with valid collection NSIDs.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"responses","in":"query","required":false,"description":"Computed recipient response statuses.","schema":{"type":"array","maxItems":100,"items":{"type":"string","enum":["accepted","rejected","unanswered"]}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum awards returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeAwards.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeAwards","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or page parameter is invalid."},{"name":"BadgeAwardQueryFailed","description":"The indexed badge awards could not be queried."}]}}, - "/xrpc/app.certified.badge.listBadgeDefinitions": {"get":{"operationId":"app_certified_badge_listBadgeDefinitions","tags":["app.certified.badge"],"description":"Lists indexed badge definitions. Author and badgeType filters are combined with AND; values within each filter are matched with OR. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs to include.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType strings to include.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Selects ascending or descending order by createdAt, then AT-URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of definitions to return in the page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page, bound to sortDirection. Keep other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeDefinitions.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeDefinitions","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, query parameter, page bound, or cursor is invalid."}]}}, - "/xrpc/app.certified.badge.listBadgeResponses": {"get":{"operationId":"app_certified_badge_listBadgeResponses","tags":["app.certified.badge"],"description":"Lists raw public badge responses with optional award filtering and pagination without authentication.","parameters":[{"name":"badgeAward","in":"query","required":false,"description":"Badge-award AT-URI to match against response records.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum responses returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeResponses.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeResponses","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or page parameter is invalid."},{"name":"BadgeResponseQueryFailed","description":"The indexed badge responses could not be queried."}]}}, - "/xrpc/app.certified.badge.searchBadgeDefinitions": {"get":{"operationId":"app_certified_badge_searchBadgeDefinitions","tags":["app.certified.badge"],"description":"Searches badge definitions by title or description without authentication.","parameters":[{"name":"search","in":"query","required":true,"description":"Text matched against definition titles and descriptions.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"DIDs of repositories that published the definitions.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum definitions returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.searchBadgeDefinitions.output"}}}}},"x-lexicon-id":"app.certified.badge.searchBadgeDefinitions","x-lexicon-errors":[{"name":"InvalidRequest","description":"A parameter is invalid, repeated where scalar, or unknown."},{"name":"BadgeDefinitionQueryFailed","description":"The indexed badge definitions could not be queried."}]}}, - "/xrpc/app.certified.feed.getActorLikes": {"get":{"operationId":"app_certified_feed_getActorLikes","tags":["app.certified.feed"],"description":"Lists an actor's indexed likes, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw like records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.","parameters":[{"name":"actor","in":"query","required":true,"description":"Repository-owner DID of the actor whose likes to list.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative like createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getActorLikes.output"}}}}},"x-lexicon-id":"app.certified.feed.getActorLikes","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"LikeQueryFailed","description":"The actor likes query failed; retry the request."}]}}, - "/xrpc/app.certified.feed.getActorReposts": {"get":{"operationId":"app_certified_feed_getActorReposts","tags":["app.certified.feed"],"description":"Lists an actor's indexed reposts, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw repost records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.","parameters":[{"name":"actor","in":"query","required":true,"description":"Repository-owner DID of the actor whose reposts to list.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getActorReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getActorReposts","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The actor reposts query failed; retry the request."}]}}, - "/xrpc/app.certified.feed.getLikes": {"get":{"operationId":"app_certified_feed_getLikes","tags":["app.certified.feed"],"description":"Lists distinct actors liking a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative like createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getLikes.output"}}}}},"x-lexicon-id":"app.certified.feed.getLikes","x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"LikeQueryFailed","description":"The like query, total count, or required actor hydration failed; retry the request."}]}}, - "/xrpc/app.certified.feed.getReposts": {"get":{"operationId":"app_certified_feed_getReposts","tags":["app.certified.feed"],"description":"Lists distinct actors reposting a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getReposts","x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The repost query, total count, or required actor hydration failed; retry the request."}]}}, - "/xrpc/app.certified.graph.getEntityFollow": {"get":{"operationId":"app_certified_graph_getEntityFollow","tags":["app.certified.graph"],"description":"Returns the representative entity-follow record for an actor and entity AT-URI, or null if none is indexed. No authentication is required; lookup failures return errors rather than null.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID that published the entity-follow record.","schema":{"type":"string","format":"did"}},{"name":"entity","in":"query","required":true,"description":"Full AT-URI of the followed record using a DID authority; any target collection is accepted, and matching is by URI, not CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getEntityFollow","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, entity AT-URI, or another query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.getFollow": {"get":{"operationId":"app_certified_graph_getFollow","tags":["app.certified.graph"],"description":"Returns the representative follow record for an actor-to-actor relationship, or null if none is indexed. No authentication is required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor who published the follow record.","schema":{"type":"string","format":"did"}},{"name":"subject","in":"query","required":true,"description":"DID named as the follow record's subject.","schema":{"type":"string","format":"did"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getFollow","x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.listActorFollowers": {"get":{"operationId":"app_certified_graph_listActorFollowers","tags":["app.certified.graph"],"description":"Lists actors whose follow records name the requested DID as subject. No authentication is required; duplicate relationships are collapsed before pagination, and only the returned actors are hydrated.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose incoming follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listActorFollowers.output"}}}}},"x-lexicon-id":"app.certified.graph.listActorFollowers","x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.listActorFollowing": {"get":{"operationId":"app_certified_graph_listActorFollowing","tags":["app.certified.graph"],"description":"Lists actors named as subjects by the requested DID's follow records. No authentication is required; duplicate relationships are collapsed before pagination, and only the returned actors are hydrated.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose outgoing follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listActorFollowing.output"}}}}},"x-lexicon-id":"app.certified.graph.listActorFollowing","x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.listEntityFollowers": {"get":{"operationId":"app_certified_graph_listEntityFollowers","tags":["app.certified.graph"],"description":"Lists actors following an entity AT-URI. No authentication is required; duplicate actor–entity relationships use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated: missing profile and organization sidecars are null, while lookup failures return errors rather than partial pages.","parameters":[{"name":"entity","in":"query","required":true,"description":"Full AT-URI of the followed record using a DID authority; any target collection is accepted, and matching is by URI, not CID.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listEntityFollowers.output"}}}}},"x-lexicon-id":"app.certified.graph.listEntityFollowers","x-lexicon-errors":[{"name":"InvalidRequest","description":"The entity AT-URI or another query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.listEntityFollowing": {"get":{"operationId":"app_certified_graph_listEntityFollowing","tags":["app.certified.graph"],"description":"Lists entities followed by an actor DID. No authentication is required; duplicate actor–entity relationships use the earliest createdAt, with URI ascending on ties. Only returned-page targets are hydrated and resolved to the latest indexed record by URI; missing or unsupported targets retain their URI and follow with entity null, while lookup failures return errors rather than partial pages.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose outgoing entity follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listEntityFollowing.output"}}}}},"x-lexicon-id":"app.certified.graph.listEntityFollowing","x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID or another query parameter is invalid."}]}}, - "/xrpc/app.certified.graph.listRecentFollows": {"get":{"operationId":"app_certified_graph_listRecentFollows","tags":["app.certified.graph"],"description":"Beta query for raw account- and entity-follow records from any publisher. No authentication is required; duplicates are preserved, and before is an exclusive bound on publisher-declared createdAt. Pages are not a snapshot, so late indexing or concurrent changes may cause records to be missed.","parameters":[{"name":"before","in":"query","required":false,"description":"Exclusive upper bound on publisher-declared createdAt, not indexedAt; omit to start with the newest records.","schema":{"type":"string","format":"datetime"}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor; keep before unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listRecentFollows.output"}}}}},"x-lexicon-id":"app.certified.graph.listRecentFollows","x-lexicon-errors":[{"name":"InvalidRequest","description":"A datetime, limit, cursor, or query parameter is invalid."}]}}, - "/xrpc/app.certified.link.getEvmLink": {"get":{"operationId":"app_certified_link_getEvmLink","tags":["app.certified.link"],"description":"Looks up an indexed EVM-link record by exact AT-URI and hydrates its actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of an app.certified.link.evm record with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.link.getEvmLink.output"}}}}},"x-lexicon-id":"app.certified.link.getEvmLink","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not an app.certified.link.evm record AT-URI."},{"name":"RecordNotFound","description":"No indexed EVM-link record exists at this AT-URI."}]}}, - "/xrpc/app.certified.link.listEvmLinks": {"get":{"operationId":"app_certified_link_listEvmLinks","tags":["app.certified.link"],"description":"Lists indexed EVM-link records, optionally filtered by actor DID and wallet address. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":false,"description":"Actor DIDs that own matching records; values within this filter use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"addresses","in":"query","required":false,"description":"EVM wallet addresses matched case-insensitively; values within this filter use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","minLength":42,"maxLength":42}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Order by the record's createdAt timestamp and then AT-URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of links to return.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous page, bound to sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.link.listEvmLinks.output"}}}}},"x-lexicon-id":"app.certified.link.listEvmLinks","x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID, EVM address, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, - "/xrpc/app.certified.location.getLocation": {"get":{"operationId":"app_certified_location_getLocation","tags":["app.certified.location"],"description":"Returns one indexed location by AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full location AT-URI with a DID authority; lookup matches the URI, not a CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.location.getLocation.output"}}}}},"x-lexicon-id":"app.certified.location.getLocation","x-lexicon-errors":[{"name":"RecordNotFound","description":"No indexed location exists at this AT-URI."}]}}, - "/xrpc/app.certified.location.listLocations": {"get":{"operationId":"app_certified_location_listLocations","tags":["app.certified.location"],"description":"Lists indexed location records; authentication is not required. Array filters use repeated unbracketed query keys; filters combine with AND, values within each array with OR, and omitted filters are unrestricted.","parameters":[{"name":"authors","in":"query","required":false,"description":"Filter by author DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by full location AT-URI with a DID authority; matching uses the URI, not a CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"locationTypes","in":"query","required":false,"description":"Match exact strings against locationType; empty strings are matched literally.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":20}},"style":"form","explode":true},{"name":"limit","in":"query","required":false,"description":"Page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque keyset cursor; keep all other parameters unchanged between pages. The cursor is bound to sortDirection, and pagination is not snapshot-isolated.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt; otherwise indexedAt or stored row creation time, then URI, all in this direction. Defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.location.listLocations.output"}}}}},"x-lexicon-id":"app.certified.location.listLocations","x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid, unknown, or repeated where only one value is allowed."}]}}, - "/xrpc/org.hypercerts.claim.getActivity": {"get":{"operationId":"org_hypercerts_claim_getActivity","tags":["org.hypercerts.claim"],"description":"Looks up one indexed activity by full AT-URI. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the activity, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getActivity","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or the request includes an unsupported parameter."},{"name":"RecordNotFound","description":"No indexed activity exists at the requested URI."},{"name":"ActivityQueryFailed","description":"The activity lookup or required hydration failed."}]}}, - "/xrpc/org.hypercerts.claim.getContribution": {"get":{"operationId":"org_hypercerts_claim_getContribution","tags":["org.hypercerts.claim"],"description":"Looks up one contribution record by exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the contribution record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getContribution.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getContribution","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI must identify a contribution record with a DID authority."},{"name":"RecordNotFound","description":"No indexed contribution record exists at this AT-URI."},{"name":"ContributionQueryFailed","description":"The contribution record or publisher could not be read from PostgreSQL."}]}}, - "/xrpc/org.hypercerts.claim.getContributorInformation": {"get":{"operationId":"org_hypercerts_claim_getContributorInformation","tags":["org.hypercerts.claim"],"description":"Publicly looks up one indexed contributor-information record by exact AT-URI and includes its publisher.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full contributor-information record AT-URI with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getContributorInformation.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getContributorInformation","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a contributor-information record AT-URI."},{"name":"RecordNotFound","description":"No indexed contributor-information record exists at this AT-URI."},{"name":"ContributorInformationQueryFailed","description":"The record or required publisher hydration lookup failed."}]}}, - "/xrpc/org.hypercerts.claim.getRights": {"get":{"operationId":"org_hypercerts_claim_getRights","tags":["org.hypercerts.claim"],"description":"Looks up one indexed rights record by exact DID-authority AT-URI. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of a rights record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getRights.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getRights","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify a rights record."},{"name":"RecordNotFound","description":"No indexed rights record exists at this AT-URI."}]}}, - "/xrpc/org.hypercerts.claim.listActivities": {"get":{"operationId":"org_hypercerts_claim_listActivities","tags":["org.hypercerts.claim"],"description":"Lists indexed activities. Filters combine with AND; values within each array filter combine with OR, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Filter by activity repository-owner DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by whether the activity author has an app.certified.actor.organization/self record, regardless of profile presence.","schema":{"type":"boolean"}},{"name":"contributors","in":"query","required":false,"description":"Filter by contributor DID, including inline identities and the exact referenced contributor-information version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"involvedActors","in":"query","required":false,"description":"Filter by activity repository-owner or contributor DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by exact activity AT-URI; results follow the selected sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by activity createdAt, then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum activities per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor for the next page, bound to sortDirection. Keep all other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listActivities.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listActivities","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, sort direction, page limit, cursor, or query parameter is invalid."},{"name":"ActivityQueryFailed","description":"The activity query or required hydration failed."}]}}, - "/xrpc/org.hypercerts.claim.listContributions": {"get":{"operationId":"org_hypercerts_claim_listContributions","tags":["org.hypercerts.claim"],"description":"Lists contribution records by publisher DID, sorted by createdAt and URI; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repeated unbracketed publisher DIDs are ORed; use at most 100 values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum records in a page from 1 to 100; defaults to 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep filters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listContributions.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listContributions","x-lexicon-errors":[{"name":"InvalidRequest","description":"An author DID, sort direction, page bound, or cursor is invalid."},{"name":"ContributionQueryFailed","description":"The contribution records or publishers could not be read from PostgreSQL."}]}}, - "/xrpc/org.hypercerts.claim.listContributorInformation": {"get":{"operationId":"org_hypercerts_claim_listContributorInformation","tags":["org.hypercerts.claim"],"description":"Publicly lists indexed contributor-information records with an optional publisher-DID filter and stable createdAt ordering.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; values use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records in the page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listContributorInformation.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listContributorInformation","x-lexicon-errors":[{"name":"InvalidRequest","description":"An author DID, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"ContributorInformationQueryFailed","description":"The record query or required publisher hydration lookup failed."}]}}, - "/xrpc/org.hypercerts.claim.listRights": {"get":{"operationId":"org_hypercerts_claim_listRights","tags":["org.hypercerts.claim"],"description":"Lists indexed rights records with an optional publisher-DID filter. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs; repeat the unbracketed key for multiple authors. Values use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for the (createdAt, uri) order; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records in the page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous page; keep the sort direction unchanged.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listRights.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listRights","x-lexicon-errors":[{"name":"InvalidRequest","description":"An author, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, - "/xrpc/org.hypercerts.claim.searchActivities": {"get":{"operationId":"org_hypercerts_claim_searchActivities","tags":["org.hypercerts.claim"],"description":"Searches indexed activities by a case-insensitive literal substring of title or shortDescription. Search and other filters combine with AND; values within each array filter combine with OR, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Literal, case-insensitive substring of title or shortDescription; surrounding whitespace is trimmed, and blank input applies no text filter.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"Filter by activity repository-owner DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by whether the activity author has an app.certified.actor.organization/self record, regardless of profile presence.","schema":{"type":"boolean"}},{"name":"contributors","in":"query","required":false,"description":"Filter by contributor DID, including inline identities and the exact referenced contributor-information version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"involvedActors","in":"query","required":false,"description":"Filter by activity repository-owner or contributor DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by exact activity AT-URI; results follow the selected sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by activity createdAt, then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum activities per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor for the next page, bound to sortDirection. Keep all other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.searchActivities.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.searchActivities","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, sort direction, page limit, cursor, or query parameter is invalid."},{"name":"ActivityQueryFailed","description":"The activity query or required hydration failed."}]}}, - "/xrpc/org.hypercerts.collection.getCollection": {"get":{"operationId":"org_hypercerts_collection_getCollection","tags":["org.hypercerts.collection"],"description":"Looks up one indexed collection by exact AT-URI. Hydrates its author and exact-version location and tag sibling projections; the original collection record remains unchanged. Missing related target versions are null without hiding the collection. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full collection record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.getCollection.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.getCollection","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a collection record AT-URI."},{"name":"RecordNotFound","description":"No indexed collection exists at this AT-URI."}]}}, - "/xrpc/org.hypercerts.collection.listCollectionItems": {"get":{"operationId":"org_hypercerts_collection_listCollectionItems","tags":["org.hypercerts.collection"],"description":"Resolves one collection's embedded items in source order, one level only. Each target must match the item's exact strong-reference URI+CID; nested collections are summaries, and unavailable versions or unsupported kinds return record null. Pagination follows the latest indexed collection at the same URI across pages, even after collection changes; no version-change error or restart is required. Item additions, removals, or reordering may cause skipped or repeated results. There is no sortDirection parameter. Authentication is not required.","parameters":[{"name":"collection","in":"query","required":true,"description":"Full collection record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}},{"name":"limit","in":"query","required":false,"description":"Maximum embedded items in the page; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque item-pagination cursor. Continue with the same collection URI; it is not bound to a collection version.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.listCollectionItems.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.listCollectionItems","x-lexicon-errors":[{"name":"InvalidRequest","description":"The collection AT-URI, page bound, cursor, or query parameters are invalid."},{"name":"RecordNotFound","description":"No indexed collection exists at this AT-URI."}]}}, - "/xrpc/org.hypercerts.collection.listCollections": {"get":{"operationId":"org_hypercerts_collection_listCollections","tags":["org.hypercerts.collection"],"description":"Lists indexed collections. Pass array filters as repeated query parameters; filters combine with AND, while values in each array are ORed except tagUris, which requires every supplied URI. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repository-owner DID to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by the author's organization/self record; false matches absence regardless of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact collection type to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":64}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Match by collection record AT-URI; this does not select a CID version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"itemUris","in":"query","required":false,"description":"Match when an embedded item's itemIdentifier.uri equals a supplied AT-URI; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"tagUris","in":"query","required":false,"description":"Require every supplied vocabulary-tag AT-URI to occur in record.tags; matching ignores CID and tag records.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri) ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum collections per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response; use the same sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.listCollections.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.listCollections","x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid or unsupported."}]}}, - "/xrpc/org.hypercerts.collection.searchCollections": {"get":{"operationId":"org_hypercerts_collection_searchCollections","tags":["org.hypercerts.collection"],"description":"Searches collection title or shortDescription for the complete trimmed text as a case-insensitive literal substring; blank text adds no search restriction. Other filters combine with search using AND, with OR within arrays except tagUris, which requires every supplied URI. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Case-insensitive literal substring in title or shortDescription; blank trimmed text adds no search restriction.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"Repository-owner DID to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by the author's organization/self record; false matches absence regardless of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact collection type to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":64}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Match by collection record AT-URI; this does not select a CID version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"itemUris","in":"query","required":false,"description":"Match when an embedded item's itemIdentifier.uri equals a supplied AT-URI; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"tagUris","in":"query","required":false,"description":"Require every supplied vocabulary-tag AT-URI to occur in record.tags; matching ignores CID and tag records.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri) ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum collections per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response; use the same sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.searchCollections.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.searchCollections","x-lexicon-errors":[{"name":"InvalidRequest","description":"Search text or another query parameter is invalid or unsupported."}]}}, - "/xrpc/org.hypercerts.context.getAcknowledgement": {"get":{"operationId":"org_hypercerts_context_getAcknowledgement","tags":["org.hypercerts.context"],"description":"Gets one public acknowledgement by exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Acknowledgement record AT-URI with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getAcknowledgement.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getAcknowledgement","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an acknowledgement record."},{"name":"RecordNotFound","description":"No acknowledgement is indexed at this AT-URI."},{"name":"AcknowledgementQueryFailed","description":"The acknowledgement or publisher lookup failed."}]}}, - "/xrpc/org.hypercerts.context.getAttachment": {"get":{"operationId":"org_hypercerts_context_getAttachment","tags":["org.hypercerts.context"],"description":"Returns the current indexed attachment at the given AT-URI with its publisher view. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of the attachment record, using a DID authority; lookup is by URI only, not a specific CID/version.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getAttachment.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getAttachment","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an attachment record."},{"name":"RecordNotFound","description":"No indexed attachment was found at this AT-URI."},{"name":"AttachmentQueryFailed","description":"The attachment lookup or required publisher hydration failed."}]}}, - "/xrpc/org.hypercerts.context.getEvaluation": {"get":{"operationId":"org_hypercerts_context_getEvaluation","tags":["org.hypercerts.context"],"description":"Returns the indexed evaluation at the given AT-URI with its publisher and evaluator views. Invalid evaluator entries are omitted from the hydrated projection while the original record is preserved. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of the evaluation record, using a DID authority; lookup is by URI only, not a specific CID/version.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getEvaluation","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an evaluation record."},{"name":"RecordNotFound","description":"No indexed evaluation was found at this AT-URI."}]}}, - "/xrpc/org.hypercerts.context.getMeasurement": {"get":{"operationId":"org_hypercerts_context_getMeasurement","tags":["org.hypercerts.context"],"description":"Gets an indexed measurement by exact AT-URI and hydrates its publisher. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full measurement record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getMeasurement.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getMeasurement","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a measurement record AT-URI."},{"name":"RecordNotFound","description":"No indexed measurement exists at this AT-URI."}]}}, - "/xrpc/org.hypercerts.context.listAcknowledgements": {"get":{"operationId":"org_hypercerts_context_listAcknowledgements","tags":["org.hypercerts.context"],"description":"Lists public acknowledgements by publisher or subject; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Subject record AT-URIs; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for createdAt and URI; default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep other parameters unchanged.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listAcknowledgements.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listAcknowledgements","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"AcknowledgementQueryFailed","description":"The acknowledgement or publisher lookup failed."}]}}, - "/xrpc/org.hypercerts.context.listAttachments": {"get":{"operationId":"org_hypercerts_context_listAttachments","tags":["org.hypercerts.context"],"description":"Lists indexed attachments with optional publisher, record URI, subject URI, and content-type filters. Values within each array filter use OR; distinct filters use AND, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Attachment AT-URIs with DID authorities to include; matching uses URI only, not CID/version. Results follow sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Match any record.subjects[].uri against these AT-URIs with DID authorities; CID values are ignored.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"contentTypes","in":"query","required":false,"description":"Exact matches on record.contentType; this is not a file MIME-type filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction by (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listAttachments.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listAttachments","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, cursor, sort direction, page size, or query parameter is invalid."},{"name":"AttachmentQueryFailed","description":"The attachment query or required publisher hydration failed."}]}}, - "/xrpc/org.hypercerts.context.listEvaluations": {"get":{"operationId":"org_hypercerts_context_listEvaluations","tags":["org.hypercerts.context"],"description":"Lists indexed evaluations with optional publisher, evaluator, and subject filters. Values within each array filter use OR; distinct filters use AND. Records remain listed when evaluator entries cannot be projected; malformed entries are omitted from hydrated views while the original record is preserved. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"evaluators","in":"query","required":false,"description":"DIDs in any record.evaluators[].did entry, including entries beyond the hydration cap.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Match record.subject.uri by AT-URI with a DID authority; the reference CID is ignored.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction by (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listEvaluations.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listEvaluations","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, cursor, sort direction, page size, or query parameter is invalid."}]}}, - "/xrpc/org.hypercerts.context.listMeasurements": {"get":{"operationId":"org_hypercerts_context_listMeasurements","tags":["org.hypercerts.context"],"description":"Lists measurements with optional publisher and subject filters. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs, not record.measurers.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Subject AT-URIs matched against record.subjects[].uri, ignoring CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by createdAt and URI; default desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; default 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; retain other filters between pages.","schema":{"type":"string","maxLength":8192}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listMeasurements.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listMeasurements","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, - "/xrpc/org.hypercerts.entity.getFeature": {"get":{"operationId":"org_hypercerts_entity_getFeature","tags":["org.hypercerts.entity"],"description":"Looks up one indexed feature by exact AT-URI with author hydration; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full feature record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.entity.getFeature.output"}}}}},"x-lexicon-id":"org.hypercerts.entity.getFeature","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a feature record AT-URI."},{"name":"RecordNotFound","description":"No indexed feature exists at this AT-URI."}]}}, - "/xrpc/org.hypercerts.entity.listFeatures": {"get":{"operationId":"org_hypercerts_entity_listFeatures","tags":["org.hypercerts.entity"],"description":"Lists indexed features with filters and direction-bound pagination; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repository-owner DIDs; values combine with OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Whether the author has an organization self record; false is independent of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact open-string feature types; values combine with OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","description":"Exact case-sensitive feature type.","maxLength":64}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for sorting by createdAt and URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of features in the page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page, bound to sortDirection.","schema":{"type":"string","maxLength":32768}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.entity.listFeatures.output"}}}}},"x-lexicon-id":"org.hypercerts.entity.listFeatures","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, - "/xrpc/org.hypercerts.funding.getReceipt": {"get":{"operationId":"org_hypercerts_funding_getReceipt","tags":["org.hypercerts.funding"],"description":"Returns one indexed funding receipt and hydrates its publisher actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Funding-receipt AT-URI using a DID authority; lookup matches the URI only, not a CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.funding.getReceipt.output"}}}}},"x-lexicon-id":"org.hypercerts.funding.getReceipt","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a funding-receipt record AT-URI."},{"name":"RecordNotFound","description":"No indexed funding receipt exists at this AT-URI."},{"name":"ReceiptQueryFailed","description":"The receipt query or publisher hydration failed."}]}}, - "/xrpc/org.hypercerts.funding.listReceipts": {"get":{"operationId":"org_hypercerts_funding_listReceipts","tags":["org.hypercerts.funding"],"description":"Lists indexed funding receipts matching the supplied filters: filters combine with AND, while values within an array combine with OR. Repeat array parameters to supply multiple values; duplicates are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs, including facilitator, personal, and group repositories.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Funding-receipt AT-URIs with a DID authority, matched by URI only, not CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"from","in":"query","required":false,"description":"Sender DIDs or full record AT-URIs with a DID authority. DIDs match only the DID variant; AT-URIs match only the strong-reference URI, not CID. Resolve handles to DIDs; invalid entries reject the request with the parameter and entry identified.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"to","in":"query","required":false,"description":"Recipient DIDs or full record AT-URIs with a DID authority. DIDs match only the DID variant; AT-URIs match only the strong-reference URI, not CID. Resolve handles to DIDs; invalid entries reject the request with the parameter and entry identified.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"forUris","in":"query","required":false,"description":"Funded-target AT-URIs with a DID authority, matched by URI only; CID is not compared.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"transactionIds","in":"query","required":false,"description":"Exact transaction ID matches; values are not normalized.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for ordering by createdAt then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum receipts per page; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.funding.listReceipts.output"}}}}},"x-lexicon-id":"org.hypercerts.funding.listReceipts","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or pagination parameter is invalid."},{"name":"ReceiptQueryFailed","description":"The receipt query or publisher hydration failed."}]}}, - "/xrpc/org.hypercerts.vocab.getVocabTag": {"get":{"operationId":"org_hypercerts_vocab_getVocabTag","tags":["org.hypercerts.vocab"],"description":"Looks up one published vocabulary tag by full DID-authority AT-URI and returns its unchanged record with publisher details; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the vocabulary-tag record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.vocab.getVocabTag.output"}}}}},"x-lexicon-id":"org.hypercerts.vocab.getVocabTag","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid, targets another collection, or query parameters are unsupported."},{"name":"RecordNotFound","description":"No indexed vocabulary tag exists at this AT-URI."},{"name":"VocabTagQueryFailed","description":"The indexed tag or required publisher lookup failed; retry after checking the HappyView PostgreSQL service."}]}}, - "/xrpc/org.hypercerts.vocab.listVocabTags": {"get":{"operationId":"org_hypercerts_vocab_listVocabTags","tags":["org.hypercerts.vocab"],"description":"Lists published vocabulary tags by optional publisher DIDs with stable cursor pagination; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; values are ORed.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri); default desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Page size from 1 to 100; default 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor; keep direction and filters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.vocab.listVocabTags.output"}}}}},"x-lexicon-id":"org.hypercerts.vocab.listVocabTags","x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"VocabTagQueryFailed","description":"The indexed tags or required publisher lookups could not be queried; retry after checking the HappyView PostgreSQL service."}]}}, - "/xrpc/org.hypercerts.workscope.getWorkscopeTag": {"get":{"operationId":"org_hypercerts_workscope_getWorkscopeTag","tags":["org.hypercerts.workscope"],"description":"Returns an indexed work-scope tag and its hydrated publisher for an exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full work-scope-tag AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.workscope.getWorkscopeTag.output"}}}}},"x-lexicon-id":"org.hypercerts.workscope.getWorkscopeTag","x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a work-scope-tag AT-URI."},{"name":"RecordNotFound","description":"No indexed work-scope tag exists at this AT-URI."},{"name":"WorkscopeTagQueryFailed","description":"The record lookup or publisher hydration failed."}]}}, - "/xrpc/org.hypercerts.workscope.listWorkscopeTags": {"get":{"operationId":"org_hypercerts_workscope_listWorkscopeTags","tags":["org.hypercerts.workscope"],"description":"Lists indexed work-scope tags by publisher DID with stable timestamp-and-URI ordering; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; repeat this key for OR matching, up to 100 values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for timestamp-and-URI ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum page size from 1 to 100; defaults to 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string","maxLength":8192}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.workscope.listWorkscopeTags.output"}}}}},"x-lexicon-id":"org.hypercerts.workscope.listWorkscopeTags","x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid."},{"name":"WorkscopeTagQueryFailed","description":"The record query or publisher hydration failed."}]}} + "/xrpc/app.certified.actor.getOrganization": {"get":{"operationId":"app_certified_actor_getOrganization","tags":["app.certified.actor"],"description":"Gets an actor's indexed organization sidecar and associated profile by DID. Authentication is not required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor to look up.","schema":{"type":"string","format":"did"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getOrganization.output"}}}}},"x-lexicon-id":"app.certified.actor.getOrganization","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/organization/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID is invalid or an unsupported parameter was supplied."},{"name":"RecordNotFound","description":"No indexed organization sidecar exists for this DID."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, + "/xrpc/app.certified.actor.getOrganizations": {"get":{"operationId":"app_certified_actor_getOrganizations","tags":["app.certified.actor"],"description":"Returns an indexed organization sidecar and associated profile for each supplied actor DID. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":true,"description":"Actor DIDs to look up, supplied as repeated unbracketed query values.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.getOrganizations","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/organization/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID or query parameter is invalid."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecars or associated profiles could not be queried."}]}}, + "/xrpc/app.certified.actor.getProfile": {"get":{"operationId":"app_certified_actor_getProfile","tags":["app.certified.actor"],"description":"Gets an indexed Certified profile by DID or handle. Handles resolve through the configured resolver's com.atproto.identity.resolveHandle endpoint; no DID document is fetched for independent verification. Authentication is not required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID or handle of the profile actor.","schema":{"type":"string","format":"at-identifier"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getProfile.output"}}}}},"x-lexicon-id":"app.certified.actor.getProfile","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/profile/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor is not a valid DID or handle, or an unsupported parameter was supplied."},{"name":"HandleResolverConfigError","description":"HYPERCERTS_HANDLE_RESOLVER_URL is missing or invalid."},{"name":"HandleResolutionFailed","description":"The handle could not be resolved to a valid DID."},{"name":"RecordNotFound","description":"The actor was identified, but no indexed Certified profile exists."},{"name":"ProfileQueryFailed","description":"The indexed profile could not be queried."}]}}, + "/xrpc/app.certified.actor.getProfiles": {"get":{"operationId":"app_certified_actor_getProfiles","tags":["app.certified.actor"],"description":"Returns an indexed Certified profile for each supplied actor DID. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":true,"description":"Actor DIDs to look up, supplied as repeated unbracketed query values.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.getProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.getProfiles","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/profile/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID or query parameter is invalid."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, + "/xrpc/app.certified.actor.listOrganizations": {"get":{"operationId":"app_certified_actor_listOrganizations","tags":["app.certified.actor"],"description":"Lists actors with indexed organization sidecars matching the supplied filters. Filters combine with AND, and values within each array filter are ORed. Authentication is not required.","parameters":[{"name":"organizationTypes","in":"query","required":false,"description":"Exact organizationType values; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":128}},"style":"form","explode":true},{"name":"visibility","in":"query","required":false,"description":"Exact visibility match; omission is unrestricted, but records with no visibility value do not match.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.listOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.listOrganizations","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/organization/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or scalar parameter is invalid or repeated, or an unsupported parameter was supplied."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, + "/xrpc/app.certified.actor.listProfiles": {"get":{"operationId":"app_certified_actor_listProfiles","tags":["app.certified.actor"],"description":"Lists indexed Certified profiles with pagination. Authentication is not required.","parameters":[{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep parameters unchanged between pages. Pages are not a snapshot.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.listProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.listProfiles","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/profile/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A pagination parameter is invalid, repeated where scalar, or unsupported."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, + "/xrpc/app.certified.actor.searchOrganizations": {"get":{"operationId":"app_certified_actor_searchOrganizations","tags":["app.certified.actor"],"description":"Searches organizations by associated profile text and optional filters. Filters combine with AND, and values within each array filter are ORed. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Trimmed text is matched literally and case-insensitively as a substring in either displayName or description. Blank text is unrestricted; nonblank text requires an associated profile.","schema":{"type":"string"}},{"name":"actors","in":"query","required":false,"description":"Actor DIDs to include; values are ORed and handles are not resolved.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"organizationTypes","in":"query","required":false,"description":"Exact organizationType values; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":128}},"style":"form","explode":true},{"name":"visibility","in":"query","required":false,"description":"Exact visibility match; omission is unrestricted, but records with no visibility value do not match.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.searchOrganizations.output"}}}}},"x-lexicon-id":"app.certified.actor.searchOrganizations","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/organization/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or scalar parameter is invalid or repeated, or an unsupported parameter was supplied."},{"name":"OrganizationQueryFailed","description":"The indexed organization sidecar or associated profile could not be queried."}]}}, + "/xrpc/app.certified.actor.searchProfiles": {"get":{"operationId":"app_certified_actor_searchProfiles","tags":["app.certified.actor"],"description":"Searches indexed Certified profiles by text, optionally restricted to actor DIDs. Filters combine with AND; values in the actor array are ORed. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Trimmed text is matched literally and case-insensitively as a substring in displayName or description. Blank text is unrestricted.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt, otherwise indexedAt, falling back to row creation time; URI breaks ties. Default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"actors","in":"query","required":false,"description":"Actor DIDs to include; values are ORed and handles are not resolved.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"limit","in":"query","required":false,"description":"Page size; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep parameters unchanged between pages. Pages are not a snapshot.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.actor.searchProfiles.output"}}}}},"x-lexicon-id":"app.certified.actor.searchProfiles","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/profile/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or pagination parameter is invalid, repeated where scalar, or unsupported."},{"name":"ProfileQueryFailed","description":"The indexed profiles could not be queried."}]}}, + "/xrpc/app.certified.badge.getBadgeAward": {"get":{"operationId":"app_certified_badge_getBadgeAward","tags":["app.certified.badge"],"description":"Gets a badge award and its recipient response by AT-URI without authentication.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the badge-award record.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeAward.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeAward","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-queries/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a badge-award AT-URI."},{"name":"RecordNotFound","description":"No indexed badge award exists at this AT-URI."},{"name":"BadgeAwardQueryFailed","description":"The indexed badge award could not be queried."}]}}, + "/xrpc/app.certified.badge.getBadgeDefinition": {"get":{"operationId":"app_certified_badge_getBadgeDefinition","tags":["app.certified.badge"],"description":"Returns an indexed badge definition and its publisher actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of a badge definition with a DID authority; lookup matches the URI, not a record CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeDefinition.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeDefinition","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-definitions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify a badge-definition AT-URI with a DID authority."},{"name":"RecordNotFound","description":"No indexed badge definition matches this AT-URI."}]}}, + "/xrpc/app.certified.badge.getBadgeResponse": {"get":{"operationId":"app_certified_badge_getBadgeResponse","tags":["app.certified.badge"],"description":"Gets one badge response by AT-URI without authentication.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the badge-response record.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.getBadgeResponse.output"}}}}},"x-lexicon-id":"app.certified.badge.getBadgeResponse","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-queries/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a badge-response AT-URI."},{"name":"RecordNotFound","description":"No indexed badge response exists at this AT-URI."},{"name":"BadgeResponseQueryFailed","description":"The indexed badge response could not be queried."}]}}, + "/xrpc/app.certified.badge.listBadgeAwards": {"get":{"operationId":"app_certified_badge_listBadgeAwards","tags":["app.certified.badge"],"description":"Lists public badge awards with recipient response status and pagination without authentication.","parameters":[{"name":"authors","in":"query","required":false,"description":"DIDs of repositories that published the awards.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeUris","in":"query","required":false,"description":"Badge-definition AT-URIs.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType values from the referenced definition version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"DIDs or full record AT-URIs with valid collection NSIDs.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"responses","in":"query","required":false,"description":"Computed recipient response statuses.","schema":{"type":"array","maxItems":100,"items":{"type":"string","enum":["accepted","rejected","unanswered"]}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum awards returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeAwards.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeAwards","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-queries/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or page parameter is invalid."},{"name":"BadgeAwardQueryFailed","description":"The indexed badge awards could not be queried."}]}}, + "/xrpc/app.certified.badge.listBadgeDefinitions": {"get":{"operationId":"app_certified_badge_listBadgeDefinitions","tags":["app.certified.badge"],"description":"Lists indexed badge definitions. Author and badgeType filters are combined with AND; values within each filter are matched with OR. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs to include.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType strings to include.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Selects ascending or descending order by createdAt, then AT-URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of definitions to return in the page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page, bound to sortDirection. Keep other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeDefinitions.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeDefinitions","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-definitions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, query parameter, page bound, or cursor is invalid."}]}}, + "/xrpc/app.certified.badge.listBadgeResponses": {"get":{"operationId":"app_certified_badge_listBadgeResponses","tags":["app.certified.badge"],"description":"Lists raw public badge responses with optional award filtering and pagination without authentication.","parameters":[{"name":"badgeAward","in":"query","required":false,"description":"Badge-award AT-URI to match against response records.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum responses returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.listBadgeResponses.output"}}}}},"x-lexicon-id":"app.certified.badge.listBadgeResponses","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-queries/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or page parameter is invalid."},{"name":"BadgeResponseQueryFailed","description":"The indexed badge responses could not be queried."}]}}, + "/xrpc/app.certified.badge.searchBadgeDefinitions": {"get":{"operationId":"app_certified_badge_searchBadgeDefinitions","tags":["app.certified.badge"],"description":"Searches badge definitions by title or description without authentication.","parameters":[{"name":"search","in":"query","required":true,"description":"Text matched against definition titles and descriptions.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"DIDs of repositories that published the definitions.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"badgeTypes","in":"query","required":false,"description":"Exact badgeType values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":100}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for (createdAt, uri).","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum definitions returned.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.badge.searchBadgeDefinitions.output"}}}}},"x-lexicon-id":"app.certified.badge.searchBadgeDefinitions","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/badge-queries/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A parameter is invalid, repeated where scalar, or unknown."},{"name":"BadgeDefinitionQueryFailed","description":"The indexed badge definitions could not be queried."}]}}, + "/xrpc/app.certified.feed.getActorLikes": {"get":{"operationId":"app_certified_feed_getActorLikes","tags":["app.certified.feed"],"description":"Lists an actor's indexed likes, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw like records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.","parameters":[{"name":"actor","in":"query","required":true,"description":"Repository-owner DID of the actor whose likes to list.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative like createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getActorLikes.output"}}}}},"x-lexicon-id":"app.certified.feed.getActorLikes","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/feed-reactions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"LikeQueryFailed","description":"The actor likes query failed; retry the request."}]}}, + "/xrpc/app.certified.feed.getActorReposts": {"get":{"operationId":"app_certified_feed_getActorReposts","tags":["app.certified.feed"],"description":"Lists an actor's indexed reposts, one representative per subject URI regardless of subject CID. No authentication is required. Duplicates use the earliest createdAt, with URI ascending on ties. Returns raw repost records including subject, via, and signatures when present; subjects are not hydrated and need not be indexed.","parameters":[{"name":"actor","in":"query","required":true,"description":"Repository-owner DID of the actor whose reposts to list.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getActorReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getActorReposts","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/feed-reactions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The actor reposts query failed; retry the request."}]}}, + "/xrpc/app.certified.feed.getLikes": {"get":{"operationId":"app_certified_feed_getLikes","tags":["app.certified.feed"],"description":"Lists distinct actors liking a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative like createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getLikes.output"}}}}},"x-lexicon-id":"app.certified.feed.getLikes","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/feed-reactions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"LikeQueryFailed","description":"The like query, total count, or required actor hydration failed; retry the request."}]}}, + "/xrpc/app.certified.feed.getReposts": {"get":{"operationId":"app_certified_feed_getReposts","tags":["app.certified.feed"],"description":"Lists distinct actors reposting a subject record. No authentication is required. Matches subject.uri regardless of CID or whether the subject is indexed. Duplicate author and subject URI pairs use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated; missing sidecars are null and hydration failures return errors, not partial pages.","parameters":[{"name":"subject","in":"query","required":true,"description":"Full DID-authority record AT-URI; any target collection is accepted.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by representative repost createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.feed.getReposts.output"}}}}},"x-lexicon-id":"app.certified.feed.getReposts","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/feed-reactions/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The subject AT-URI, sort direction, limit, cursor, or another query parameter is invalid."},{"name":"RepostQueryFailed","description":"The repost query, total count, or required actor hydration failed; retry the request."}]}}, + "/xrpc/app.certified.graph.getEntityFollow": {"get":{"operationId":"app_certified_graph_getEntityFollow","tags":["app.certified.graph"],"description":"Returns the representative entity-follow record for an actor and entity AT-URI, or null if none is indexed. No authentication is required; lookup failures return errors rather than null.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID that published the entity-follow record.","schema":{"type":"string","format":"did"}},{"name":"entity","in":"query","required":true,"description":"Full AT-URI of the followed record using a DID authority; any target collection is accepted, and matching is by URI, not CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getEntityFollow","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/entity-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, entity AT-URI, or another query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.getEntityFollows": {"get":{"operationId":"app_certified_graph_getEntityFollows","tags":["app.certified.graph"],"description":"Returns the representative raw entity-follow record, or null, for each requested actor-to-entity relationship. Authentication is not required. Supply 1–100 repeated entities values; the encoded GET request-target must not exceed 7,000 bytes.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID that published the entity-follow records.","schema":{"type":"string","format":"did","maxLength":2048}},{"name":"entities","in":"query","required":true,"description":"Full entity AT-URIs using DID authorities, supplied as repeated unbracketed query values. Matching uses the subject URI and ignores CID; every occurrence is returned in request order, including duplicates.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"at-uri","maxLength":8192}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getEntityFollows.output"}}}}},"x-lexicon-id":"app.certified.graph.getEntityFollows","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/entity-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, entities array, an entity AT-URI, request-target length, or another query parameter is invalid."},{"name":"EntityFollowQueryFailed","description":"The indexed entity-follow relationships could not be queried."}]}}, + "/xrpc/app.certified.graph.getFollow": {"get":{"operationId":"app_certified_graph_getFollow","tags":["app.certified.graph"],"description":"Returns the representative follow record for an actor-to-actor relationship, or null if none is indexed. No authentication is required.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor who published the follow record.","schema":{"type":"string","format":"did"}},{"name":"subject","in":"query","required":true,"description":"DID named as the follow record's subject.","schema":{"type":"string","format":"did"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getFollow.output"}}}}},"x-lexicon-id":"app.certified.graph.getFollow","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/actor-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.getFollows": {"get":{"operationId":"app_certified_graph_getFollows","tags":["app.certified.graph"],"description":"Returns the representative raw follow record, or null, for each requested actor-to-actor relationship. Authentication is not required. Supply 1–100 repeated subjects values; the encoded GET request-target must not exceed 7,000 bytes.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID of the actor who published the follow records.","schema":{"type":"string","format":"did","maxLength":2048}},{"name":"subjects","in":"query","required":true,"description":"Subject DIDs to check, supplied as repeated unbracketed query values. Every occurrence is returned in the same order, including duplicates.","schema":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"did","maxLength":2048}},"style":"form","explode":true}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.getFollows.output"}}}}},"x-lexicon-id":"app.certified.graph.getFollows","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/actor-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID, subjects array, request-target length, or another query parameter is invalid."},{"name":"ActorFollowQueryFailed","description":"The indexed follow relationships could not be queried."}]}}, + "/xrpc/app.certified.graph.listActorFollowers": {"get":{"operationId":"app_certified_graph_listActorFollowers","tags":["app.certified.graph"],"description":"Lists actors whose follow records name the requested DID as subject. No authentication is required; duplicate relationships are collapsed before pagination, and only the returned actors are hydrated.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose incoming follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listActorFollowers.output"}}}}},"x-lexicon-id":"app.certified.graph.listActorFollowers","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/actor-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.listActorFollowing": {"get":{"operationId":"app_certified_graph_listActorFollowing","tags":["app.certified.graph"],"description":"Lists actors named as subjects by the requested DID's follow records. No authentication is required; duplicate relationships are collapsed before pagination, and only the returned actors are hydrated.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose outgoing follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listActorFollowing.output"}}}}},"x-lexicon-id":"app.certified.graph.listActorFollowing","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/actor-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A DID or query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.listEntityFollowers": {"get":{"operationId":"app_certified_graph_listEntityFollowers","tags":["app.certified.graph"],"description":"Lists actors following an entity AT-URI. No authentication is required; duplicate actor–entity relationships use the earliest createdAt, with URI ascending on ties. Only the returned page is hydrated: missing profile and organization sidecars are null, while lookup failures return errors rather than partial pages.","parameters":[{"name":"entity","in":"query","required":true,"description":"Full AT-URI of the followed record using a DID authority; any target collection is accepted, and matching is by URI, not CID.","schema":{"type":"string","format":"at-uri"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listEntityFollowers.output"}}}}},"x-lexicon-id":"app.certified.graph.listEntityFollowers","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/entity-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The entity AT-URI or another query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.listEntityFollowing": {"get":{"operationId":"app_certified_graph_listEntityFollowing","tags":["app.certified.graph"],"description":"Lists entities followed by an actor DID. No authentication is required; duplicate actor–entity relationships use the earliest createdAt, with URI ascending on ties. Only returned-page targets are hydrated and resolved to the latest indexed record by URI; missing or unsupported targets retain their URI and follow with entity null, while lookup failures return errors rather than partial pages.","parameters":[{"name":"actor","in":"query","required":true,"description":"DID whose outgoing entity follows are listed.","schema":{"type":"string","format":"did"}},{"name":"sortDirection","in":"query","required":false,"description":"Sorts by representative follow createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum relationships per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep the other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listEntityFollowing.output"}}}}},"x-lexicon-id":"app.certified.graph.listEntityFollowing","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/entity-follow/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The actor DID or another query parameter is invalid."}]}}, + "/xrpc/app.certified.graph.listRecentFollows": {"get":{"operationId":"app_certified_graph_listRecentFollows","tags":["app.certified.graph"],"description":"Beta query for raw account- and entity-follow records from any publisher. No authentication is required; duplicates are preserved, and before is an exclusive bound on publisher-declared createdAt. Pages are not a snapshot, so late indexing or concurrent changes may cause records to be missed.","parameters":[{"name":"before","in":"query","required":false,"description":"Exclusive upper bound on publisher-declared createdAt, not indexedAt; omit to start with the newest records.","schema":{"type":"string","format":"datetime"}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor; keep before unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.graph.listRecentFollows.output"}}}}},"x-lexicon-id":"app.certified.graph.listRecentFollows","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/recent-follows/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A datetime, limit, cursor, or query parameter is invalid."}]}}, + "/xrpc/app.certified.link.getEvmLink": {"get":{"operationId":"app_certified_link_getEvmLink","tags":["app.certified.link"],"description":"Looks up an indexed EVM-link record by exact AT-URI and hydrates its actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of an app.certified.link.evm record with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.link.getEvmLink.output"}}}}},"x-lexicon-id":"app.certified.link.getEvmLink","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/evm-links/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not an app.certified.link.evm record AT-URI."},{"name":"RecordNotFound","description":"No indexed EVM-link record exists at this AT-URI."}]}}, + "/xrpc/app.certified.link.listEvmLinks": {"get":{"operationId":"app_certified_link_listEvmLinks","tags":["app.certified.link"],"description":"Lists indexed EVM-link records, optionally filtered by actor DID and wallet address. Authentication is not required.","parameters":[{"name":"actors","in":"query","required":false,"description":"Actor DIDs that own matching records; values within this filter use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"addresses","in":"query","required":false,"description":"EVM wallet addresses matched case-insensitively; values within this filter use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","minLength":42,"maxLength":42}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Order by the record's createdAt timestamp and then AT-URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of links to return.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous page, bound to sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.link.listEvmLinks.output"}}}}},"x-lexicon-id":"app.certified.link.listEvmLinks","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/evm-links/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An actor DID, EVM address, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, + "/xrpc/app.certified.location.getLocation": {"get":{"operationId":"app_certified_location_getLocation","tags":["app.certified.location"],"description":"Returns one indexed location by AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full location AT-URI with a DID authority; lookup matches the URI, not a CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.location.getLocation.output"}}}}},"x-lexicon-id":"app.certified.location.getLocation","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/location/manifest.json"},"x-lexicon-errors":[{"name":"RecordNotFound","description":"No indexed location exists at this AT-URI."}]}}, + "/xrpc/app.certified.location.listLocations": {"get":{"operationId":"app_certified_location_listLocations","tags":["app.certified.location"],"description":"Lists indexed location records; authentication is not required. Array filters use repeated unbracketed query keys; filters combine with AND, values within each array with OR, and omitted filters are unrestricted.","parameters":[{"name":"authors","in":"query","required":false,"description":"Filter by author DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by full location AT-URI with a DID authority; matching uses the URI, not a CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"locationTypes","in":"query","required":false,"description":"Match exact strings against locationType; empty strings are matched literally.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":20}},"style":"form","explode":true},{"name":"limit","in":"query","required":false,"description":"Page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque keyset cursor; keep all other parameters unchanged between pages. The cursor is bound to sortDirection, and pagination is not snapshot-isolated.","schema":{"type":"string"}},{"name":"sortDirection","in":"query","required":false,"description":"Sort by a valid zoned createdAt; otherwise indexedAt or stored row creation time, then URI, all in this direction. Defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app.certified.location.listLocations.output"}}}}},"x-lexicon-id":"app.certified.location.listLocations","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/location/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid, unknown, or repeated where only one value is allowed."}]}}, + "/xrpc/org.hypercerts.claim.getActivity": {"get":{"operationId":"org_hypercerts_claim_getActivity","tags":["org.hypercerts.claim"],"description":"Looks up one indexed activity by full AT-URI. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the activity, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getActivity","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/shared/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or the request includes an unsupported parameter."},{"name":"RecordNotFound","description":"No indexed activity exists at the requested URI."},{"name":"ActivityQueryFailed","description":"The activity lookup or required hydration failed."}]}}, + "/xrpc/org.hypercerts.claim.getContribution": {"get":{"operationId":"org_hypercerts_claim_getContribution","tags":["org.hypercerts.claim"],"description":"Looks up one contribution record by exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the contribution record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getContribution.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getContribution","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/contribution/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI must identify a contribution record with a DID authority."},{"name":"RecordNotFound","description":"No indexed contribution record exists at this AT-URI."},{"name":"ContributionQueryFailed","description":"The contribution record or publisher could not be read from PostgreSQL."}]}}, + "/xrpc/org.hypercerts.claim.getContributorInformation": {"get":{"operationId":"org_hypercerts_claim_getContributorInformation","tags":["org.hypercerts.claim"],"description":"Publicly looks up one indexed contributor-information record by exact AT-URI and includes its publisher.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full contributor-information record AT-URI with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getContributorInformation.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getContributorInformation","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/contributor-information/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a contributor-information record AT-URI."},{"name":"RecordNotFound","description":"No indexed contributor-information record exists at this AT-URI."},{"name":"ContributorInformationQueryFailed","description":"The record or required publisher hydration lookup failed."}]}}, + "/xrpc/org.hypercerts.claim.getRights": {"get":{"operationId":"org_hypercerts_claim_getRights","tags":["org.hypercerts.claim"],"description":"Looks up one indexed rights record by exact DID-authority AT-URI. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of a rights record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.getRights.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.getRights","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/rights/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify a rights record."},{"name":"RecordNotFound","description":"No indexed rights record exists at this AT-URI."}]}}, + "/xrpc/org.hypercerts.claim.listActivities": {"get":{"operationId":"org_hypercerts_claim_listActivities","tags":["org.hypercerts.claim"],"description":"Lists indexed activities. Filters combine with AND; values within each array filter combine with OR, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Filter by activity repository-owner DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by whether the activity author has an app.certified.actor.organization/self record, regardless of profile presence.","schema":{"type":"boolean"}},{"name":"contributors","in":"query","required":false,"description":"Filter by contributor DID, including inline identities and the exact referenced contributor-information version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"involvedActors","in":"query","required":false,"description":"Filter by activity repository-owner or contributor DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by exact activity AT-URI; results follow the selected sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by activity createdAt, then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum activities per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor for the next page, bound to sortDirection. Keep all other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listActivities.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listActivities","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/activity/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, sort direction, page limit, cursor, or query parameter is invalid."},{"name":"ActivityQueryFailed","description":"The activity query or required hydration failed."}]}}, + "/xrpc/org.hypercerts.claim.listContributions": {"get":{"operationId":"org_hypercerts_claim_listContributions","tags":["org.hypercerts.claim"],"description":"Lists contribution records by publisher DID, sorted by createdAt and URI; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repeated unbracketed publisher DIDs are ORed; use at most 100 values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for createdAt and URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum records in a page from 1 to 100; defaults to 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep filters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listContributions.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listContributions","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/contribution/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An author DID, sort direction, page bound, or cursor is invalid."},{"name":"ContributionQueryFailed","description":"The contribution records or publishers could not be read from PostgreSQL."}]}}, + "/xrpc/org.hypercerts.claim.listContributorInformation": {"get":{"operationId":"org_hypercerts_claim_listContributorInformation","tags":["org.hypercerts.claim"],"description":"Publicly lists indexed contributor-information records with an optional publisher-DID filter and stable createdAt ordering.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; values use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records in the page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listContributorInformation.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listContributorInformation","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/contributor-information/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An author DID, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"ContributorInformationQueryFailed","description":"The record query or required publisher hydration lookup failed."}]}}, + "/xrpc/org.hypercerts.claim.listRights": {"get":{"operationId":"org_hypercerts_claim_listRights","tags":["org.hypercerts.claim"],"description":"Lists indexed rights records with an optional publisher-DID filter. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs; repeat the unbracketed key for multiple authors. Values use OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for the (createdAt, uri) order; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records in the page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous page; keep the sort direction unchanged.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.listRights.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.listRights","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/rights/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"An author, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, + "/xrpc/org.hypercerts.claim.searchActivities": {"get":{"operationId":"org_hypercerts_claim_searchActivities","tags":["org.hypercerts.claim"],"description":"Searches indexed activities by a case-insensitive literal substring of title or shortDescription. Search and other filters combine with AND; values within each array filter combine with OR, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Literal, case-insensitive substring of title or shortDescription; surrounding whitespace is trimmed, and blank input applies no text filter.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"Filter by activity repository-owner DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by whether the activity author has an app.certified.actor.organization/self record, regardless of profile presence.","schema":{"type":"boolean"}},{"name":"contributors","in":"query","required":false,"description":"Filter by contributor DID, including inline identities and the exact referenced contributor-information version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"involvedActors","in":"query","required":false,"description":"Filter by activity repository-owner or contributor DID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Filter by exact activity AT-URI; results follow the selected sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by activity createdAt, then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum activities per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor for the next page, bound to sortDirection. Keep all other query parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.claim.searchActivities.output"}}}}},"x-lexicon-id":"org.hypercerts.claim.searchActivities","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/activity/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, sort direction, page limit, cursor, or query parameter is invalid."},{"name":"ActivityQueryFailed","description":"The activity query or required hydration failed."}]}}, + "/xrpc/org.hypercerts.collection.getCollection": {"get":{"operationId":"org_hypercerts_collection_getCollection","tags":["org.hypercerts.collection"],"description":"Looks up one indexed collection by exact AT-URI. Hydrates its author and exact-version location and tag sibling projections; the original collection record remains unchanged. Missing related target versions are null without hiding the collection. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full collection record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.getCollection.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.getCollection","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/shared/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a collection record AT-URI."},{"name":"RecordNotFound","description":"No indexed collection exists at this AT-URI."}]}}, + "/xrpc/org.hypercerts.collection.listCollectionItems": {"get":{"operationId":"org_hypercerts_collection_listCollectionItems","tags":["org.hypercerts.collection"],"description":"Resolves one collection's embedded items in source order, one level only. Each target must match the item's exact strong-reference URI+CID; nested collections are summaries, and unavailable versions or unsupported kinds return record null. Pagination follows the latest indexed collection at the same URI across pages, even after collection changes; no version-change error or restart is required. Item additions, removals, or reordering may cause skipped or repeated results. There is no sortDirection parameter. Authentication is not required.","parameters":[{"name":"collection","in":"query","required":true,"description":"Full collection record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}},{"name":"limit","in":"query","required":false,"description":"Maximum embedded items in the page; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque item-pagination cursor. Continue with the same collection URI; it is not bound to a collection version.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.listCollectionItems.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.listCollectionItems","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/shared/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The collection AT-URI, page bound, cursor, or query parameters are invalid."},{"name":"RecordNotFound","description":"No indexed collection exists at this AT-URI."}]}}, + "/xrpc/org.hypercerts.collection.listCollections": {"get":{"operationId":"org_hypercerts_collection_listCollections","tags":["org.hypercerts.collection"],"description":"Lists indexed collections. Pass array filters as repeated query parameters; filters combine with AND, while values in each array are ORed except tagUris, which requires every supplied URI. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repository-owner DID to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by the author's organization/self record; false matches absence regardless of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact collection type to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":64}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Match by collection record AT-URI; this does not select a CID version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"itemUris","in":"query","required":false,"description":"Match when an embedded item's itemIdentifier.uri equals a supplied AT-URI; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"tagUris","in":"query","required":false,"description":"Require every supplied vocabulary-tag AT-URI to occur in record.tags; matching ignores CID and tag records.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri) ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum collections per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response; use the same sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.listCollections.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.listCollections","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/collection/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid or unsupported."}]}}, + "/xrpc/org.hypercerts.collection.searchCollections": {"get":{"operationId":"org_hypercerts_collection_searchCollections","tags":["org.hypercerts.collection"],"description":"Searches collection title or shortDescription for the complete trimmed text as a case-insensitive literal substring; blank text adds no search restriction. Other filters combine with search using AND, with OR within arrays except tagUris, which requires every supplied URI. Authentication is not required.","parameters":[{"name":"search","in":"query","required":true,"description":"Case-insensitive literal substring in title or shortDescription; blank trimmed text adds no search restriction.","schema":{"type":"string"}},{"name":"authors","in":"query","required":false,"description":"Repository-owner DID to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Filter by the author's organization/self record; false matches absence regardless of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact collection type to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":64}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Match by collection record AT-URI; this does not select a CID version.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"itemUris","in":"query","required":false,"description":"Match when an embedded item's itemIdentifier.uri equals a supplied AT-URI; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"tagUris","in":"query","required":false,"description":"Require every supplied vocabulary-tag AT-URI to occur in record.tags; matching ignores CID and tag records.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri) ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum collections per page; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from a previous response; use the same sortDirection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.collection.searchCollections.output"}}}}},"x-lexicon-id":"org.hypercerts.collection.searchCollections","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/collection/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"Search text or another query parameter is invalid or unsupported."}]}}, + "/xrpc/org.hypercerts.context.getAcknowledgement": {"get":{"operationId":"org_hypercerts_context_getAcknowledgement","tags":["org.hypercerts.context"],"description":"Gets one public acknowledgement by exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Acknowledgement record AT-URI with a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getAcknowledgement.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getAcknowledgement","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/acknowledgements/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an acknowledgement record."},{"name":"RecordNotFound","description":"No acknowledgement is indexed at this AT-URI."},{"name":"AcknowledgementQueryFailed","description":"The acknowledgement or publisher lookup failed."}]}}, + "/xrpc/org.hypercerts.context.getAttachment": {"get":{"operationId":"org_hypercerts_context_getAttachment","tags":["org.hypercerts.context"],"description":"Returns the current indexed attachment at the given AT-URI with its publisher view. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of the attachment record, using a DID authority; lookup is by URI only, not a specific CID/version.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getAttachment.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getAttachment","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-attachments/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an attachment record."},{"name":"RecordNotFound","description":"No indexed attachment was found at this AT-URI."},{"name":"AttachmentQueryFailed","description":"The attachment lookup or required publisher hydration failed."}]}}, + "/xrpc/org.hypercerts.context.getEvaluation": {"get":{"operationId":"org_hypercerts_context_getEvaluation","tags":["org.hypercerts.context"],"description":"Returns the indexed evaluation at the given AT-URI with its publisher and evaluator views. Invalid evaluator entries are omitted from the hydrated projection while the original record is preserved. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"AT-URI of the evaluation record, using a DID authority; lookup is by URI only, not a specific CID/version.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getEvaluation","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-evaluation/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or does not identify an evaluation record."},{"name":"RecordNotFound","description":"No indexed evaluation was found at this AT-URI."}]}}, + "/xrpc/org.hypercerts.context.getMeasurement": {"get":{"operationId":"org_hypercerts_context_getMeasurement","tags":["org.hypercerts.context"],"description":"Gets an indexed measurement by exact AT-URI and hydrates its publisher. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full measurement record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.getMeasurement.output"}}}}},"x-lexicon-id":"org.hypercerts.context.getMeasurement","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-measurements/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a measurement record AT-URI."},{"name":"RecordNotFound","description":"No indexed measurement exists at this AT-URI."}]}}, + "/xrpc/org.hypercerts.context.listAcknowledgements": {"get":{"operationId":"org_hypercerts_context_listAcknowledgements","tags":["org.hypercerts.context"],"description":"Lists public acknowledgements by publisher or subject; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher DIDs; OR within this filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Subject record AT-URIs; matching ignores CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for createdAt and URI; default desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque next-page cursor bound to sortDirection; keep other parameters unchanged.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listAcknowledgements.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listAcknowledgements","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/acknowledgements/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"AcknowledgementQueryFailed","description":"The acknowledgement or publisher lookup failed."}]}}, + "/xrpc/org.hypercerts.context.listAttachments": {"get":{"operationId":"org_hypercerts_context_listAttachments","tags":["org.hypercerts.context"],"description":"Lists indexed attachments with optional publisher, record URI, subject URI, and content-type filters. Values within each array filter use OR; distinct filters use AND, and duplicate values are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Attachment AT-URIs with DID authorities to include; matching uses URI only, not CID/version. Results follow sort order, not input order.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Match any record.subjects[].uri against these AT-URIs with DID authorities; CID values are ignored.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"contentTypes","in":"query","required":false,"description":"Exact matches on record.contentType; this is not a file MIME-type filter.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction by (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listAttachments.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listAttachments","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-attachments/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, cursor, sort direction, page size, or query parameter is invalid."},{"name":"AttachmentQueryFailed","description":"The attachment query or required publisher hydration failed."}]}}, + "/xrpc/org.hypercerts.context.listEvaluations": {"get":{"operationId":"org_hypercerts_context_listEvaluations","tags":["org.hypercerts.context"],"description":"Lists indexed evaluations with optional publisher, evaluator, and subject filters. Values within each array filter use OR; distinct filters use AND. Records remain listed when evaluator entries cannot be projected; malformed entries are omitted from hydrated views while the original record is preserved. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs to match.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"evaluators","in":"query","required":false,"description":"DIDs in any record.evaluators[].did entry, including entries beyond the hydration cap.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Match record.subject.uri by AT-URI with a DID authority; the reference CID is ignored.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction by (createdAt, uri); defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listEvaluations.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listEvaluations","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-evaluation/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, cursor, sort direction, page size, or query parameter is invalid."}]}}, + "/xrpc/org.hypercerts.context.listMeasurements": {"get":{"operationId":"org_hypercerts_context_listMeasurements","tags":["org.hypercerts.context"],"description":"Lists measurements with optional publisher and subject filters. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs, not record.measurers.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"subjects","in":"query","required":false,"description":"Subject AT-URIs matched against record.subjects[].uri, ignoring CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort by createdAt and URI; default desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum page size; default 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor tied to sortDirection; retain other filters between pages.","schema":{"type":"string","maxLength":8192}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.context.listMeasurements.output"}}}}},"x-lexicon-id":"org.hypercerts.context.listMeasurements","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/context-measurements/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, sort direction, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, + "/xrpc/org.hypercerts.entity.getFeature": {"get":{"operationId":"org_hypercerts_entity_getFeature","tags":["org.hypercerts.entity"],"description":"Looks up one indexed feature by exact AT-URI with author hydration; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full feature record AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.entity.getFeature.output"}}}}},"x-lexicon-id":"org.hypercerts.entity.getFeature","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/features/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a feature record AT-URI."},{"name":"RecordNotFound","description":"No indexed feature exists at this AT-URI."}]}}, + "/xrpc/org.hypercerts.entity.listFeatures": {"get":{"operationId":"org_hypercerts_entity_listFeatures","tags":["org.hypercerts.entity"],"description":"Lists indexed features with filters and direction-bound pagination; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Repository-owner DIDs; values combine with OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"hasOrganizationRecord","in":"query","required":false,"description":"Whether the author has an organization self record; false is independent of profile presence.","schema":{"type":"boolean"}},{"name":"types","in":"query","required":false,"description":"Exact open-string feature types; values combine with OR.","schema":{"type":"array","maxItems":100,"items":{"type":"string","description":"Exact case-sensitive feature type.","maxLength":64}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for sorting by createdAt and URI.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of features in the page.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor from the previous page, bound to sortDirection.","schema":{"type":"string","maxLength":32768}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.entity.listFeatures.output"}}}}},"x-lexicon-id":"org.hypercerts.entity.listFeatures","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/features/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter, page bound, cursor, repeated scalar, or query parameter is invalid."}]}}, + "/xrpc/org.hypercerts.funding.getReceipt": {"get":{"operationId":"org_hypercerts_funding_getReceipt","tags":["org.hypercerts.funding"],"description":"Returns one indexed funding receipt and hydrates its publisher actor. Authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Funding-receipt AT-URI using a DID authority; lookup matches the URI only, not a CID.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.funding.getReceipt.output"}}}}},"x-lexicon-id":"org.hypercerts.funding.getReceipt","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/funding/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a funding-receipt record AT-URI."},{"name":"RecordNotFound","description":"No indexed funding receipt exists at this AT-URI."},{"name":"ReceiptQueryFailed","description":"The receipt query or publisher hydration failed."}]}}, + "/xrpc/org.hypercerts.funding.listReceipts": {"get":{"operationId":"org_hypercerts_funding_listReceipts","tags":["org.hypercerts.funding"],"description":"Lists indexed funding receipts matching the supplied filters: filters combine with AND, while values within an array combine with OR. Repeat array parameters to supply multiple values; duplicates are ignored. Authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs, including facilitator, personal, and group repositories.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"uris","in":"query","required":false,"description":"Funding-receipt AT-URIs with a DID authority, matched by URI only, not CID.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"from","in":"query","required":false,"description":"Sender DIDs or full record AT-URIs with a DID authority. DIDs match only the DID variant; AT-URIs match only the strong-reference URI, not CID. Resolve handles to DIDs; invalid entries reject the request with the parameter and entry identified.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"to","in":"query","required":false,"description":"Recipient DIDs or full record AT-URIs with a DID authority. DIDs match only the DID variant; AT-URIs match only the strong-reference URI, not CID. Resolve handles to DIDs; invalid entries reject the request with the parameter and entry identified.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"forUris","in":"query","required":false,"description":"Funded-target AT-URIs with a DID authority, matched by URI only; CID is not compared.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"at-uri"}},"style":"form","explode":true},{"name":"transactionIds","in":"query","required":false,"description":"Exact transaction ID matches; values are not normalized.","schema":{"type":"array","maxItems":100,"items":{"type":"string"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for ordering by createdAt then URI; defaults to desc.","schema":{"type":"string","enum":["asc","desc"]}},{"name":"limit","in":"query","required":false,"description":"Maximum receipts per page; default 25.","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep all other parameters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.funding.listReceipts.output"}}}}},"x-lexicon-id":"org.hypercerts.funding.listReceipts","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/funding/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter or pagination parameter is invalid."},{"name":"ReceiptQueryFailed","description":"The receipt query or publisher hydration failed."}]}}, + "/xrpc/org.hypercerts.vocab.getVocabTag": {"get":{"operationId":"org_hypercerts_vocab_getVocabTag","tags":["org.hypercerts.vocab"],"description":"Looks up one published vocabulary tag by full DID-authority AT-URI and returns its unchanged record with publisher details; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full AT-URI of the vocabulary-tag record, using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.vocab.getVocabTag.output"}}}}},"x-lexicon-id":"org.hypercerts.vocab.getVocabTag","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/vocab/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid, targets another collection, or query parameters are unsupported."},{"name":"RecordNotFound","description":"No indexed vocabulary tag exists at this AT-URI."},{"name":"VocabTagQueryFailed","description":"The indexed tag or required publisher lookup failed; retry after checking the HappyView PostgreSQL service."}]}}, + "/xrpc/org.hypercerts.vocab.listVocabTags": {"get":{"operationId":"org_hypercerts_vocab_listVocabTags","tags":["org.hypercerts.vocab"],"description":"Lists published vocabulary tags by optional publisher DIDs with stable cursor pagination; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; values are ORed.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Direction for (createdAt, uri); default desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Page size from 1 to 100; default 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor; keep direction and filters unchanged between pages.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.vocab.listVocabTags.output"}}}}},"x-lexicon-id":"org.hypercerts.vocab.listVocabTags","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/vocab/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A filter value, page bound, cursor, repeated scalar, or query parameter is invalid."},{"name":"VocabTagQueryFailed","description":"The indexed tags or required publisher lookups could not be queried; retry after checking the HappyView PostgreSQL service."}]}}, + "/xrpc/org.hypercerts.workscope.getWorkscopeTag": {"get":{"operationId":"org_hypercerts_workscope_getWorkscopeTag","tags":["org.hypercerts.workscope"],"description":"Returns an indexed work-scope tag and its hydrated publisher for an exact AT-URI; authentication is not required.","parameters":[{"name":"uri","in":"query","required":true,"description":"Full work-scope-tag AT-URI using a DID authority.","schema":{"type":"string","format":"at-uri"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.workscope.getWorkscopeTag.output"}}}}},"x-lexicon-id":"org.hypercerts.workscope.getWorkscopeTag","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/workscope-tags/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"The URI is invalid or is not a work-scope-tag AT-URI."},{"name":"RecordNotFound","description":"No indexed work-scope tag exists at this AT-URI."},{"name":"WorkscopeTagQueryFailed","description":"The record lookup or publisher hydration failed."}]}}, + "/xrpc/org.hypercerts.workscope.listWorkscopeTags": {"get":{"operationId":"org_hypercerts_workscope_listWorkscopeTags","tags":["org.hypercerts.workscope"],"description":"Lists indexed work-scope tags by publisher DID with stable timestamp-and-URI ordering; authentication is not required.","parameters":[{"name":"authors","in":"query","required":false,"description":"Publisher repository DIDs; repeat this key for OR matching, up to 100 values.","schema":{"type":"array","maxItems":100,"items":{"type":"string","format":"did"}},"style":"form","explode":true},{"name":"sortDirection","in":"query","required":false,"description":"Sort direction for timestamp-and-URI ordering; defaults to desc.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","required":false,"description":"Maximum page size from 1 to 100; defaults to 25.","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor bound to sortDirection; keep other parameters unchanged between pages.","schema":{"type":"string","maxLength":8192}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/org.hypercerts.workscope.listWorkscopeTags.output"}}}}},"x-lexicon-id":"org.hypercerts.workscope.listWorkscopeTags","x-hypercerts-coverage":"manifest-registered","x-hypercerts-source":{"module":"modules/workscope-tags/manifest.json"},"x-lexicon-errors":[{"name":"InvalidRequest","description":"A query parameter is invalid."},{"name":"WorkscopeTagQueryFailed","description":"The record query or publisher hydration failed."}]}} }, "components": { "schemas": { @@ -111,9 +113,13 @@ "app.certified.graph.entityFollow.main": {"type":"record","description":"Record declaring a social 'follow' relationship with a non-account entity, such as a specific record. Account (DID) follows are out of scope and belong to app.certified.graph.follow. The subject is an open union so additional non-DID entity kinds can be added in the future."}, "app.certified.graph.getEntityFollow.entityFollowRecordView": {"type":"object","description":"Raw indexed entity-follow record view, including the DID that published the relationship.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/app.certified.graph.entityFollow.main"}}}, "app.certified.graph.getEntityFollow.output": {"type":"object","required":["follow"],"properties":{"follow":{"anyOf":[{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.entityFollowRecordView"},{"type":"null"}]}}}, + "app.certified.graph.getEntityFollows.entityFollowResult": {"type":"object","required":["entity","follow"],"properties":{"entity":{"type":"string","description":"Entity AT-URI from the corresponding request occurrence.","format":"at-uri"},"follow":{"anyOf":[{"$ref":"#/components/schemas/app.certified.graph.getEntityFollow.entityFollowRecordView"},{"type":"null"}]}}}, + "app.certified.graph.getEntityFollows.output": {"type":"object","required":["results"],"properties":{"results":{"type":"array","description":"One result per supplied entity occurrence, in request order.","items":{"$ref":"#/components/schemas/app.certified.graph.getEntityFollows.entityFollowResult"}}}}, "app.certified.graph.follow.main": {"type":"record","description":"Record declaring a social 'follow' relationship of another account."}, "app.certified.graph.getFollow.followRecordView": {"type":"object","description":"Raw indexed follow record with its publisher DID. For duplicates, the earliest createdAt wins, with URI ascending on ties.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/app.certified.graph.follow.main"}}}, "app.certified.graph.getFollow.output": {"type":"object","description":"Representative raw follow record, or null if no relationship is indexed.","required":["follow"],"properties":{"follow":{"anyOf":[{"$ref":"#/components/schemas/app.certified.graph.getFollow.followRecordView"},{"type":"null"}]}}}, + "app.certified.graph.getFollows.followResult": {"type":"object","required":["subject","follow"],"properties":{"subject":{"type":"string","description":"Subject DID from the corresponding request occurrence.","format":"did"},"follow":{"anyOf":[{"$ref":"#/components/schemas/app.certified.graph.getFollow.followRecordView"},{"type":"null"}]}}}, + "app.certified.graph.getFollows.output": {"type":"object","required":["results"],"properties":{"results":{"type":"array","description":"One result per supplied subject occurrence, in request order.","items":{"$ref":"#/components/schemas/app.certified.graph.getFollows.followResult"}}}}, "app.certified.graph.getFollow.actorFollowView": {"type":"object","description":"Displayed actor with nullable profile and organization sidecars, paired with the representative follow record.","required":["did","profile","organization","follow"],"properties":{"did":{"type":"string","format":"did"},"profile":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.profileView"},{"type":"null"}]},"organization":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.organizationView"},{"type":"null"}]},"follow":{"$ref":"#/components/schemas/app.certified.graph.getFollow.followRecordView"}}}, "app.certified.graph.listActorFollowers.output": {"type":"object","required":["followers","totalCount"],"properties":{"followers":{"type":"array","description":"Incoming relationships; each displayed DID identifies the actor who published the follow.","items":{"$ref":"#/components/schemas/app.certified.graph.getFollow.actorFollowView"}},"totalCount":{"type":"integer","description":"Count of deduplicated relationships before cursor and limit are applied.","minimum":0},"cursor":{"type":"string","description":"Next-page cursor; omitted when no more results are available."}}}, "app.certified.graph.listActorFollowing.output": {"type":"object","required":["following","totalCount"],"properties":{"following":{"type":"array","description":"Outgoing relationships; each displayed DID identifies the actor named as the follow subject.","items":{"$ref":"#/components/schemas/app.certified.graph.getFollow.actorFollowView"}},"totalCount":{"type":"integer","description":"Count of deduplicated relationships before cursor and limit are applied.","minimum":0},"cursor":{"type":"string","description":"Next-page cursor; omitted when no more results are available."}}}, @@ -127,7 +133,7 @@ "org.hypercerts.claim.getActivity.contributorInformationView": {"type":"object","description":"Contributor-information record matched by the source reference's URI and CID; newer versions are not substituted.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/org.hypercerts.claim.contributorInformation.main"}}}, "org.hypercerts.claim.getActivity.contributorActorView": {"type":"object","description":"Contributor DID with its current profile, if indexed.","required":["did","profile"],"properties":{"did":{"type":"string","format":"did"},"profile":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.profileView"},{"type":"null"}]}}}, "org.hypercerts.claim.getActivity.activityContributorView": {"type":"object","description":"Source contributor fields with nullable hydrated projections; unavailable referenced information or unresolved identities are null, while lookup failures fail the query.","required":["contributorIdentity","contributorInformation","actor"],"properties":{"contributorIdentity":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.claim.activity.contributorIdentity"},{"$ref":"#/components/schemas/com.atproto.repo.strongRef.main"}]},"contributionWeight":{"type":"string","maxLength":100},"contributionDetails":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.claim.activity.contributorRole"},{"$ref":"#/components/schemas/com.atproto.repo.strongRef.main"}]},"contributorInformation":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.contributorInformationView"},{"type":"null"}]},"actor":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.contributorActorView"},{"type":"null"}]}}}, - "org.hypercerts.claim.getActivity.activityView": {"type":"object","description":"Activity record with its original payload, hydrated author, and contributors only when present in the source record.","required":["uri","cid","indexedAt","did","author","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"author":{"$ref":"#/components/schemas/org.hypercerts.api.defs.actorView"},"record":{"$ref":"#/components/schemas/org.hypercerts.claim.activity.main"},"contributors":{"type":"array","description":"Hydrated contributor projection in the same order and with the same multiplicity as record.contributors; omitted only when the source field is absent.","items":{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.activityContributorView"}}}}, + "org.hypercerts.claim.getActivity.activityView": {"type":"object","description":"Activity record with its original payload, hydrated author, and contributors only when present in the source record.","required":["uri","cid","indexedAt","did","author","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"author":{"$ref":"#/components/schemas/org.hypercerts.api.defs.actorView"},"record":{"$ref":"#/components/schemas/org.hypercerts.claim.activity.main"},"contributors":{"type":"array","description":"Hydrated projection of object entries in record.contributors, preserving their relative order and multiplicity; legacy string entries are skipped and the original record remains unchanged. Omitted only when the source field is absent; if it contains no object entries, this is an empty array.","items":{"$ref":"#/components/schemas/org.hypercerts.claim.getActivity.activityContributorView"}}}}, "org.hypercerts.collection.getCollection.collectionProfileView": {"type":"object","description":"Profile record view used in collection author projections; indexedAt is required and may be null when the index has no timestamp.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/app.certified.actor.profile.main"}}}, "org.hypercerts.collection.getCollection.collectionOrganizationView": {"type":"object","description":"Organization sidecar view used in collection author projections; indexedAt is required and may be null when the index has no timestamp.","required":["uri","cid","indexedAt","did","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"record":{"$ref":"#/components/schemas/app.certified.actor.organization.main"}}}, "org.hypercerts.collection.getCollection.collectionActorView": {"type":"object","description":"Collection author DID with nullable profile and organization sidecar views whose indexedAt values may be null.","required":["did","profile","organization"],"properties":{"did":{"type":"string","format":"did"},"profile":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.collection.getCollection.collectionProfileView"},{"type":"null"}]},"organization":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.collection.getCollection.collectionOrganizationView"},{"type":"null"}]}}}, @@ -181,7 +187,7 @@ "org.hypercerts.context.evaluation.main": {"type":"record","description":"An evaluation of a hypercert record (e.g. an activity and its impact)."}, "org.hypercerts.context.getEvaluation.hydratedEvaluationEvaluatorView": {"type":"object","description":"A valid original evaluator entry with its profile and organization sidecars looked up.","required":["did","hydrationStatus","profile","organization"],"properties":{"did":{"type":"string","format":"did"},"hydrationStatus":{"type":"string","const":"hydrated"},"profile":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.profileView"},{"type":"null"}]},"organization":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.api.defs.organizationView"},{"type":"null"}]}}}, "org.hypercerts.context.getEvaluation.omittedEvaluationEvaluatorView": {"type":"object","description":"A valid original evaluator entry beyond the hydration position limit.","required":["did","hydrationStatus"],"properties":{"did":{"type":"string","format":"did"},"hydrationStatus":{"type":"string","const":"omitted"}}}, - "org.hypercerts.context.getEvaluation.evaluationView": {"type":"object","description":"Evaluation with the original record, hydrated publisher, and evaluator projections. The record is not rewritten when evaluator data is malformed. A missing or non-array evaluator container yields an empty hydrated array; malformed entries are omitted. For hydrated actors, missing profile or organization sidecars are null; query or hydration failures fail the request. Evaluator arrays over 1000 entries still fail exact lookup and are skipped by lists.","required":["uri","cid","indexedAt","did","author","record","evaluators"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"type":"string","format":"datetime"},"did":{"type":"string","format":"did"},"author":{"$ref":"#/components/schemas/org.hypercerts.api.defs.actorView"},"record":{"$ref":"#/components/schemas/org.hypercerts.context.evaluation.main"},"evaluators":{"type":"array","description":"Dense projection of valid record.evaluators object entries with valid DIDs, preserving source order and duplicates. Entries with malformed shapes or invalid DIDs are omitted without changing the original record; a missing or non-array source yields an empty array. Valid entries at source positions after 100 remain present with hydrationStatus \"omitted\" and no profile or organization. The raw evaluator array is limited to 1000 entries; over-limit records fail exact lookup and are skipped by lists.","maxItems":1000,"items":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.hydratedEvaluationEvaluatorView"},{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.omittedEvaluationEvaluatorView"}]}}}}, + "org.hypercerts.context.getEvaluation.evaluationView": {"type":"object","description":"Evaluation with the original record, hydrated publisher, and evaluator projections. The record is not rewritten when evaluator data is malformed. A missing or non-array evaluator container yields an empty hydrated array; malformed entries are omitted. For hydrated actors, missing profile or organization sidecars are null; query or hydration failures fail the request. Evaluator arrays over 1000 entries still fail exact lookup and are skipped by lists.","required":["uri","cid","indexedAt","did","author","record","evaluators"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"type":"string","format":"datetime"},"did":{"type":"string","format":"did"},"author":{"$ref":"#/components/schemas/org.hypercerts.api.defs.actorView"},"record":{"$ref":"#/components/schemas/org.hypercerts.context.evaluation.main"},"evaluators":{"type":"array","description":"Dense projection of valid record.evaluators object entries with valid DIDs, preserving their source order and duplicates. Entries with malformed shapes or invalid DIDs are omitted without changing the original record; a missing or non-array source yields an empty array. Valid entries at source positions after 100 remain present with hydrationStatus \"omitted\" and no profile or organization. The raw evaluator array is limited to 1000 entries; over-limit records fail exact lookup and are skipped by lists.","maxItems":1000,"items":{"anyOf":[{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.hydratedEvaluationEvaluatorView"},{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.omittedEvaluationEvaluatorView"}]}}}}, "org.hypercerts.context.getEvaluation.output": {"type":"object","required":["evaluation"],"properties":{"evaluation":{"$ref":"#/components/schemas/org.hypercerts.context.getEvaluation.evaluationView"}}}, "org.hypercerts.context.measurement.main": {"type":"record","description":"Measurement data related to one or more records (e.g. activities, projects, etc.)."}, "org.hypercerts.context.getMeasurement.measurementView": {"type":"object","description":"Indexed measurement with its publisher actor and original record.","required":["uri","cid","indexedAt","did","author","record"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"indexedAt":{"anyOf":[{"type":"string","format":"datetime"},{"type":"null"}]},"did":{"type":"string","format":"did"},"author":{"$ref":"#/components/schemas/org.hypercerts.api.defs.actorView"},"record":{"$ref":"#/components/schemas/org.hypercerts.context.measurement.main"}}}, diff --git a/.agents/skills/hypercerts-api/references/reliability.md b/.agents/skills/hypercerts-api/references/reliability.md index c700b6c..e0fd2e6 100644 --- a/.agents/skills/hypercerts-api/references/reliability.md +++ b/.agents/skills/hypercerts-api/references/reliability.md @@ -28,6 +28,7 @@ For a potentially fresh record that is absent: ## Null, omitted, empty, not-found, and failure are different - **Null field:** the endpoint returned a known enclosing result, but one optional or nullable projection is unavailable/absent under that contract. Examples include a null profile in batch `getProfiles`, a null organization in `getOrganizations`, a null `follow` in `getFollow`, or a null resolved collection-item `record`. Preserve and render this state intentionally. +- **Batch relationship result:** `getFollows` and `getEntityFollows` return an explicit target and nullable `follow` for every input occurrence. Null means no relationship is indexed for that actor and target; a database/query failure remains an error and must not be converted to null. - **Omitted optional field:** the schema does not require the property in that condition. Do not treat omission as the same representation as explicit null unless the exact endpoint contract says so. - **Empty list:** the query succeeded and returned no matching items. It is not the same as an invalid query or failed hydration. - **Not-found error:** a `get...` query may declare `RecordNotFound` when no matching record is indexed. That is not necessarily a successful response containing a null record. Use the endpoint's own declared error contract. diff --git a/.agents/skills/hypercerts-api/references/request-patterns.md b/.agents/skills/hypercerts-api/references/request-patterns.md index 7d4364e..503a3d1 100644 --- a/.agents/skills/hypercerts-api/references/request-patterns.md +++ b/.agents/skills/hypercerts-api/references/request-patterns.md @@ -37,6 +37,8 @@ Do not use `authors[]=` or comma-joined values unless that exact endpoint contra A useful starting distinction: arrays often mean “any of these” for a filter, while separate filters may combine with AND, but this is **not a universal rule**. For example, the bundled collection contract says `tagUris` requires every supplied tag URI, while `itemUris` matches URI without CID. Confirm the exact operation before relying on filter logic. +The batch relationship checks `app.certified.graph.getFollows` and `app.certified.graph.getEntityFollows` each take one `actor` and 1–100 repeated, unbracketed `subjects` or `entities` values. Keep duplicates if the caller supplied them: each output `results` entry echoes its target and preserves its position. The encoded GET request-target must stay within 7,000 bytes. A null `follow` means only that no indexed relationship was found; database failures are errors, not nulls. + ## Keep page requests cursor-consistent For a list/search operation whose output has a cursor, keep a base set of all non-cursor parameters and replace only the cursor on continuation. Preserve repeated array values, filters, `search`, `limit`, and `sortDirection` exactly. The example below is illustrative; verify the chosen endpoint's cursor binding and response-array name. diff --git a/.changeset/batch-follow-queries.md b/.changeset/batch-follow-queries.md new file mode 100644 index 0000000..61a4411 --- /dev/null +++ b/.changeset/batch-follow-queries.md @@ -0,0 +1,5 @@ +--- +'@hypercerts-org/hypercerts-api': minor +--- + +Clients can check multiple actor or entity follow relationships in one request. The new batch queries return one explicitly keyed result for each supplied target, preserving request order and duplicates; null means no indexed relationship, while lookup failures remain errors. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b05d7ab..560993b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,7 +56,7 @@ The HTTP gate fails if it discovers no suites, executes no `node:test` cases, or - Context attachment and evaluation retrieval with publisher/evaluator sidecar hydration, list filters, pagination across tied `createdAt` values, and named runtime errors. - Activity retrieval with contributor-sidecar hydration, author/organization/contributor/URI filters, tied timestamp pagination, and literal wildcard search. - Collection retrieval with CBOR-derived CIDs, location/tag projections, author, organization, item and tag filters, title/shortDescription search, `createdAt`/URI pagination ties, and source-order item pagination with exact-version resolution. -- Graph actor/entity lookups and lists, tied-key pagination, nullable profile/organization sidecars, entity target resolution, and the global recent-follows `before` filter. +- Graph actor/entity lookups and lists, tied-key pagination, nullable profile/organization sidecars, entity target resolution, and the global recent-follows `before` filter. The `getFollows` and `getEntityFollows` batch queries also cover repeated target order and duplicates, explicit target keys, nullable raw views, and invalid-entry rejection. - Feed-reaction subject and actor queries, earliest duplicate selection, URI-only subject matching across CIDs, distinct-actor totals, tied cursor pagination, page-only sidecar hydration, raw `via`/signature preservation, and actor queries without hydration. - Contribution exact-record retrieval, repeated publisher filters, tied ascending/descending cursor pagination, nullable publisher sidecars, and named errors. - Badge, badge-query, acknowledgement, contribution, and rights HTTP fixtures with CBOR-derived record CIDs; contribution DIDs are distinct from baseline fixture identities. diff --git a/api/README.md b/api/README.md index b1fb4cb..2cb087f 100644 --- a/api/README.md +++ b/api/README.md @@ -6,6 +6,10 @@ This package owns the shared API installer and build tooling, pinned upstream Le Use the [`docs/` endpoint explorer](../docs/) and generated [OpenAPI reference](../docs/openapi.json) for endpoint names and contracts. The [coverage report](../docs/coverage.json) lists manifest-registered operations; inclusion does not imply runtime or deployment validation. See [docs/README.md](../docs/README.md) to run the explorer locally and refresh its committed reference. +## Graph follow capability + +`modules/actor-follow/manifest.json` provides `app.certified.graph.getFollows` for checking up to 100 subject DIDs against one actor. `modules/entity-follow/manifest.json` provides `app.certified.graph.getEntityFollows` for checking up to 100 entity AT-URIs against one actor. Supply targets as repeated query parameters; both queries preserve every input position and duplicate, return the original target beside its raw nullable representative, and reject the whole request when any target is invalid. The encoded GET request-target is bounded to 7,000 bytes. A null follow means no indexed relationship; database failures remain errors. + ## Feed reaction capability `modules/feed-reactions/manifest.json` owns the like and repost record schemas and four public queries: `app.certified.feed.getLikes`, `getReposts`, `getActorLikes`, and `getActorReposts`. The root manifest includes this independently installable module in the aggregate bundle. Its record schemas enable backfill, and queries read the current indexed records directly; deleted reactions therefore stop appearing once removed from HappyView's index, without a separate stored count. diff --git a/api/lexicons/app.certified.graph.getEntityFollows.json b/api/lexicons/app.certified.graph.getEntityFollows.json new file mode 100644 index 0000000..6dfc8d3 --- /dev/null +++ b/api/lexicons/app.certified.graph.getEntityFollows.json @@ -0,0 +1,65 @@ +{ + "lexicon": 1, + "id": "app.certified.graph.getEntityFollows", + "defs": { + "main": { + "type": "query", + "description": "Returns the representative raw entity-follow record, or null, for each requested actor-to-entity relationship. Authentication is not required. Supply 1–100 repeated entities values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": { + "type": "params", + "required": ["actor", "entities"], + "properties": { + "actor": { + "type": "string", + "format": "did", + "maxLength": 2048, + "description": "DID that published the entity-follow records." + }, + "entities": { + "type": "array", + "description": "Full entity AT-URIs using DID authorities, supplied as repeated unbracketed query values. Matching uses the subject URI and ignores CID; every occurrence is returned in request order, including duplicates.", + "minLength": 1, + "maxLength": 100, + "items": { "type": "string", "format": "at-uri", "maxLength": 8192 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { "type": "ref", "ref": "#output" } + }, + "errors": [ + { "name": "InvalidRequest", "description": "The actor DID, entities array, an entity AT-URI, request-target length, or another query parameter is invalid." }, + { "name": "EntityFollowQueryFailed", "description": "The indexed entity-follow relationships could not be queried." } + ] + }, + "output": { + "type": "object", + "required": ["results"], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied entity occurrence, in request order.", + "items": { "type": "ref", "ref": "#entityFollowResult" } + } + } + }, + "entityFollowResult": { + "type": "object", + "required": ["entity", "follow"], + "nullable": ["follow"], + "properties": { + "entity": { + "type": "string", + "format": "at-uri", + "description": "Entity AT-URI from the corresponding request occurrence." + }, + "follow": { + "type": "ref", + "ref": "app.certified.graph.getEntityFollow#entityFollowRecordView", + "description": "Earliest indexed matching record by createdAt, with URI ascending on ties, or null when no relationship is indexed. Query failures are errors, not null results." + } + } + } + } +} diff --git a/api/lexicons/app.certified.graph.getFollows.json b/api/lexicons/app.certified.graph.getFollows.json new file mode 100644 index 0000000..ce7b8c1 --- /dev/null +++ b/api/lexicons/app.certified.graph.getFollows.json @@ -0,0 +1,65 @@ +{ + "lexicon": 1, + "id": "app.certified.graph.getFollows", + "defs": { + "main": { + "type": "query", + "description": "Returns the representative raw follow record, or null, for each requested actor-to-actor relationship. Authentication is not required. Supply 1–100 repeated subjects values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": { + "type": "params", + "required": ["actor", "subjects"], + "properties": { + "actor": { + "type": "string", + "format": "did", + "maxLength": 2048, + "description": "DID of the actor who published the follow records." + }, + "subjects": { + "type": "array", + "description": "Subject DIDs to check, supplied as repeated unbracketed query values. Every occurrence is returned in the same order, including duplicates.", + "minLength": 1, + "maxLength": 100, + "items": { "type": "string", "format": "did", "maxLength": 2048 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { "type": "ref", "ref": "#output" } + }, + "errors": [ + { "name": "InvalidRequest", "description": "The actor DID, subjects array, request-target length, or another query parameter is invalid." }, + { "name": "ActorFollowQueryFailed", "description": "The indexed follow relationships could not be queried." } + ] + }, + "output": { + "type": "object", + "required": ["results"], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied subject occurrence, in request order.", + "items": { "type": "ref", "ref": "#followResult" } + } + } + }, + "followResult": { + "type": "object", + "required": ["subject", "follow"], + "nullable": ["follow"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "Subject DID from the corresponding request occurrence." + }, + "follow": { + "type": "ref", + "ref": "app.certified.graph.getFollow#followRecordView", + "description": "Earliest indexed matching record by createdAt, with URI ascending on ties, or null when no relationship is indexed. Query failures are errors, not null results." + } + } + } + } +} diff --git a/api/lua/endpoints/getEntityFollows.lua b/api/lua/endpoints/getEntityFollows.lua new file mode 100644 index 0000000..e22319e --- /dev/null +++ b/api/lua/endpoints/getEntityFollows.lua @@ -0,0 +1,203 @@ +local function valid_did(value) + if type(value) ~= "string" or #value > 2048 then return false end + local method, specific = value:match("^did:([a-z]+):(.+)$") + if not method or not specific or specific:sub(-1) == ":" or specific:sub(-1) == "%" + or value:find("[^%w%.:_%%%-]") then return false end + return true +end + +local function invalid(message) + error("InvalidRequest: " .. message, 0) +end + +local function keys_only(values, allowed, unknown_message_prefix) + for key in pairs(values) do + if not allowed[key] then + invalid(unknown_message_prefix and (unknown_message_prefix .. key) or "unknown query parameter") + end + end +end + +local function scalar(params, key) + local value = params[key] + if value == nil then return nil end + if type(value) ~= "string" and type(value) ~= "number" then + invalid(key .. " must occur once") + end + return tostring(value) +end + +local function valid_record_key(value) + return #value >= 1 and #value <= 512 and value ~= "." and value ~= ".." + and not value:find("[^%w_~%.:%-]") +end + +local function valid_record_uri(value) + if type(value) ~= "string" or value:find("[?#]") then return false end + local authority, collection, rkey = value:match("^at://([^/]+)/([^/]+)/([^/]+)$") + if not authority or not valid_did(authority) or not valid_record_key(rkey) then return false end + return true, collection, authority +end + +local ENTITY_FOLLOW = "app.certified.graph.entityFollow" +local ENTITY_FOLLOW_NULL = json.decode("null") + +local function entity_follow_query(sql, values) + if db.backend() ~= "postgres" then + error("EntityFollowQueryFailed: entity-follow queries require PostgreSQL", 0) + end + local ok, result = pcall(db.raw, sql, values) + if not ok or type(result) ~= "table" then + error("EntityFollowQueryFailed: entity-follow lookup failed", 0) + end + return result +end + +-- Keep malformed publisher timestamps out of casts; preserve the actor-follow fallback order. +local function entity_follow_sort_key() + local created = "record::jsonb->>'createdAt'" + local zoned = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" + return "CASE WHEN jsonb_typeof(record::jsonb->'createdAt') = 'string' AND " .. created .. " ~ '" .. zoned .. "' AND " .. created .. " !~ '-00:00$' AND pg_input_is_valid(" .. created .. ", 'timestamptz') THEN (" .. created .. ")::timestamptz ELSE COALESCE(indexed_at::timestamptz, created_at::timestamptz) END" +end + +local function entity_follow_indexed_record_view(row) + return { + uri = row.uri, + cid = row.cid, + indexedAt = row.indexed_at == nil and ENTITY_FOLLOW_NULL or row.indexed_at, + did = row.did, + record = json.decode(row.record), + } +end + +local function entity_follow_record_view(row) + local view = entity_follow_indexed_record_view(row) + view["$type"] = "app.certified.graph.getEntityFollow#entityFollowRecordView" + return view +end + +local FOLLOW_BATCH_MAX_TARGETS = 100 +local FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES = 7000 + +local function encoded_query_value_length(value) + local length = 0 + for index = 1, #value do + local byte = value:byte(index) + if (byte >= 48 and byte <= 57) or (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) + or byte == 42 or byte == 45 or byte == 46 or byte == 95 then + length = length + 1 + else + length = length + 3 + end + end + return length +end + +local function follow_batch_targets(actor, key, value, validate, description, endpoint) + if value == nil then invalid(key .. " is required") end + + local targets = {} + if type(value) == "string" then + targets[1] = value + elseif type(value) == "table" then + local count, max_index = 0, 0 + for index in pairs(value) do + if type(index) ~= "number" or index < 1 or index % 1 ~= 0 then + invalid(key .. " must be a repeated string parameter") + end + count = count + 1 + if index > max_index then max_index = index end + end + if count ~= max_index then invalid(key .. " must be a contiguous array") end + if count < 1 or count > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + for index = 1, max_index do + targets[index] = value[index] + end + else + invalid(key .. " must be a repeated string parameter") + end + + if #targets < 1 or #targets > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + + local request_target_length = #("/xrpc/" .. endpoint .. "?actor=") + encoded_query_value_length(actor) + local unique, seen = {}, {} + for _, target in ipairs(targets) do + if type(target) ~= "string" or not validate(target) then + invalid("each " .. key .. " value must be " .. description) + end + request_target_length = request_target_length + #("&" .. key .. "=") + encoded_query_value_length(target) + if not seen[target] then + seen[target] = true + unique[#unique + 1] = target + end + end + + if request_target_length > FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES then + invalid(key .. " makes the encoded GET request-target exceed " .. FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES .. " bytes") + end + return targets, unique +end + +local function get_entity_follows() + keys_only(params, { actor = true, entities = true }) + local actor = scalar(params, "actor") + if not actor or not valid_did(actor) then invalid("actor must be a valid DID") end + local function valid_nsid(value) + if type(value) ~= "string" or #value > 317 then return false end + local segments = {} + for segment in value:gmatch("[^%.]+") do segments[#segments + 1] = segment end + if #segments < 3 or table.concat(segments, ".") ~= value then return false end + + for _, segment in ipairs(segments) do + if #segment > 63 or segment:find("[^A-Za-z0-9%-]") + or segment:sub(1, 1) == "-" or segment:sub(-1) == "-" then + return false + end + end + + if not segments[1]:sub(1, 1):match("^[A-Za-z]$") then return false end + return segments[#segments]:match("^[A-Za-z][A-Za-z0-9]*$") ~= nil + end + + local function valid_entity_uri(value) + if #value > 8192 then return false end + local valid, collection = valid_record_uri(value) + return valid and valid_nsid(collection) + end + local entities, unique_entities = follow_batch_targets( + actor, "entities", params.entities, valid_entity_uri, + "full AT-URI with a DID authority and valid collection NSID", "app.certified.graph.getEntityFollows") + + local values, requested = { ENTITY_FOLLOW, actor }, {} + for _, entity_uri in ipairs(unique_entities) do + values[#values + 1] = entity_uri + requested[#requested + 1] = "($" .. #values .. "::text)" + end + + local rows = entity_follow_query( + "WITH requested(target) AS (VALUES " .. table.concat(requested, ", ") .. ") " + .. "SELECT DISTINCT ON (requested.target) requested.target AS target, uri, did, cid, " + .. "indexed_at::text AS indexed_at, record::text AS record FROM requested " + .. "JOIN happyview_records ON collection = $1 AND did = $2 " + .. "AND record::jsonb->'subject'->>'uri' = requested.target " + .. "CROSS JOIN LATERAL (SELECT " .. entity_follow_sort_key() .. " AS sort_at) sorted " + .. "ORDER BY requested.target, sorted.sort_at ASC, uri ASC", + values) + + local follows_by_entity = {} + for _, row in ipairs(rows) do follows_by_entity[row.target] = entity_follow_record_view(row) end + + local results = {} + for _, entity_uri in ipairs(entities) do + results[#results + 1] = { entity = entity_uri, follow = follows_by_entity[entity_uri] or ENTITY_FOLLOW_NULL } + end + return { results = toarray(results) } +end + +function handle() + return get_entity_follows() +end diff --git a/api/lua/endpoints/getFollows.lua b/api/lua/endpoints/getFollows.lua new file mode 100644 index 0000000..ed89a8e --- /dev/null +++ b/api/lua/endpoints/getFollows.lua @@ -0,0 +1,159 @@ +local function valid_did(value) + if type(value) ~= "string" or #value > 2048 then return false end + local method, specific = value:match("^did:([a-z]+):(.+)$") + if not method or not specific or specific:sub(-1) == ":" or specific:sub(-1) == "%" + or value:find("[^%w%.:_%%%-]") then return false end + return true +end + +local function invalid(message) + error("InvalidRequest: " .. message, 0) +end + +local function keys_only(values, allowed, unknown_message_prefix) + for key in pairs(values) do + if not allowed[key] then + invalid(unknown_message_prefix and (unknown_message_prefix .. key) or "unknown query parameter") + end + end +end + +local function scalar(params, key) + local value = params[key] + if value == nil then return nil end + if type(value) ~= "string" and type(value) ~= "number" then + invalid(key .. " must occur once") + end + return tostring(value) +end + +local NULL = json.decode("null") + +local function record_view(row) + return { + uri = row.uri, + cid = row.cid, + indexedAt = row.indexed_at == nil and NULL or row.indexed_at, + did = row.did, + record = json.decode(row.record), + } +end + +local FOLLOW = "app.certified.graph.follow" + +-- Guard the cast inside CASE; a WHERE filter cannot protect it from planner reordering. +local function follow_sort_key() + local created = "record::jsonb->>'createdAt'" + local zoned = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$" + return "CASE WHEN jsonb_typeof(record::jsonb->'createdAt') = 'string' AND " .. created .. " ~ '" .. zoned .. "' AND " .. created .. " !~ '-00:00$' AND pg_input_is_valid(" .. created .. ", 'timestamptz') THEN (" .. created .. ")::timestamptz ELSE COALESCE(indexed_at::timestamptz, created_at::timestamptz) END" +end + +local function query(sql, values) + if db.backend() ~= "postgres" then error("ActorFollowQueryFailed: actor-follow queries require PostgreSQL", 0) end + local ok, result = pcall(db.raw, sql, values) + if not ok then error("ActorFollowQueryFailed: actor-follow lookup failed", 0) end + return result +end + +local FOLLOW_BATCH_MAX_TARGETS = 100 +local FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES = 7000 + +local function encoded_query_value_length(value) + local length = 0 + for index = 1, #value do + local byte = value:byte(index) + if (byte >= 48 and byte <= 57) or (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) + or byte == 42 or byte == 45 or byte == 46 or byte == 95 then + length = length + 1 + else + length = length + 3 + end + end + return length +end + +local function follow_batch_targets(actor, key, value, validate, description, endpoint) + if value == nil then invalid(key .. " is required") end + + local targets = {} + if type(value) == "string" then + targets[1] = value + elseif type(value) == "table" then + local count, max_index = 0, 0 + for index in pairs(value) do + if type(index) ~= "number" or index < 1 or index % 1 ~= 0 then + invalid(key .. " must be a repeated string parameter") + end + count = count + 1 + if index > max_index then max_index = index end + end + if count ~= max_index then invalid(key .. " must be a contiguous array") end + if count < 1 or count > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + for index = 1, max_index do + targets[index] = value[index] + end + else + invalid(key .. " must be a repeated string parameter") + end + + if #targets < 1 or #targets > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + + local request_target_length = #("/xrpc/" .. endpoint .. "?actor=") + encoded_query_value_length(actor) + local unique, seen = {}, {} + for _, target in ipairs(targets) do + if type(target) ~= "string" or not validate(target) then + invalid("each " .. key .. " value must be " .. description) + end + request_target_length = request_target_length + #("&" .. key .. "=") + encoded_query_value_length(target) + if not seen[target] then + seen[target] = true + unique[#unique + 1] = target + end + end + + if request_target_length > FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES then + invalid(key .. " makes the encoded GET request-target exceed " .. FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES .. " bytes") + end + return targets, unique +end + +local function get_follows() + keys_only(params, { actor = true, subjects = true }) + local actor = scalar(params, "actor") + if not actor or not valid_did(actor) then invalid("actor must be a valid DID") end + local subjects, unique_subjects = follow_batch_targets( + actor, "subjects", params.subjects, valid_did, "valid DIDs", "app.certified.graph.getFollows") + + local values, requested = { FOLLOW, actor }, {} + for _, subject in ipairs(unique_subjects) do + values[#values + 1] = subject + requested[#requested + 1] = "($" .. #values .. "::text)" + end + + local rows = query( + "WITH requested(target) AS (VALUES " .. table.concat(requested, ", ") .. ") " + .. "SELECT DISTINCT ON (requested.target) requested.target AS target, uri, did, cid, " + .. "indexed_at::text AS indexed_at, record::text AS record FROM requested " + .. "JOIN happyview_records ON collection = $1 AND did = $2 AND record::jsonb->>'subject' = requested.target " + .. "CROSS JOIN LATERAL (SELECT " .. follow_sort_key() .. " AS sort_at) sorted " + .. "ORDER BY requested.target, sorted.sort_at ASC, uri ASC", + values) + if type(rows) ~= "table" then error("ActorFollowQueryFailed: actor-follow lookup failed", 0) end + + local follows_by_subject = {} + for _, row in ipairs(rows) do follows_by_subject[row.target] = record_view(row) end + + local results = {} + for _, subject in ipairs(subjects) do + results[#results + 1] = { subject = subject, follow = follows_by_subject[subject] or NULL } + end + return { results = toarray(results) } +end + +function handle() + return get_follows() +end diff --git a/api/lua/shared/followBatch.lua b/api/lua/shared/followBatch.lua new file mode 100644 index 0000000..a888503 --- /dev/null +++ b/api/lua/shared/followBatch.lua @@ -0,0 +1,65 @@ +local FOLLOW_BATCH_MAX_TARGETS = 100 +local FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES = 7000 + +local function encoded_query_value_length(value) + local length = 0 + for index = 1, #value do + local byte = value:byte(index) + if (byte >= 48 and byte <= 57) or (byte >= 65 and byte <= 90) or (byte >= 97 and byte <= 122) + or byte == 42 or byte == 45 or byte == 46 or byte == 95 then + length = length + 1 + else + length = length + 3 + end + end + return length +end + +local function follow_batch_targets(actor, key, value, validate, description, endpoint) + if value == nil then invalid(key .. " is required") end + + local targets = {} + if type(value) == "string" then + targets[1] = value + elseif type(value) == "table" then + local count, max_index = 0, 0 + for index in pairs(value) do + if type(index) ~= "number" or index < 1 or index % 1 ~= 0 then + invalid(key .. " must be a repeated string parameter") + end + count = count + 1 + if index > max_index then max_index = index end + end + if count ~= max_index then invalid(key .. " must be a contiguous array") end + if count < 1 or count > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + for index = 1, max_index do + targets[index] = value[index] + end + else + invalid(key .. " must be a repeated string parameter") + end + + if #targets < 1 or #targets > FOLLOW_BATCH_MAX_TARGETS then + invalid(key .. " must contain 1 through " .. FOLLOW_BATCH_MAX_TARGETS .. " values") + end + + local request_target_length = #("/xrpc/" .. endpoint .. "?actor=") + encoded_query_value_length(actor) + local unique, seen = {}, {} + for _, target in ipairs(targets) do + if type(target) ~= "string" or not validate(target) then + invalid("each " .. key .. " value must be " .. description) + end + request_target_length = request_target_length + #("&" .. key .. "=") + encoded_query_value_length(target) + if not seen[target] then + seen[target] = true + unique[#unique + 1] = target + end + end + + if request_target_length > FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES then + invalid(key .. " makes the encoded GET request-target exceed " .. FOLLOW_BATCH_MAX_REQUEST_TARGET_BYTES .. " bytes") + end + return targets, unique +end diff --git a/api/lua/src/getEntityFollows.lua b/api/lua/src/getEntityFollows.lua new file mode 100644 index 0000000..148059d --- /dev/null +++ b/api/lua/src/getEntityFollows.lua @@ -0,0 +1,59 @@ +local function get_entity_follows() + keys_only(params, { actor = true, entities = true }) + local actor = scalar(params, "actor") + if not actor or not valid_did(actor) then invalid("actor must be a valid DID") end + local function valid_nsid(value) + if type(value) ~= "string" or #value > 317 then return false end + local segments = {} + for segment in value:gmatch("[^%.]+") do segments[#segments + 1] = segment end + if #segments < 3 or table.concat(segments, ".") ~= value then return false end + + for _, segment in ipairs(segments) do + if #segment > 63 or segment:find("[^A-Za-z0-9%-]") + or segment:sub(1, 1) == "-" or segment:sub(-1) == "-" then + return false + end + end + + if not segments[1]:sub(1, 1):match("^[A-Za-z]$") then return false end + return segments[#segments]:match("^[A-Za-z][A-Za-z0-9]*$") ~= nil + end + + local function valid_entity_uri(value) + if #value > 8192 then return false end + local valid, collection = valid_record_uri(value) + return valid and valid_nsid(collection) + end + local entities, unique_entities = follow_batch_targets( + actor, "entities", params.entities, valid_entity_uri, + "full AT-URI with a DID authority and valid collection NSID", "app.certified.graph.getEntityFollows") + + local values, requested = { ENTITY_FOLLOW, actor }, {} + for _, entity_uri in ipairs(unique_entities) do + values[#values + 1] = entity_uri + requested[#requested + 1] = "($" .. #values .. "::text)" + end + + local rows = entity_follow_query( + "WITH requested(target) AS (VALUES " .. table.concat(requested, ", ") .. ") " + .. "SELECT DISTINCT ON (requested.target) requested.target AS target, uri, did, cid, " + .. "indexed_at::text AS indexed_at, record::text AS record FROM requested " + .. "JOIN happyview_records ON collection = $1 AND did = $2 " + .. "AND record::jsonb->'subject'->>'uri' = requested.target " + .. "CROSS JOIN LATERAL (SELECT " .. entity_follow_sort_key() .. " AS sort_at) sorted " + .. "ORDER BY requested.target, sorted.sort_at ASC, uri ASC", + values) + + local follows_by_entity = {} + for _, row in ipairs(rows) do follows_by_entity[row.target] = entity_follow_record_view(row) end + + local results = {} + for _, entity_uri in ipairs(entities) do + results[#results + 1] = { entity = entity_uri, follow = follows_by_entity[entity_uri] or ENTITY_FOLLOW_NULL } + end + return { results = toarray(results) } +end + +function handle() + return get_entity_follows() +end diff --git a/api/lua/src/getFollows.lua b/api/lua/src/getFollows.lua new file mode 100644 index 0000000..2ec5958 --- /dev/null +++ b/api/lua/src/getFollows.lua @@ -0,0 +1,36 @@ +local function get_follows() + keys_only(params, { actor = true, subjects = true }) + local actor = scalar(params, "actor") + if not actor or not valid_did(actor) then invalid("actor must be a valid DID") end + local subjects, unique_subjects = follow_batch_targets( + actor, "subjects", params.subjects, valid_did, "valid DIDs", "app.certified.graph.getFollows") + + local values, requested = { FOLLOW, actor }, {} + for _, subject in ipairs(unique_subjects) do + values[#values + 1] = subject + requested[#requested + 1] = "($" .. #values .. "::text)" + end + + local rows = query( + "WITH requested(target) AS (VALUES " .. table.concat(requested, ", ") .. ") " + .. "SELECT DISTINCT ON (requested.target) requested.target AS target, uri, did, cid, " + .. "indexed_at::text AS indexed_at, record::text AS record FROM requested " + .. "JOIN happyview_records ON collection = $1 AND did = $2 AND record::jsonb->>'subject' = requested.target " + .. "CROSS JOIN LATERAL (SELECT " .. follow_sort_key() .. " AS sort_at) sorted " + .. "ORDER BY requested.target, sorted.sort_at ASC, uri ASC", + values) + if type(rows) ~= "table" then error("ActorFollowQueryFailed: actor-follow lookup failed", 0) end + + local follows_by_subject = {} + for _, row in ipairs(rows) do follows_by_subject[row.target] = record_view(row) end + + local results = {} + for _, subject in ipairs(subjects) do + results[#results + 1] = { subject = subject, follow = follows_by_subject[subject] or NULL } + end + return { results = toarray(results) } +end + +function handle() + return get_follows() +end diff --git a/api/manifest.json b/api/manifest.json index 7dbe9b9..a4addc9 100644 --- a/api/manifest.json +++ b/api/manifest.json @@ -22,6 +22,10 @@ "id": "app.certified.graph.getEntityFollow", "path": "lexicons/app.certified.graph.getEntityFollow.json" }, + { + "id": "app.certified.graph.getEntityFollows", + "path": "lexicons/app.certified.graph.getEntityFollows.json" + }, { "id": "app.certified.graph.listEntityFollowers", "path": "lexicons/app.certified.graph.listEntityFollowers.json" @@ -34,6 +38,10 @@ "id": "app.certified.graph.getFollow", "path": "lexicons/app.certified.graph.getFollow.json" }, + { + "id": "app.certified.graph.getFollows", + "path": "lexicons/app.certified.graph.getFollows.json" + }, { "id": "app.certified.graph.listActorFollowers", "path": "lexicons/app.certified.graph.listActorFollowers.json" diff --git a/api/modules/actor-follow/manifest.json b/api/modules/actor-follow/manifest.json index 068d8fd..105a91c 100644 --- a/api/modules/actor-follow/manifest.json +++ b/api/modules/actor-follow/manifest.json @@ -13,6 +13,18 @@ "org.hypercerts.api.defs" ] }, + { + "kind": "lexicon", + "id": "app.certified.graph.getFollows", + "path": "../../lexicons/app.certified.graph.getFollows.json", + "config": { + "backfill": false, + "target_collection": "app.certified.graph.follow" + }, + "dependsOn": [ + "app.certified.graph.getFollow" + ] + }, { "kind": "lexicon", "id": "app.certified.graph.listActorFollowers", @@ -57,6 +69,26 @@ "app.certified.graph.getFollow" ] }, + { + "kind": "script", + "id": "xrpc.query:app.certified.graph.getFollows", + "path": "../../lua/endpoints/getFollows.lua", + "sourcePath": "../../lua/src/getFollows.lua", + "sharedSourcePaths": [ + "../../lua/shared/didValidation.lua", + "../../lua/shared/query.lua", + "../../lua/shared/recordView.lua", + "../../lua/shared/actorFollow.lua", + "../../lua/shared/followBatch.lua" + ], + "config": { + "script_type": "lua", + "description": "Actor follow API handler" + }, + "dependsOn": [ + "app.certified.graph.getFollows" + ] + }, { "kind": "script", "id": "xrpc.query:app.certified.graph.listActorFollowers", diff --git a/api/modules/entity-follow/manifest.json b/api/modules/entity-follow/manifest.json index 0df42f6..be601bd 100644 --- a/api/modules/entity-follow/manifest.json +++ b/api/modules/entity-follow/manifest.json @@ -12,6 +12,18 @@ "app.certified.graph.entityFollow" ] }, + { + "kind": "lexicon", + "id": "app.certified.graph.getEntityFollows", + "path": "../../lexicons/app.certified.graph.getEntityFollows.json", + "config": { + "backfill": false, + "target_collection": "app.certified.graph.entityFollow" + }, + "dependsOn": [ + "app.certified.graph.getEntityFollow" + ] + }, { "kind": "lexicon", "id": "app.certified.graph.listEntityFollowers", @@ -60,6 +72,26 @@ "app.certified.graph.getEntityFollow" ] }, + { + "kind": "script", + "id": "xrpc.query:app.certified.graph.getEntityFollows", + "path": "../../lua/endpoints/getEntityFollows.lua", + "sourcePath": "../../lua/src/getEntityFollows.lua", + "sharedSourcePaths": [ + "../../lua/shared/didValidation.lua", + "../../lua/shared/query.lua", + "../../lua/shared/recordIdentifier.lua", + "../../lua/shared/entityFollow.lua", + "../../lua/shared/followBatch.lua" + ], + "config": { + "script_type": "lua", + "description": "Entity follow API handler" + }, + "dependsOn": [ + "app.certified.graph.getEntityFollows" + ] + }, { "kind": "script", "id": "xrpc.query:app.certified.graph.listEntityFollowers", diff --git a/api/tests/http/actor-follow.http.test.js b/api/tests/http/actor-follow.http.test.js index a404fd2..34417f1 100644 --- a/api/tests/http/actor-follow.http.test.js +++ b/api/tests/http/actor-follow.http.test.js @@ -217,6 +217,10 @@ test('actor list input failures use the pinned HappyView runtime error response' nsid: 'app.certified.graph.listActorFollowing', params: { actor: graphDids.publisher, sortDirection: 'sideways' }, }, + { + nsid: 'app.certified.graph.getFollows', + params: { actor: graphDids.publisher, subjects: [graphDids.primarySubject, 'not-a-did'] }, + }, ]; for (const { nsid, params } of cases) { diff --git a/api/tests/http/entity-follow.http.test.js b/api/tests/http/entity-follow.http.test.js index e82493d..3a233e3 100644 --- a/api/tests/http/entity-follow.http.test.js +++ b/api/tests/http/entity-follow.http.test.js @@ -67,6 +67,28 @@ test('getEntityFollow returns explicit null for an unindexed actor-entity pair', assert.deepEqual(body, { follow: null }); }); +test('getEntityFollows preserves URI order and duplicates with explicit targets and raw nullable representatives', async () => { + const missingEntity = `at://${graphDids.entityAuthor}/org.hypercerts.entity.feature/not-indexed`; + const entities = [graphUris.followedFeature, graphUris.unresolvedFeature, missingEntity, graphUris.followedFeature]; + const { response, body } = await request('app.certified.graph.getEntityFollows', { + actor: graphDids.publisher, + entities, + }); + + assert.equal(response.status, 200, JSON.stringify(body)); + assert.deepEqual(body.results.map(({ entity }) => entity), entities); + assert.deepEqual(body.results.map(({ follow }) => follow?.uri ?? null), [ + `at://${graphDids.publisher}/${entityFollowCollection}/3jzfcijpj2z2a`, + `at://${graphDids.publisher}/${entityFollowCollection}/3jzfcijpj2z2c`, + null, + `at://${graphDids.publisher}/${entityFollowCollection}/3jzfcijpj2z2a`, + ]); + assert.equal(body.results[0].follow.$type, 'app.certified.graph.getEntityFollow#entityFollowRecordView'); + assert.equal(body.results[0].follow.did, graphDids.publisher); + assert.equal(body.results[0].follow.record.subject.uri, graphUris.followedFeature); + assert.equal(Object.hasOwn(body.results[0].follow, 'profile'), false); +}); + test('listEntityFollowers filters by target, deduplicates tied records, paginates, and returns nullable sidecars', async () => { const items = await collectPages( 'app.certified.graph.listEntityFollowers', @@ -140,6 +162,10 @@ test('entity-follow input failures use the pinned HappyView runtime error respon nsid: 'app.certified.graph.getEntityFollow', params: { actor: graphDids.publisher, entity: 'not-an-at-uri' }, }, + { + nsid: 'app.certified.graph.getEntityFollows', + params: { actor: graphDids.publisher, entities: [graphUris.followedFeature, 'not-an-at-uri'] }, + }, { nsid: 'app.certified.graph.listEntityFollowers', params: { entity: graphUris.followedFeature, limit: 101 }, diff --git a/api/tests/http/graph-getFollow.http.test.js b/api/tests/http/graph-getFollow.http.test.js index b8c0719..b2f4ccd 100644 --- a/api/tests/http/graph-getFollow.http.test.js +++ b/api/tests/http/graph-getFollow.http.test.js @@ -1,7 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { contractUrl, requireContractTarget } from './helpers.js'; -import { graphDids } from './fixtures/graph-follows.fixture.js'; +import { graphDids, seedRows } from './fixtures/graph-follows.fixture.js'; const endpoint = 'app.certified.graph.getFollow'; const baseUrl = requireContractTarget(); @@ -50,3 +50,33 @@ test('getFollow returns explicit null when the actor-subject pair is not indexed assert.equal(response.status, 200, JSON.stringify(body)); assert.deepEqual(body, { follow: null }); }); + +test('getFollows preserves subject order and duplicates with explicit targets and raw nullable representatives', async () => { + const subjects = [graphDids.primarySubject, graphDids.thirdFollower, graphDids.secondSubject, graphDids.primarySubject]; + const response = await fetch(contractUrl(baseUrl, 'app.certified.graph.getFollows', { + actor: graphDids.publisher, + subjects, + }), { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(10_000), + }); + const body = await response.json(); + + assert.equal(response.status, 200, JSON.stringify(body)); + assert.deepEqual(body.results.map(({ subject }) => subject), subjects); + assert.deepEqual(body.results.map(({ follow }) => follow?.uri ?? null), [ + `at://${graphDids.publisher}/app.certified.graph.follow/3jzfcijpj2z2a`, + null, + `at://${graphDids.publisher}/app.certified.graph.follow/3jzfcijpj2z2c`, + `at://${graphDids.publisher}/app.certified.graph.follow/3jzfcijpj2z2a`, + ]); + const earliest = seedRows.find(({ uri }) => uri === `at://${graphDids.publisher}/app.certified.graph.follow/3jzfcijpj2z2a`); + assert.deepEqual(body.results[0].follow, { + uri: earliest.uri, + cid: earliest.cid, + indexedAt: earliest.indexedAt, + did: earliest.did, + record: earliest.record, + }); + assert.equal(Object.hasOwn(body.results[0].follow, 'profile'), false); +}); diff --git a/api/tests/unit/tooling/actor-follow.test.js b/api/tests/unit/tooling/actor-follow.test.js index ec9af35..db88de1 100644 --- a/api/tests/unit/tooling/actor-follow.test.js +++ b/api/tests/unit/tooling/actor-follow.test.js @@ -140,9 +140,11 @@ test('actor-follow module installs the follow record schema, three queries, and const expected = [ followCollection, 'app.certified.graph.getFollow', + 'app.certified.graph.getFollows', 'app.certified.graph.listActorFollowers', 'app.certified.graph.listActorFollowing', 'xrpc.query:app.certified.graph.getFollow', + 'xrpc.query:app.certified.graph.getFollows', 'xrpc.query:app.certified.graph.listActorFollowers', 'xrpc.query:app.certified.graph.listActorFollowing', ]; @@ -153,6 +155,7 @@ test('actor-follow module installs the follow record schema, three queries, and [ ...expected.filter((id) => id.startsWith('xrpc.query:app.certified.graph.')), 'xrpc.query:app.certified.graph.getEntityFollow', + 'xrpc.query:app.certified.graph.getEntityFollows', 'xrpc.query:app.certified.graph.listEntityFollowers', 'xrpc.query:app.certified.graph.listEntityFollowing', 'xrpc.query:app.certified.graph.listRecentFollows', @@ -165,11 +168,18 @@ test('actor-follow query Lexicons validate the endpoint inputs, nullable lookup, const byId = new Map(documents.map((document) => [document.id, document])); const follow = byId.get(followCollection); const getFollow = byId.get('app.certified.graph.getFollow'); + const getFollows = byId.get('app.certified.graph.getFollows'); const followers = byId.get('app.certified.graph.listActorFollowers'); const following = byId.get('app.certified.graph.listActorFollowing'); - assert.ok(follow && getFollow && followers && following, 'the follow record and all query Lexicons belong to the validation closure'); + assert.ok(follow && getFollow && getFollows && followers && following, 'the follow record and all query Lexicons belong to the validation closure'); assert.equal(lexicons.getDefOrThrow(followCollection).type, 'record'); assert.deepEqual(Object.keys(getFollow.defs.main.parameters.properties).sort(), ['actor', 'subject']); + assert.deepEqual(Object.keys(getFollows.defs.main.parameters.properties).sort(), ['actor', 'subjects']); + assert.equal(getFollows.defs.main.parameters.properties.subjects.minLength, 1); + assert.equal(getFollows.defs.main.parameters.properties.subjects.maxLength, 100); + assert.deepEqual(getFollows.defs.output.required, ['results']); + assert.equal(getFollows.defs.followResult.properties.follow.ref, 'lex:app.certified.graph.getFollow#followRecordView'); + assert.deepEqual(getFollows.defs.followResult.nullable, ['follow']); assert.deepEqual(getFollow.defs.output.required, ['follow']); assert.deepEqual(getFollow.defs.output.nullable, ['follow']); assert.deepEqual(Object.keys(followers.defs.main.parameters.properties).sort(), ['actor', 'cursor', 'limit', 'sortDirection']); @@ -423,6 +433,54 @@ test('actor-follow queries reject invalid DIDs, limits, scalar repetition, unkno runLua({ endpoint: 'getFollow', params: { actor, subject: 'alice.example' }, expectError: 'InvalidRequest:' }); }); +test('getFollows preserves requested positions and nulls while selecting one raw representative per subject in one query', () => { + const tiedEarliest = { + ...followRow('3jzfcijpj2z2a', actor, actor, '2025-01-01T00:00:00Z', { subjectDid: subject }), + target: subject, + }; + const otherSubject = 'did:plc:dddddddddddddddddddddddd'; + const otherFollow = { + ...followRow('other-subject', actor, actor, '2025-01-02T00:00:00Z', { subjectDid: otherSubject }), + target: otherSubject, + }; + const output = runLua({ + endpoint: 'getFollows', params: { actor, subjects: [subject, otherSubject, follower, subject] }, + rows: [tiedEarliest, otherFollow], + assertions: ` +assert(#result.results == 4) +assert(result.results[1].subject == '${subject}' and result.results[1].follow.uri == '${tiedEarliest.uri}') +assert(result.results[2].subject == '${otherSubject}' and result.results[2].follow.uri == '${otherFollow.uri}') +assert(result.results[3].subject == '${follower}' and result.results[3].follow == NULL_VALUE) +assert(result.results[4].subject == '${subject}' and result.results[4].follow.uri == '${tiedEarliest.uri}') +assert(result.results[1].follow.did == '${actor}' and result.results[1].follow.record.subject == '${subject}') +assert(result.results[1].follow.profile == nil, 'batch checks return the raw existing follow view without enrichment') +assert(#calls == 1, 'all distinct targets must be looked up in one database query') +assert(calls[1].values[1] == '${followCollection}' and calls[1].values[2] == '${actor}') +assert(#calls[1].values == 5 and calls[1].values[3] == '${subject}' and calls[1].values[4] == '${otherSubject}' and calls[1].values[5] == '${follower}', 'duplicates are removed only from bound lookup targets') +assert(calls[1].sql:find('WITH requested(target) AS (VALUES ($3::text), ($4::text), ($5::text))', 1, true)) +assert(calls[1].sql:find('DISTINCT ON (requested.target)', 1, true)) +assert(calls[1].sql:find('ORDER BY requested.target, sorted.sort_at ASC, uri ASC', 1, true)) +`, + }); + assert.equal(output, ''); +}); + +test('getFollows rejects invalid or oversized target arrays before querying and surfaces database failures', () => { + const oversizedUrlTargets = Array.from({ length: 4 }, (_, index) => `did:plc:${'a'.repeat(2039)}${index}`); + for (const params of [ + { actor }, + { actor, subjects: [] }, + { actor, subjects: [subject, 'alice.example'] }, + { actor, subjects: Array(101).fill(subject) }, + { actor, subjects: oversizedUrlTargets }, + { actor, subjects: [subject], unexpected: 'value' }, + { actor: 'alice.example', subjects: [subject] }, + ]) runLua({ endpoint: 'getFollows', params, expectError: 'InvalidRequest:', expectedCalls: 0 }); + + runLua({ endpoint: 'getFollows', params: { actor, subjects: [subject] }, backend: 'sqlite', expectError: 'ActorFollowQueryFailed:', expectedCalls: 0 }); + runLua({ endpoint: 'getFollows', params: { actor, subjects: [subject] }, queryFailure: true, expectError: 'ActorFollowQueryFailed:', expectedCalls: 1 }); +}); + test('actor-follow queries surface the beta PostgreSQL-only and operational failure errors', () => { runLua({ endpoint: 'listActorFollowers', params: { actor }, backend: 'sqlite', expectError: 'ActorFollowQueryFailed:', expectedCalls: 0 }); runLua({ endpoint: 'listActorFollowers', params: { actor }, queryFailure: true, expectError: 'ActorFollowQueryFailed:', expectedCalls: 1 }); diff --git a/api/tests/unit/tooling/entity-follow.test.js b/api/tests/unit/tooling/entity-follow.test.js index 218e98f..6904ada 100644 --- a/api/tests/unit/tooling/entity-follow.test.js +++ b/api/tests/unit/tooling/entity-follow.test.js @@ -650,9 +650,11 @@ test('entity-follow module owns its view Lexicons and declares their shared depe const moduleIds = moduleManifest.assets.map(({ id }) => id).sort(); assert.deepEqual(moduleIds, [ 'app.certified.graph.getEntityFollow', + 'app.certified.graph.getEntityFollows', 'app.certified.graph.listEntityFollowers', 'app.certified.graph.listEntityFollowing', 'xrpc.query:app.certified.graph.getEntityFollow', + 'xrpc.query:app.certified.graph.getEntityFollows', 'xrpc.query:app.certified.graph.listEntityFollowers', 'xrpc.query:app.certified.graph.listEntityFollowing', ].sort()); @@ -687,6 +689,7 @@ test('entity-follow module owns its view Lexicons and declares their shared depe 'app.certified.defs', 'app.certified.graph.entityFollow', 'app.certified.graph.getEntityFollow', + 'app.certified.graph.getEntityFollows', 'app.certified.graph.listEntityFollowers', 'app.certified.graph.listEntityFollowing', 'app.certified.signature.defs', @@ -713,6 +716,7 @@ test('entity-follow module owns its view Lexicons and declares their shared depe assert.deepEqual(apiDefs.defs.organizationView.nullable, ['indexedAt']); for (const id of [ 'app.certified.graph.getEntityFollow', + 'app.certified.graph.getEntityFollows', 'app.certified.graph.listEntityFollowers', 'app.certified.graph.listEntityFollowing', ]) { @@ -722,6 +726,7 @@ test('entity-follow module owns its view Lexicons and declares their shared depe assert.equal(asset.config.backfill, false); } const get = localLexicons.get('app.certified.graph.getEntityFollow'); + const getBatch = localLexicons.get('app.certified.graph.getEntityFollows'); const followers = localLexicons.get('app.certified.graph.listEntityFollowers'); const followingDoc = localLexicons.get('app.certified.graph.listEntityFollowing'); assert.deepEqual(get.defs.entityFollowRecordView.nullable, ['indexedAt']); @@ -735,6 +740,12 @@ test('entity-follow module owns its view Lexicons and declares their shared depe ]); assert.deepEqual(followingItem.properties.follow.ref, 'app.certified.graph.getEntityFollow#entityFollowRecordView'); assert.deepEqual(Object.keys(get.defs.main.parameters.properties).sort(), ['actor', 'entity']); + assert.deepEqual(Object.keys(getBatch.defs.main.parameters.properties).sort(), ['actor', 'entities']); + assert.equal(getBatch.defs.main.parameters.properties.entities.minLength, 1); + assert.equal(getBatch.defs.main.parameters.properties.entities.maxLength, 100); + assert.deepEqual(getBatch.defs.output.required, ['results']); + assert.equal(getBatch.defs.entityFollowResult.properties.follow.ref, 'app.certified.graph.getEntityFollow#entityFollowRecordView'); + assert.deepEqual(getBatch.defs.entityFollowResult.nullable, ['follow']); assert.deepEqual(get.defs.output.required, ['follow']); assert.deepEqual(get.defs.output.nullable, ['follow']); for (const [document, outputKey] of [[followers, 'followers'], [followingDoc, 'entities']]) { @@ -764,3 +775,140 @@ assert(calls == 0, 'unknown parameters must be rejected before database access') `; runLua('listEntityFollowing', source); }); + +test('getEntityFollows preserves request order and duplicates, matches target URIs, and returns raw nullable views', () => { + const missingEntity = `at://${author}/org.example.unsupported/missing`; + const rows = [ + { + target: entityUri, + uri: `at://${actor}/${entityFollow}/3jzfcijpj2z2a`, + did: actor, + cid: 'bafy-earliest-entity-follow', + indexed_at: '2025-01-03T00:00:00Z', + record: 'entity-follow-earliest', + follow_record: { $type: entityFollow, subject: { $type: 'app.certified.defs#recordSubject', uri: entityUri }, createdAt: '2025-01-01T00:00:00Z' }, + }, + { + target: nextEntityUri, + uri: `at://${actor}/${entityFollow}/other-target`, + did: actor, + cid: 'bafy-other-entity-follow', + indexed_at: null, + record: 'entity-follow-other', + follow_record: { $type: entityFollow, subject: { $type: 'app.certified.defs#recordSubject', uri: nextEntityUri }, createdAt: '2025-01-02T00:00:00Z' }, + }, + ]; + const records = Object.fromEntries(rows.map(({ record, follow_record }) => [record, follow_record])); + const source = ` +local FIXTURE = ${lua({ actor, entityUri, nextEntityUri, missingEntity, entityFollow, rows, records })} +local NULL_VALUE, calls = {}, {} +json = { + decode = function(value) + if value == 'null' then return NULL_VALUE end + if FIXTURE.records[value] then return FIXTURE.records[value] end + error('unexpected JSON decode input ' .. tostring(value)) + end, +} +toarray = function(values) return values end +params = { actor = FIXTURE.actor, entities = { FIXTURE.entityUri, FIXTURE.nextEntityUri, FIXTURE.missingEntity, FIXTURE.entityUri } } +db = { + backend = function() return 'postgres' end, + raw = function(sql, values) + calls[#calls + 1] = { sql = sql, values = values } + return FIXTURE.rows + end, +} +dofile('lua/endpoints/getEntityFollows.lua') +local ok, result = pcall(handle) +assert(ok, tostring(result)) +assert(#result.results == 4) +assert(result.results[1].entity == FIXTURE.entityUri and result.results[1].follow.uri == FIXTURE.rows[1].uri) +assert(result.results[2].entity == FIXTURE.nextEntityUri and result.results[2].follow.uri == FIXTURE.rows[2].uri) +assert(result.results[3].entity == FIXTURE.missingEntity and result.results[3].follow == NULL_VALUE) +assert(result.results[4].entity == FIXTURE.entityUri and result.results[4].follow.uri == FIXTURE.rows[1].uri) +assert(result.results[1].follow['$type'] == 'app.certified.graph.getEntityFollow#entityFollowRecordView') +assert(result.results[2].follow.indexedAt == NULL_VALUE) +assert(result.results[1].follow.record.subject.uri == FIXTURE.entityUri and result.results[1].follow.profile == nil) +assert(#calls == 1, 'all targets must be resolved with one database query and no hydration') +assert(calls[1].values[1] == FIXTURE.entityFollow and calls[1].values[2] == FIXTURE.actor) +assert(#calls[1].values == 5 and calls[1].values[3] == FIXTURE.entityUri and calls[1].values[4] == FIXTURE.nextEntityUri and calls[1].values[5] == FIXTURE.missingEntity) +assert(calls[1].sql:find('WITH requested(target) AS (VALUES ($3::text), ($4::text), ($5::text))', 1, true)) +assert(calls[1].sql:find("record::jsonb->'subject'->>'uri'", 1, true)) +assert(calls[1].sql:find('DISTINCT ON (requested.target)', 1, true)) +assert(calls[1].sql:find('ORDER BY requested.target, sorted.sort_at ASC, uri ASC', 1, true)) +assert(not calls[1].sql:find("record::jsonb->'subject'->>'cid'", 1, true), 'entity relationships match by URI independently of CID') +`; + runLua('getEntityFollows', source); +}); + +test('getEntityFollows rejects malformed collection NSIDs but accepts valid arbitrary collections', () => { + const malformedEntityUri = `at://${author}/bad%20collection/key`; + const arbitraryCollectionUri = `at://${author}/com.example.arbitrary/key`; + const source = ` +local NULL_VALUE, calls = {}, 0 +json = { decode = function(value) if value == 'null' then return NULL_VALUE end return {} end } +toarray = function(values) return values end +params = { actor = '${actor}', entities = { '${malformedEntityUri}' } } +db = { + backend = function() return 'postgres' end, + raw = function() calls = calls + 1; return {} end, +} +dofile('lua/endpoints/getEntityFollows.lua') +local ok, result = pcall(handle) +assert(not ok and tostring(result):find('InvalidRequest: each entities value must be full AT-URI with a DID authority and valid collection NSID', 1, true), tostring(result)) +assert(calls == 0, 'malformed collection NSIDs must be rejected before querying') + +params = { actor = '${actor}', entities = { '${arbitraryCollectionUri}' } } +ok, result = pcall(handle) +assert(ok, tostring(result)) +assert(#result.results == 1 and result.results[1].entity == '${arbitraryCollectionUri}') +assert(result.results[1].follow == NULL_VALUE, 'valid arbitrary collection NSIDs remain accepted when no follow exists') +assert(calls == 1, 'valid arbitrary collections reach the database lookup') +`; + runLua('getEntityFollows', source); +}); + +test('getEntityFollows rejects invalid or oversized target arrays before querying and surfaces database failures', () => { + const oversizedUris = Array.from({ length: 3 }, (_, index) => `at://${author}/${'x'.repeat(2500)}/target-${index}`); + const invalidSource = ` +local calls = 0 +json = { decode = function(value) if value == 'null' then return {} end return {} end } +toarray = function(values) return values end +params = REQUEST_PARAMS +db = { + backend = function() return 'postgres' end, + raw = function() calls = calls + 1; return {} end, +} +dofile('lua/endpoints/getEntityFollows.lua') +local ok, result = pcall(handle) +assert(not ok and tostring(result):find('InvalidRequest:', 1, true), tostring(result)) +assert(calls == 0, 'invalid batch requests must fail before querying') +`; + for (const params of [ + { actor, entities: [] }, + { actor }, + { actor, entities: [entityUri, 'not-an-at-uri'] }, + { actor, entities: [`at://${author}/org.example.collection/.`] }, + { actor, entities: Array(101).fill(entityUri) }, + { actor, entities: oversizedUris }, + { actor, entities: [entityUri], unexpected: 'value' }, + { actor: 'alice.example', entities: [entityUri] }, + ]) { + const source = invalidSource.replace('REQUEST_PARAMS', lua(params)); + runLua('getEntityFollows', source); + } + + const failureSource = ` +json = { decode = function(value) if value == 'null' then return {} end return {} end } +toarray = function(values) return values end +params = { actor = '${actor}', entities = { '${entityUri}' } } +db = { + backend = function() return 'postgres' end, + raw = function() error('fixture database failure') end, +} +dofile('lua/endpoints/getEntityFollows.lua') +local ok, result = pcall(handle) +assert(not ok and tostring(result):find('EntityFollowQueryFailed:', 1, true), tostring(result)) +`; + runLua('getEntityFollows', failureSource); +}); diff --git a/docs/coverage.json b/docs/coverage.json index 596a9d2..8813af5 100644 --- a/docs/coverage.json +++ b/docs/coverage.json @@ -5,12 +5,12 @@ }, "runtimeValidation": "not-assessed", "deploymentValidation": "not-assessed", - "endpointCount": 59, + "endpointCount": 61, "methods": { - "GET": 59 + "GET": 61 }, "inclusion": { - "manifest-registered": 59 + "manifest-registered": 61 }, "unresolvedReferences": [], "endpoints": [ @@ -134,12 +134,24 @@ "inclusion": "manifest-registered", "module": "modules/entity-follow/manifest.json" }, + { + "id": "app.certified.graph.getEntityFollows", + "method": "GET", + "inclusion": "manifest-registered", + "module": "modules/entity-follow/manifest.json" + }, { "id": "app.certified.graph.getFollow", "method": "GET", "inclusion": "manifest-registered", "module": "modules/actor-follow/manifest.json" }, + { + "id": "app.certified.graph.getFollows", + "method": "GET", + "inclusion": "manifest-registered", + "module": "modules/actor-follow/manifest.json" + }, { "id": "app.certified.graph.listActorFollowers", "method": "GET", diff --git a/docs/openapi.json b/docs/openapi.json index d1247de..8b31468 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1643,6 +1643,73 @@ ] } }, + "/xrpc/app.certified.graph.getEntityFollows": { + "get": { + "operationId": "app_certified_graph_getEntityFollows", + "tags": [ + "app.certified.graph" + ], + "description": "Returns the representative raw entity-follow record, or null, for each requested actor-to-entity relationship. Authentication is not required. Supply 1–100 repeated entities values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": [ + { + "name": "actor", + "in": "query", + "required": true, + "description": "DID that published the entity-follow records.", + "schema": { + "type": "string", + "format": "did", + "maxLength": 2048 + } + }, + { + "name": "entities", + "in": "query", + "required": true, + "description": "Full entity AT-URIs using DID authorities, supplied as repeated unbracketed query values. Matching uses the subject URI and ignores CID; every occurrence is returned in request order, including duplicates.", + "schema": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string", + "format": "at-uri", + "maxLength": 8192 + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/app.certified.graph.getEntityFollows.output" + } + } + } + } + }, + "x-lexicon-id": "app.certified.graph.getEntityFollows", + "x-hypercerts-coverage": "manifest-registered", + "x-hypercerts-source": { + "module": "modules/entity-follow/manifest.json" + }, + "x-lexicon-errors": [ + { + "name": "InvalidRequest", + "description": "The actor DID, entities array, an entity AT-URI, request-target length, or another query parameter is invalid." + }, + { + "name": "EntityFollowQueryFailed", + "description": "The indexed entity-follow relationships could not be queried." + } + ] + } + }, "/xrpc/app.certified.graph.getFollow": { "get": { "operationId": "app_certified_graph_getFollow", @@ -1697,6 +1764,73 @@ ] } }, + "/xrpc/app.certified.graph.getFollows": { + "get": { + "operationId": "app_certified_graph_getFollows", + "tags": [ + "app.certified.graph" + ], + "description": "Returns the representative raw follow record, or null, for each requested actor-to-actor relationship. Authentication is not required. Supply 1–100 repeated subjects values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": [ + { + "name": "actor", + "in": "query", + "required": true, + "description": "DID of the actor who published the follow records.", + "schema": { + "type": "string", + "format": "did", + "maxLength": 2048 + } + }, + { + "name": "subjects", + "in": "query", + "required": true, + "description": "Subject DIDs to check, supplied as repeated unbracketed query values. Every occurrence is returned in the same order, including duplicates.", + "schema": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string", + "format": "did", + "maxLength": 2048 + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/app.certified.graph.getFollows.output" + } + } + } + } + }, + "x-lexicon-id": "app.certified.graph.getFollows", + "x-hypercerts-coverage": "manifest-registered", + "x-hypercerts-source": { + "module": "modules/actor-follow/manifest.json" + }, + "x-lexicon-errors": [ + { + "name": "InvalidRequest", + "description": "The actor DID, subjects array, request-target length, or another query parameter is invalid." + }, + { + "name": "ActorFollowQueryFailed", + "description": "The indexed follow relationships could not be queried." + } + ] + } + }, "/xrpc/app.certified.graph.listActorFollowers": { "get": { "operationId": "app_certified_graph_listActorFollowers", @@ -5923,6 +6057,45 @@ } } }, + "app.certified.graph.getEntityFollows.entityFollowResult": { + "type": "object", + "required": [ + "entity", + "follow" + ], + "properties": { + "entity": { + "type": "string", + "description": "Entity AT-URI from the corresponding request occurrence.", + "format": "at-uri" + }, + "follow": { + "anyOf": [ + { + "$ref": "#/components/schemas/app.certified.graph.getEntityFollow.entityFollowRecordView" + }, + { + "type": "null" + } + ] + } + } + }, + "app.certified.graph.getEntityFollows.output": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied entity occurrence, in request order.", + "items": { + "$ref": "#/components/schemas/app.certified.graph.getEntityFollows.entityFollowResult" + } + } + } + }, "app.certified.graph.follow.main": { "type": "record", "description": "Record declaring a social 'follow' relationship of another account." @@ -5985,6 +6158,45 @@ } } }, + "app.certified.graph.getFollows.followResult": { + "type": "object", + "required": [ + "subject", + "follow" + ], + "properties": { + "subject": { + "type": "string", + "description": "Subject DID from the corresponding request occurrence.", + "format": "did" + }, + "follow": { + "anyOf": [ + { + "$ref": "#/components/schemas/app.certified.graph.getFollow.followRecordView" + }, + { + "type": "null" + } + ] + } + } + }, + "app.certified.graph.getFollows.output": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied subject occurrence, in request order.", + "items": { + "$ref": "#/components/schemas/app.certified.graph.getFollows.followResult" + } + } + } + }, "app.certified.graph.getFollow.actorFollowView": { "type": "object", "description": "Displayed actor with nullable profile and organization sidecars, paired with the representative follow record.", diff --git a/docs/sources/index.json b/docs/sources/index.json index e12e27e..4a3377a 100644 --- a/docs/sources/index.json +++ b/docs/sources/index.json @@ -185,6 +185,15 @@ "path": "../../lexicons/app.certified.graph.getEntityFollow.json" } }, + { + "id": "app.certified.graph.getEntityFollows", + "type": "query", + "module": "modules/entity-follow/manifest.json", + "file": "sources/lexicons/app.certified.graph.getEntityFollows.json", + "source": { + "path": "../../lexicons/app.certified.graph.getEntityFollows.json" + } + }, { "id": "app.certified.graph.getFollow", "type": "query", @@ -194,6 +203,15 @@ "path": "../../lexicons/app.certified.graph.getFollow.json" } }, + { + "id": "app.certified.graph.getFollows", + "type": "query", + "module": "modules/actor-follow/manifest.json", + "file": "sources/lexicons/app.certified.graph.getFollows.json", + "source": { + "path": "../../lexicons/app.certified.graph.getFollows.json" + } + }, { "id": "app.certified.graph.listActorFollowers", "type": "query", @@ -806,6 +824,15 @@ "path": "../../lexicons/app.certified.graph.getEntityFollow.json" } }, + { + "id": "app.certified.graph.getEntityFollows", + "file": "sources/lexicons/app.certified.graph.getEntityFollows.json", + "kind": "endpoint", + "source": { + "module": "modules/entity-follow/manifest.json", + "path": "../../lexicons/app.certified.graph.getEntityFollows.json" + } + }, { "id": "app.certified.graph.getFollow", "file": "sources/lexicons/app.certified.graph.getFollow.json", @@ -815,6 +842,15 @@ "path": "../../lexicons/app.certified.graph.getFollow.json" } }, + { + "id": "app.certified.graph.getFollows", + "file": "sources/lexicons/app.certified.graph.getFollows.json", + "kind": "endpoint", + "source": { + "module": "modules/actor-follow/manifest.json", + "path": "../../lexicons/app.certified.graph.getFollows.json" + } + }, { "id": "app.certified.graph.listActorFollowers", "file": "sources/lexicons/app.certified.graph.listActorFollowers.json", diff --git a/docs/sources/lexicons/app.certified.graph.getEntityFollows.json b/docs/sources/lexicons/app.certified.graph.getEntityFollows.json new file mode 100644 index 0000000..02695b0 --- /dev/null +++ b/docs/sources/lexicons/app.certified.graph.getEntityFollows.json @@ -0,0 +1,91 @@ +{ + "lexicon": 1, + "id": "app.certified.graph.getEntityFollows", + "defs": { + "main": { + "type": "query", + "description": "Returns the representative raw entity-follow record, or null, for each requested actor-to-entity relationship. Authentication is not required. Supply 1–100 repeated entities values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": { + "type": "params", + "required": [ + "actor", + "entities" + ], + "properties": { + "actor": { + "type": "string", + "format": "did", + "maxLength": 2048, + "description": "DID that published the entity-follow records." + }, + "entities": { + "type": "array", + "description": "Full entity AT-URIs using DID authorities, supplied as repeated unbracketed query values. Matching uses the subject URI and ignores CID; every occurrence is returned in request order, including duplicates.", + "minLength": 1, + "maxLength": 100, + "items": { + "type": "string", + "format": "at-uri", + "maxLength": 8192 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "#output" + } + }, + "errors": [ + { + "name": "InvalidRequest", + "description": "The actor DID, entities array, an entity AT-URI, request-target length, or another query parameter is invalid." + }, + { + "name": "EntityFollowQueryFailed", + "description": "The indexed entity-follow relationships could not be queried." + } + ] + }, + "output": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied entity occurrence, in request order.", + "items": { + "type": "ref", + "ref": "#entityFollowResult" + } + } + } + }, + "entityFollowResult": { + "type": "object", + "required": [ + "entity", + "follow" + ], + "nullable": [ + "follow" + ], + "properties": { + "entity": { + "type": "string", + "format": "at-uri", + "description": "Entity AT-URI from the corresponding request occurrence." + }, + "follow": { + "type": "ref", + "ref": "app.certified.graph.getEntityFollow#entityFollowRecordView", + "description": "Earliest indexed matching record by createdAt, with URI ascending on ties, or null when no relationship is indexed. Query failures are errors, not null results." + } + } + } + } +} diff --git a/docs/sources/lexicons/app.certified.graph.getFollows.json b/docs/sources/lexicons/app.certified.graph.getFollows.json new file mode 100644 index 0000000..311cbf4 --- /dev/null +++ b/docs/sources/lexicons/app.certified.graph.getFollows.json @@ -0,0 +1,91 @@ +{ + "lexicon": 1, + "id": "app.certified.graph.getFollows", + "defs": { + "main": { + "type": "query", + "description": "Returns the representative raw follow record, or null, for each requested actor-to-actor relationship. Authentication is not required. Supply 1–100 repeated subjects values; the encoded GET request-target must not exceed 7,000 bytes.", + "parameters": { + "type": "params", + "required": [ + "actor", + "subjects" + ], + "properties": { + "actor": { + "type": "string", + "format": "did", + "maxLength": 2048, + "description": "DID of the actor who published the follow records." + }, + "subjects": { + "type": "array", + "description": "Subject DIDs to check, supplied as repeated unbracketed query values. Every occurrence is returned in the same order, including duplicates.", + "minLength": 1, + "maxLength": 100, + "items": { + "type": "string", + "format": "did", + "maxLength": 2048 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "#output" + } + }, + "errors": [ + { + "name": "InvalidRequest", + "description": "The actor DID, subjects array, request-target length, or another query parameter is invalid." + }, + { + "name": "ActorFollowQueryFailed", + "description": "The indexed follow relationships could not be queried." + } + ] + }, + "output": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "description": "One result per supplied subject occurrence, in request order.", + "items": { + "type": "ref", + "ref": "#followResult" + } + } + } + }, + "followResult": { + "type": "object", + "required": [ + "subject", + "follow" + ], + "nullable": [ + "follow" + ], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "Subject DID from the corresponding request occurrence." + }, + "follow": { + "type": "ref", + "ref": "app.certified.graph.getFollow#followRecordView", + "description": "Earliest indexed matching record by createdAt, with URI ascending on ties, or null when no relationship is indexed. Query failures are errors, not null results." + } + } + } + } +}