Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions hypercerts-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

This directory contains the tooling for building and testing a Hypercerts XRPC API on HappyView. It provides a reusable installer for HappyView admin assets (Lexicons and Lua scripts), pinned Hypercerts Lexicon dependencies, and test fixtures for checking record behavior. The installer lets API modules declare their assets together, install missing assets in dependency order, and refuse to overwrite assets that differ from what is already installed.

**Current status:** The root `manifest.json` includes the location, actor-follow, profile, and organization query slices. Build their Lua handlers and run the offline checks before installing the bundle on an approved HappyView target. Only the location and actor-follow slices have runtime validation at the target revision; profile and organization query HTTP proof remains unrun.
**Current status:** The root `manifest.json` includes the location, actor-follow, profile, organization, and activity query slices. Build their Lua handlers and run the offline checks before installing the bundle on an approved HappyView target. Only the location and actor-follow slices have runtime validation at the target revision; profile, organization, and activity query HTTP proof remains unrun.

The actor-follow slice exposes `app.certified.graph.getFollow`, `app.certified.graph.listActorFollowers`, and `app.certified.graph.listActorFollowing`, backed by the pinned `app.certified.graph.follow` record Lexicon. Both listing queries return a required `totalCount` for the complete deduplicated relationship set, independent of cursor and limit. Lookup and listings sort follows by `createdAt`; if it is missing or malformed, they use `indexed_at` (or the database insertion time when `indexed_at` is absent) so those follows remain visible and counted.

The profile slice exposes `app.certified.actor.getProfile`, `app.certified.actor.getProfiles`, `app.certified.actor.listProfiles`, and `app.certified.actor.searchProfiles`. `getProfiles` requires 1–100 supplied actor DIDs, counting duplicates before deduplication; it preserves input order and duplicates, returns an explicit `null` profile for missing actors, does not resolve handles or make network requests, and does not paginate. `listProfiles` accepts actor DIDs and pagination only; use `searchProfiles` for a required profile-text query over `displayName` and `description`. Both list and search preserve the original profile record and do not attach organization sidecars. `getProfile(actor=DID)` performs a direct indexed lookup without a resolver request. For `getProfile(actor=handle)`, set the HappyView instance script variable `HYPERCERTS_HANDLE_RESOLVER_URL` to an HTTPS resolver origin. Each Lua execution reads that setting and calls `com.atproto.identity.resolveHandle` at the configured resolver's `/xrpc/` endpoint; no per-request override or bsky.social default is used. A syntactically valid DID in that resolver response is accepted as identity confirmation: the operator trusts the selected resolver, and HappyView does not independently fetch or verify a DID document. Removing arbitrary `did:web` document fetching removes that specific SSRF path; this change does not secure resolver redirects or broader outbound egress. Use `listProfiles` when no text restriction is intended.

The organization slice exposes `app.certified.actor.getOrganization`, `app.certified.actor.listOrganizations`, and `app.certified.actor.searchOrganizations`. Each result includes the original organization sidecar with record metadata and publisher DID, plus the associated profile or explicit `null`; a missing sidecar fails singular lookup and is omitted from listings. Listing filters combine with AND, while values within `actors` and `organizationTypes` use OR. A supplied visibility is an exact match; omitting it includes public, unlisted, and unspecified values. Search matches the complete trimmed, case-insensitive literal text within the associated profile's `displayName` or `description`; an organization without a profile cannot match nonblank text. Both listing endpoints use `(createdAt, uri)` ordering and direction-bound cursors. Founding-date range parameters are not part of this API.

The activity slice exposes `org.hypercerts.claim.getActivity`, `org.hypercerts.claim.listActivities`, and `org.hypercerts.claim.searchActivities`. Singular lookup uses an exact activity AT-URI. All three responses preserve the original activity record and hydrate the publisher's Certified profile and organization sidecar. Contributor hydration is returned as a sibling projection: it preserves source order and duplicates, omits the projection only when `record.contributors` is absent, and keeps an explicit empty array empty. Contributor-information strong references resolve only by exact URI and CID; a newer version at the same URI is never substituted. DID and AT-URI contributor identifiers hydrate Certified profiles when resolvable; external or unrecognized identifiers remain visible with `actor: null`. Missing related records remain nullable, while query or required hydration failures fail the request. Activity and hydrated exact-version contributor-information views always include `indexedAt`: SQL `NULL` in `indexed_at` is returned as JSON `null`, while non-null timestamps are strings.

`listActivities` accepts `authors`, `hasOrganizationRecord`, `contributors`, `involvedActors`, and activity `uris`; different filters combine with AND and values within each array use OR. `hasOrganizationRecord=true` requires an `app.certified.actor.organization/self` record, while `false` requires its absence; neither value depends on whether the author has a profile. `searchActivities` has those filters plus required text search over literal, case-insensitive substrings in `title` or `shortDescription`. Listings default to 25 results and cap pages and each filter array at 100 values. They sort by `(createdAt, uri)` and use an opaque cursor bound to sort direction. Use repeated, unbracketed query keys for arrays.

## Get started

From `hypercerts-api/`:
Expand All @@ -20,7 +24,7 @@ pnpm build:lua
pnpm check
```

These checks run offline; they do not require a running HappyView instance or database. `build:lua` builds the standalone location, actor-follow, profile, and organization handlers in `lua/endpoints/`. `pnpm check` verifies the generated Lua bundles are current, runs JavaScript and Lua lint, a strict `checkJs` typecheck of the installer, then the offline unit tests; run `pnpm run typecheck` to run just the typecheck. TypeScript is a development-time checker only: the installer remains JavaScript and runs directly with Node, with no transpilation. Run `pnpm check:generated` to check bundle freshness on its own; it does not modify files. If it fails, run `pnpm build:lua` and review the generated changes. The bundle combines local API Lexicons and scripts with schemas from the pinned `@hypercerts-org/lexicon` package. HTTP contract tests require an installed bundle and a separately approved disposable target. Only location and actor-follow slices have runtime validation at the target revision; profile and organization query HTTP proof remains unrun.
These checks run offline; they do not require a running HappyView instance or database. `build:lua` builds the standalone location, actor-follow, profile, organization, and activity handlers in `lua/endpoints/`. `pnpm check` verifies the generated Lua bundles are current, runs JavaScript and Lua lint, a strict `checkJs` typecheck of the installer, then the offline unit tests; run `pnpm run typecheck` to run just the typecheck. TypeScript is a development-time checker only: the installer remains JavaScript and runs directly with Node, with no transpilation. Run `pnpm check:generated` to check bundle freshness on its own; it does not modify files. If it fails, run `pnpm build:lua` and review the generated changes. The bundle combines local API Lexicons and scripts with schemas from the pinned `@hypercerts-org/lexicon` package. HTTP contract tests require an installed bundle and a separately approved disposable target. Only location and actor-follow slices have runtime validation at the target revision; profile, organization, and activity query HTTP proof remains unrun.

ESLint is installed with the package dependencies. It uses ESLint's recommended checks plus strict equality, no implicit coercion, no shadowed names, no reassigned parameters, no `var`, and `const` where possible. It also rejects direct `eval`, implied evaluation (such as string-based timers), and `Function` constructors. Console output is allowed only in CLI tooling, and unused ESLint disable directives fail lint. When a branch contains Lua scripts, Luacheck checks the generated handlers in `lua/endpoints/`; it rejects unknown globals, sandbox-removed Lua APIs, and unused arguments. It rejects `io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`, `collectgarbage`, and unsafe `os` functions; only `os.time`, `os.date`, `os.difftime`, and `os.clock` are allowed from `os`. Only the HappyView `handle` global is writable; `db`, `http`, `json`, `params`, and `toarray` are read-only. It targets Lua 5.4 and skips line-length checks for long SQL expressions. Install Lua 5.4 and LuaRocks, then run `luarocks --lua-version=5.4 --local install luacheck 1.2.0` to enable that check locally. The lint runner also finds the default `~/.luarocks/bin` install if it is not on `PATH`. Branches without Lua files skip Luacheck. If Lua sources exist but generated handlers are missing, run `pnpm build:lua` first.

Expand Down Expand Up @@ -99,11 +103,11 @@ The root manifest's `validationLexicons` is the complete local schema closure us

The bootstrap command above should target only an explicitly approved HappyView instance. It sends `Authorization: Bearer <token>`; session-cookie authentication is not supported.

The token must have `lexicons:read` and `lexicons:create` for lexicon assets. If the bundle includes scripts, it also needs `scripts:read` and `scripts:manage`. For a bundle containing `getProfile`, it needs `script-variables:read` to check whether `HYPERCERTS_HANDLE_RESOLVER_URL` exists, and `script-variables:create` if that key is absent and must be set. HappyView's list response contains only keys and masked previews. The installer lists keys before prompting or creating the resolver variable and never deletes or intentionally overwrites an existing key. The HappyView POST endpoint is an upsert, so a concurrent change after the list preflight can still race with creation and overwrite the setting; avoid changing this key concurrently during installation. If a later asset write fails after setting creation, the installer reports that the setting remains and does not attempt rollback. If a manifest requests backfill for a new record lexicon, `backfill:create` is additionally needed to start that job; without it, the lexicon is still uploaded but no backfill starts. Remote admin targets must use HTTPS; HTTP is allowed only on `localhost`, `127.0.0.1`, or `::1`. Admin URL credentials and redirects are rejected. Resolver URLs must be an HTTPS origin with no credentials, path, query, or fragment. Hosts may be DNS names, IPv4 addresses, or syntactically valid bracketed IPv6 addresses; optional ports must be between 1 and 65535. Resolve installed-asset conflicts manually. The location, actor-follow, profile, and organization handlers, and the fixtures, require PostgreSQL.
The token must have `lexicons:read` and `lexicons:create` for lexicon assets. If the bundle includes scripts, it also needs `scripts:read` and `scripts:manage`. For a bundle containing `getProfile`, it needs `script-variables:read` to check whether `HYPERCERTS_HANDLE_RESOLVER_URL` exists, and `script-variables:create` if that key is absent and must be set. HappyView's list response contains only keys and masked previews. The installer lists keys before prompting or creating the resolver variable and never deletes or intentionally overwrites an existing key. The HappyView POST endpoint is an upsert, so a concurrent change after the list preflight can still race with creation and overwrite the setting; avoid changing this key concurrently during installation. If a later asset write fails after setting creation, the installer reports that the setting remains and does not attempt rollback. If a manifest requests backfill for a new record lexicon, `backfill:create` is additionally needed to start that job; without it, the lexicon is still uploaded but no backfill starts. Remote admin targets must use HTTPS; HTTP is allowed only on `localhost`, `127.0.0.1`, or `::1`. Admin URL credentials and redirects are rejected. Resolver URLs must be an HTTPS origin with no credentials, path, query, or fragment. Hosts may be DNS names, IPv4 addresses, or syntactically valid bracketed IPv6 addresses; optional ports must be between 1 and 65535. Resolve installed-asset conflicts manually. The location, actor-follow, profile, organization, and activity handlers, and the fixtures, require PostgreSQL.

## Test fixtures (disposable databases only)

The fixture tools seed records directly into PostgreSQL, bypassing HappyView ingestion. Use them **only with a separately approved, disposable loopback test database**, never persistent data. The `seed:test` and `seed:bad-dates` scripts require `HAPPYVIEW_DISPOSABLE_TEST_TARGET=YES`, a loopback `PGHOST`, a `PGDATABASE` name containing a `test` marker, and an absolute `PSQL_PATH` to a trusted `psql` executable. Standard `PGPORT` and `PGUSER` can select the test instance and user. The normal `seed:test` command seeds location, profile, organization, and actor-follow examples by default; `seed:bad-dates` seeds only location examples and remains separate. `pnpm test:contracts` runs the location, actor-follow, and organization HTTP contract suites; `pnpm test:bad-dates` remains a separate opt-in suite. Both seed tools also accept supplied record rows through `buildSeedInput` and `buildBadDateSeedInput` in `tooling/seed.js`.
The fixture tools seed records directly into PostgreSQL, bypassing HappyView ingestion. Use them **only with a separately approved, disposable loopback test database**, never persistent data. The `seed:test` and `seed:bad-dates` scripts require `HAPPYVIEW_DISPOSABLE_TEST_TARGET=YES`, a loopback `PGHOST`, a `PGDATABASE` name containing a `test` marker, and an absolute `PSQL_PATH` to a trusted `psql` executable. Standard `PGPORT` and `PGUSER` can select the test instance and user. The normal `seed:test` command seeds location, profile, organization, actor-follow, and activity/contributor-information examples by default; `seed:bad-dates` seeds only location examples and remains separate. `pnpm test:contracts` runs the location, actor-follow, organization, and activity HTTP contract suites; `pnpm test:bad-dates` remains a separate opt-in suite. Both seed tools also accept supplied record rows through `buildSeedInput` and `buildBadDateSeedInput` in `tooling/seed.js`.

Fixture CIDs are computed from stored record JSON with `@atcute/cbor` and `@atcute/cid`. If you seeded an older version of the blob-backed fixture, reseed the **disposable test database** before comparing CIDs: the earlier `jsonToLex` conversion produced a different CID.

Expand Down
98 changes: 98 additions & 0 deletions hypercerts-api/lexicons/org.hypercerts.claim.getActivity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"lexicon": 1,
"id": "org.hypercerts.claim.getActivity",
"defs": {
"main": {
"type": "query",
"description": "Public lookup of one indexed activity by full AT-URI. Authentication is not required.",
"parameters": {
"type": "params",
"required": ["uri"],
"properties": {
"uri": {
"type": "string",
"format": "at-uri",
"description": "Full AT-URI of the activity record, using a DID authority."
}
}
},
"output": {
"encoding": "application/json",
"schema": { "type": "ref", "ref": "#output" }
},
"errors": [
{ "name": "InvalidRequest", "description": "The URI is not a full activity AT-URI or the request contains an unsupported parameter." },
{ "name": "RecordNotFound", "description": "No indexed activity exists at this AT-URI." },
{ "name": "ActivityQueryFailed", "description": "The activity or required hydration lookup failed; retry after checking the HappyView PostgreSQL service." }
]
},
"output": {
"type": "object",
"required": ["activity"],
"properties": {
"activity": { "type": "ref", "ref": "#activityView" }
}
},
"activityView": {
"type": "object",
"description": "Activity record with metadata, unchanged source payload, hydrated author, and optional contributor projection.",
"required": ["uri", "cid", "indexedAt", "did", "author", "record"],
"nullable": ["indexedAt"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"indexedAt": { "type": "string", "format": "datetime" },
"did": { "type": "string", "format": "did" },
"author": { "type": "ref", "ref": "org.hypercerts.api.defs#actorView" },
"record": { "type": "ref", "ref": "org.hypercerts.claim.activity" },
"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": { "type": "ref", "ref": "#activityContributorView" }
}
}
},
"activityContributorView": {
"type": "object",
"description": "Original activity contributor fields with exact-version contributor information and nullable actor hydration.",
"required": ["contributorIdentity", "contributorInformation", "actor"],
"nullable": ["contributorInformation", "actor"],
"properties": {
"contributorIdentity": {
"type": "union",
"refs": ["org.hypercerts.claim.activity#contributorIdentity", "com.atproto.repo.strongRef"]
},
"contributionWeight": { "type": "string", "maxLength": 100 },
"contributionDetails": {
"type": "union",
"refs": ["org.hypercerts.claim.activity#contributorRole", "com.atproto.repo.strongRef"]
},
"contributorInformation": { "type": "ref", "ref": "#contributorInformationView" },
"actor": { "type": "ref", "ref": "#contributorActorView" }
}
},
"contributorInformationView": {
"type": "object",
"description": "The exact indexed contributor-information version identified by the source strong reference.",
"required": ["uri", "cid", "indexedAt", "did", "record"],
"nullable": ["indexedAt"],
"properties": {
"uri": { "type": "string", "format": "at-uri" },
"cid": { "type": "string", "format": "cid" },
"indexedAt": { "type": "string", "format": "datetime" },
"did": { "type": "string", "format": "did" },
"record": { "type": "ref", "ref": "org.hypercerts.claim.contributorInformation" }
}
},
"contributorActorView": {
"type": "object",
"description": "Contributor DID with nullable current Certified profile.",
"required": ["did", "profile"],
"nullable": ["profile"],
"properties": {
"did": { "type": "string", "format": "did" },
"profile": { "type": "ref", "ref": "org.hypercerts.api.defs#profileView" }
}
}
}
}
Loading
Loading