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
49 changes: 49 additions & 0 deletions .github/workflows/hypercerts-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Hypercerts API checks

on:
pull_request:
paths:
- "hypercerts-api/**"
- ".github/workflows/hypercerts-api.yml"
push:
branches: [main, dev]
paths:
- "hypercerts-api/**"
- ".github/workflows/hypercerts-api.yml"

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-latest
defaults:
run:
working-directory: hypercerts-api
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: leafo/gh-actions-lua@6919171ccf181b826f44b9bca76307b577217377 # v13.0.0
with:
luaVersion: "5.4"

- uses: leafo/gh-actions-luarocks@35d062def313a7699a0d513e996bcf4352f05389 # v6.1.0

- name: Install Luacheck
run: luarocks install luacheck 1.2.0

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10.15.1

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: hypercerts-api/pnpm-lock.yaml

- name: Install Node dependencies
run: pnpm install --frozen-lockfile

- name: Lint and test
run: pnpm check
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Agent instructions

## Map
- `src/`, `crates/`, `tests/`, `migrations/`: Rust server, crates, tests, and database migrations.
- `web/`: Next.js app (npm). `packages/`: Bun workspace SDKs. `scripts/`: repo tooling.
- `hypercerts-api/`: standalone pnpm toolkit for installing/testing Hypercerts XRPC assets; this foundation branch has tooling, not a complete installable API. It is not part of the root workspace.

## Checks
Run checks for the area changed:
- Rust: `cargo fmt -- --check`; `cargo clippy --workspace --all-targets -- -D warnings`; unit tests: `cargo test --lib` and `cargo test --workspace --exclude happyview`.
- DB-backed tests: `cargo test --tests` with `TEST_DATABASE_URL` set. Start local Postgres with `docker compose -f docker-compose.test.yml up -d`.
- Web: from `web/`, `npm ci && npm run build` (lint: `npm run lint`).
- SDK package: from the root, run `bun install`, then `bun run --filter '<package>' build`, `bun run --filter '<package>' typecheck`, and `bun run --filter '<package>' test`.
- Hypercerts API: from `hypercerts-api/`, `pnpm check`.
5 changes: 5 additions & 0 deletions hypercerts-api/.luacheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
std = "lua54"
globals = { "db", "json", "params", "toarray", "handle" }
unused_args = true
-- Generated SQL expressions can be long; keep Luacheck focused on code issues.
max_line_length = false
80 changes: 80 additions & 0 deletions hypercerts-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Hypercerts API for HappyView

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:** This branch contains the installer and offline test tooling, not an installable API. There is no production `manifest.json` or Lua handler bundle here yet. You can run the unit tests, but do not run `tooling/installer.js` against a HappyView target from this branch.

## Get started

From `hypercerts-api/`:

```sh
pnpm install --frozen-lockfile
pnpm check
```

`pnpm check` 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. These checks do not require a running HappyView instance or database. The installable API bundle and its endpoint contract tests must be supplied separately before you can install or exercise API endpoints.

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. Console output is allowed only in CLI tooling. When a branch contains Lua scripts, Luacheck checks the generated handlers in `lua/endpoints/`; it rejects unknown globals and unused arguments, allowing only HappyView's `db`, `json`, `params`, `toarray`, and `handle`. 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`. On this foundation-only branch, which has no Lua files, the Lua check reports that it is skipped. If Lua sources exist but generated handlers are missing, run `pnpm build:lua` first.

## How an API bundle is installed

Once an API bundle supplies a root `manifest.json` and its referenced module manifests, the installer loads Lexicon and Lua script sources, checks dependencies and existing admin assets, and only writes missing assets. It skips unchanged assets and stops on conflicts so you can resolve them manually. An installation is not an all-or-nothing transaction: if a write fails, the installer reports what it already installed and what remains.

```mermaid
flowchart TD
Start(["Run installer"]) --> Load

subgraph S1["1. Load"]
Load["Read manifests and source files"] --> Valid{"All valid?"}
end

subgraph S2["2. Plan"]
Order["Sort assets by dependencies"]
end

subgraph S3["3. Check server"]
Fetch["Fetch what HappyView already has"] --> Conflict{"Any installed asset<br/>differs from ours?"}
end

subgraph S4["4. Install"]
Each["Take next asset"] --> Same{"Already identical?"}
Same -- Yes --> Skip["Skip it"]
Same -- No --> Post["Create it"]
Post --> OK{"Worked?"}
Skip --> More{"More assets?"}
OK -- Yes --> More
More -- Yes --> Each
end

Valid -- Yes --> Order --> Fetch
Conflict -- No --> Each
More -- No --> Done(["Print what changed<br/>and what didn't"])

Valid -- No --> Stop1["Stop: fix local files"]
Conflict -- Yes --> Stop2["Stop: resolve conflict by hand"]
OK -- No --> Stop3["Stop: report progress<br/>(no rollback)"]

classDef stop fill:#fde2e2,stroke:#c0392b,color:#7b1f1f
classDef done fill:#e2f5e6,stroke:#27ae60,color:#1e5631
class Stop1,Stop2,Stop3 stop
class Done done
```

The root manifest lists module manifests once:

```json
{ "modules": ["modules/shared/manifest.json", "modules/location/manifest.json"] }
```

Each module manifest declares `{ "assets": [...] }`. Assets have an `id`, `kind` (`lexicon` or `script`), `config`, and optional `dependsOn` asset IDs. Lexicons point to a `packagePath` in `@hypercerts-org/lexicon` or a local `path`; scripts use a local `path`. Local paths are relative to the **module manifest that declares them**. Declare shared assets in one module and reference their IDs from dependent modules. The installer rejects duplicate IDs, missing dependencies, cycles, and invalid source files before making admin requests.

With a complete bundle and an explicitly approved HappyView target, the entry point is `node tooling/installer.js`. Set `HAPPYVIEW_BASE_URL` and `HAPPYVIEW_ADMIN_TOKEN` in the environment. The installer sends `Authorization: Bearer <token>`; session-cookie authentication is not supported. Remote targets must use HTTPS; HTTP is allowed only on `localhost`, `127.0.0.1`, or `::1`. URL credentials and HTTP redirects are rejected.

The admin API key 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. Do not use the installer until the bundle includes its production manifests, handlers, and domain-specific completeness tests.

## 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, and organization examples by default; `seed:bad-dates` seeds only location examples by default. Both 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.
32 changes: 32 additions & 0 deletions hypercerts-api/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import js from "@eslint/js";
import globals from "globals";

export default [
{
ignores: ["node_modules/**"],
},
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.node,
},
rules: {
...js.configs.recommended.rules,
eqeqeq: ["error", "always", { null: "ignore" }],
"no-console": "error",
"no-implicit-coercion": "error",
"no-param-reassign": ["error", { props: true }],
"no-shadow": "error",
"no-var": "error",
"prefer-const": ["error", { destructuring: "all" }],
},
},
{
files: ["tooling/installer.js", "tooling/seed.js", "tooling/lint-lua.js"],
rules: {
"no-console": "off",
},
},
];
29 changes: 29 additions & 0 deletions hypercerts-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@happyview/hypercerts-api-kit",
"private": true,
"type": "module",
"dependencies": {
"@atcute/cbor": "2.3.2",
"@atcute/cid": "2.4.1",
"@atproto/lexicon": "0.6.2",
"@atproto/syntax": "0.5.4",
"@hypercerts-org/lexicon": "1.4.0"
},
"scripts": {
"lint:js": "eslint --max-warnings=0 .",
"lint:lua": "node tooling/lint-lua.js",
"lint": "pnpm run lint:js && pnpm run lint:lua",
"typecheck": "tsc --project tsconfig.json",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test:unit",
"test:unit": "node --test tooling/*.test.js tests/fixtures/*.test.js tests/contracts/helpers.test.js",
"seed:test": "node tooling/seed.js",
"seed:bad-dates": "node tooling/seed.js --bad-dates"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "24.3.0",
"eslint": "^10.11.0",
"globals": "^17.12.0",
"typescript": "5.9.3"
}
}
Loading
Loading