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
2 changes: 1 addition & 1 deletion hypercerts-api/.luacheckrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
std = "lua54"
globals = { "handle" }
read_globals = { "db", "json", "params", "toarray" }
read_globals = { "db", "env", "http", "json", "params", "toarray" }
not_globals = {
"io", "debug", "package", "require", "dofile", "loadfile", "load", "collectgarbage",
"os.execute", "os.exit", "os.getenv", "os.remove", "os.rename", "os.setlocale", "os.tmpname",
Expand Down
13 changes: 8 additions & 5 deletions hypercerts-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

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 and actor-follow query slices. Build their Lua handlers and run the offline checks before installing the bundle on an approved HappyView target.
**Current status:** The root `manifest.json` includes the location, actor-follow, and profile 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 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.

## Get started

From `hypercerts-api/`:
Expand All @@ -16,15 +18,15 @@ 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 and actor-follow 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.
These checks run offline; they do not require a running HappyView instance or database. `build:lua` builds the standalone location, actor-follow, and profile 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 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`, `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.
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.

## Bootstrap a running HappyView instance

After those offline checks pass, run this from `hypercerts-api/` to install the bundle on an approved, running HappyView instance. Unlike the checks above, this command contacts the instance and uploads assets.

For an interactive run, start the installer and enter the requested URL and admin token when prompted. Token input is hidden. Any nonblank values already set in the environment are used, and the installer prompts only for missing values.
For an interactive run, start the installer and enter the HappyView URL and admin token when prompted. Token input is hidden. If the bundle contains `getProfile` and `HYPERCERTS_HANDLE_RESOLVER_URL` is absent from HappyView, the installer also prompts for the resolver HTTPS base URL after checking existing assets and script variables. A pre-existing resolver variable is not prompted for or changed; HappyView only exposes its key and masked preview, so the installer cannot verify its actual value. Any nonblank HappyView URL/token values already set in the environment are used.

```sh
pnpm install:api
Expand All @@ -35,6 +37,7 @@ For a noninteractive run, provide both values in the environment:
```sh
HAPPYVIEW_BASE_URL='https://your-happyview.example' \
HAPPYVIEW_ADMIN_TOKEN='<scoped-admin-token>' \
HYPERCERTS_HANDLE_RESOLVER_URL='https://your-resolver.example' \
pnpm install:api
```

Expand Down Expand Up @@ -94,7 +97,7 @@ 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`. 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 targets must use HTTPS; HTTP is allowed only on `localhost`, `127.0.0.1`, or `::1`. URL credentials and HTTP redirects are rejected. Resolve installed-asset conflicts manually. The location and actor-follow 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, and profile handlers, and the fixtures, require PostgreSQL.

## Test fixtures (disposable databases only)

Expand Down
43 changes: 43 additions & 0 deletions hypercerts-api/lexicons/app.certified.actor.getProfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"lexicon": 1,
"id": "app.certified.actor.getProfile",
"defs": {
"main": {
"type": "query",
"description": "Public lookup of one indexed Certified profile by DID or handle. Handles are resolved through the configured resolver's com.atproto.identity.resolveHandle response; the response DID is validated, but no DID document is fetched for independent verification. Operators trust the resolver configured for their HappyView instance. Authentication is not required.",
"parameters": {
"type": "params",
"required": ["actor"],
"properties": {
"actor": {
"type": "string",
"format": "at-identifier",
"description": "DID or handle of the profile actor."
}
}
},
"output": {
"encoding": "application/json",
"schema": { "type": "ref", "ref": "#output" }
},
"errors": [
{ "name": "InvalidRequest", "description": "The actor is neither a valid DID nor a valid handle, or an unknown parameter was supplied." },
{ "name": "HandleResolverConfigError", "description": "HYPERCERTS_HANDLE_RESOLVER_URL is missing or invalid; configure an HTTPS resolver base URL for this HappyView instance." },
{ "name": "HandleResolutionFailed", "description": "The configured resolver request failed or returned a response without 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." }
]
},
"output": {
"type": "object",
"required": ["profile"],
"properties": {
"profile": {
"type": "ref",
"ref": "org.hypercerts.api.defs#profileView",
"description": "The complete indexed Certified profile record with record metadata and its repository DID."
}
}
}
}
}
51 changes: 51 additions & 0 deletions hypercerts-api/lexicons/app.certified.actor.getProfiles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"lexicon": 1,
"id": "app.certified.actor.getProfiles",
"defs": {
"main": {
"type": "query",
"description": "Returns one profile result for each supplied actor DID, preserving input order and duplicates. Authentication is not required. actors is a required nonempty array of 1..100 DIDs, counted before deduplication, encoded as repeated unbracketed query keys. Handles are not resolved. Missing indexed profiles are returned as null. This endpoint does not paginate.",
"parameters": {
"type": "params",
"required": ["actors"],
"properties": {
"actors": {
"type": "array",
"description": "Actor DIDs to look up; repeated unbracketed values preserve their original order and duplicates.",
"minLength": 1,
"maxLength": 100,
"items": { "type": "string", "format": "did" }
}
}
},
"output": {
"encoding": "application/json",
"schema": { "type": "ref", "ref": "#output" }
},
"errors": [
{ "name": "InvalidRequest", "description": "actors must contain 1 through 100 valid DIDs; unknown query parameters are not accepted." },
{ "name": "ProfileQueryFailed", "description": "The indexed profiles could not be queried." }
]
},
"output": {
"type": "object",
"required": ["profiles"],
"properties": {
"profiles": {
"type": "array",
"description": "One result per supplied actor occurrence, in input order. The profile is null when no indexed profile exists.",
"items": { "type": "ref", "ref": "#profileResult" }
}
}
},
"profileResult": {
"type": "object",
"required": ["actor", "profile"],
"nullable": ["profile"],
"properties": {
"actor": { "type": "string", "format": "did" },
"profile": { "type": "ref", "ref": "org.hypercerts.api.defs#profileView" }
}
}
}
}
38 changes: 38 additions & 0 deletions hypercerts-api/lexicons/app.certified.actor.listProfiles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"lexicon": 1,
"id": "app.certified.actor.listProfiles",
"defs": {
"main": {
"type": "query",
"description": "Public listing of indexed Certified profiles. Authentication is not required. actors uses repeated unbracketed keys, accepts at most 100 supplied DIDs before deduplication, and ORs them together. This listing has no text-search parameter; use searchProfiles for profile text discovery. The handler applies limit=25 and sortDirection=desc when omitted; limit is 1..100. Results sort by a valid zoned createdAt, otherwise indexedAt (or stored row creation time when indexedAt is absent), then URI, both in the requested direction. The opaque cursor is direction-bound and provides no snapshot guarantee; keep other parameters unchanged between pages. Unknown parameters and repeated scalar keys are rejected.",
"parameters": {
"type": "params",
"properties": {
"sortDirection": { "type": "string", "description": "Sort direction; handler default is desc.", "enum": ["asc", "desc"] },
"actors": { "type": "array", "description": "DIDs whose indexed profiles should be returned. Repeated values are ORed; handles are not resolved.", "maxLength": 100, "items": { "type": "string", "format": "did" } },
"limit": { "type": "integer", "description": "Page size; handler default is 25.", "minimum": 1, "maximum": 100 },
"cursor": { "type": "string", "description": "Opaque versioned keyset cursor bound to sortDirection." }
}
},
"output": {
"encoding": "application/json",
"schema": { "type": "ref", "ref": "#output" }
},
"errors": [
{ "name": "InvalidRequest", "description": "A parameter is invalid, repeated where scalar, or unknown." },
{ "name": "ProfileQueryFailed", "description": "The indexed profiles could not be queried." }
]
},
"output": {
"type": "object",
"required": ["profiles"],
"properties": {
"profiles": {
"type": "array",
"items": { "type": "ref", "ref": "org.hypercerts.api.defs#profileView" }
},
"cursor": { "type": "string", "description": "Opaque cursor for the next page; omitted when pagination is complete." }
}
}
}
}
Loading
Loading