- Maintainer: Jason Grey
- Updated: 2026-08-28
- Version: 0.1.0, draft v1 API
- Status: Runnable reference server
- For: directory operators and integration developers
- Reading time: 10 minutes
This repository contains the runnable Node.js reference server for the HTMLTrust trust directory API. It stores author profiles and public keys, accepts signed content and endorsements, and exposes directory search and reputation data.
The wire contract is documented in openapi.yaml. The server is the implementation used by the local development workflow and the end-to-end simulation.
For a local Node run, install:
- Node.js 22 or newer
- MongoDB 7 or a compatible MongoDB deployment
Docker users can run the complete test suite without installing Node.js or MongoDB. See Test in Docker.
git clone https://github.com/HTMLTrust/htmltrust-server-reference.git
cd htmltrust-server-reference
npm ci
cp .env.example .env
npm run devSet MONGO_URI in .env to the database used by the server. The default development URI is mongodb://localhost:27017/content-signing, and the server listens on port 3000. Open http://localhost:3000/ for the demo page.
npm run dev uses nodemon. Use npm start for a regular Node process.
Unit tests use Node's built-in test runner and require no database:
npm test
npm run openapi:lintInstall the conformance runner once, then run the reference server against a disposable in-process MongoDB:
npm --prefix conformance/runner ci
npm run conformanceThe conformance command runs every fixture and the canonical v1 smoke checks. Set SERVER_PORT or MONGO_PORT when the defaults are occupied. The first run can download a MongoDB binary for mongodb-memory-server.
The repository script runs unit and conformance tests inside a disposable Node 22 container. It mounts the checkout read-only, copies sources into the container, and installs dependencies there. Test output and generated runtime files leave no files in the checkout. Checkout-scoped Docker volumes cache npm packages and the MongoDB test binary.
./scripts/test-in-docker.shThis is the lowest-dependency test path: it requires Docker and a shell. Set HTMLTRUST_TEST_IMAGE to use another compatible Node image. The older npm run conformance:docker command remains available for developers who want to run the conformance runner with a host Node process and a Docker MongoDB container.
-
Provision MongoDB 7, create a database for this service, and set
MONGO_URIwith credentials appropriate for the deployment. -
Install production dependencies from the lockfile:
npm ci --omit=dev
-
Set
NODE_ENV=production,AUTHOR_API_KEY_PEPPER, andDIRECTORY_BASE_URL. Use a random, long-lived pepper and keep it outside the repository.DIRECTORY_BASE_URLmust be the public HTTPS origin clients use to resolve directory key URLs. When the variable is unset, the server falls back to the request origin for local development, includinghttp://localhost; that fallback is not a production deployment setting. -
Set
GENERAL_API_KEYandADMIN_API_KEYwhen compatibility or operator routes need them. Static general and author API-key authentication is disabled in production unlessHTMLTRUST_ALLOW_API_KEY_AUTH=1is set. -
Start the service with
npm startbehind a TLS-terminating reverse proxy. SetTRUST_PROXYto the number of trusted proxy hops when the proxy forwards client addresses.
Before upgrading a database created by an earlier server version, run the explicit index migration with the same connection string used by the service:
MONGO_URI="mongodb://user:password@db.example/htmltrust" npm run migrate:v1The migration replaces the legacy content identity and endorsement indexes. Run it during a maintenance window and verify backups before changing production data.
See .env.example for every supported setting. Remote did: and HTTPS key resolution is disabled by default because dereferencing submitter-provided URLs creates an outbound-request risk. Enable HTMLTRUST_REMOTE_KEY_RESOLUTION=1 only after reviewing the network policy for the deployment.
The server exposes two HTTP surfaces. The root routes are the canonical HTMLTrust v1 directory surface. The /api routes are compatibility routes used by the demo UI and existing integrations.
| Method and path | Purpose | Authentication |
|---|---|---|
GET /.well-known/htmltrust |
Discover directory version, capabilities, algorithms, and profiles | Public |
GET /keys/:id |
Retrieve a directory key document | Public |
GET /signers/:id/reputation |
Retrieve signer reputation | Public |
POST /content |
Submit and re-verify a signed content record | RFC 9421 HTTP Message Signature |
GET /content/:hash |
Retrieve a content record by percent-encoded hash | Public |
GET /content/:hash/endorsements |
List endorsements for a content hash | Public |
POST /endorsements |
Store a signed endorsement | RFC 9421 HTTP Message Signature |
Canonical writes require a resolvable key in an RFC 9421 signature. The sig1 input MUST cover exactly @method, @target-uri, host, date, and content-digest, in that order. The keyid identifies the key that signed the request.
Canonical reads return an HTMLTrust media type by default and accept application/json. Responses include Vary: Accept; public reads include Cache-Control and ETag. A matching If-None-Match request receives 304 Not Modified. Canonical JSON submissions accept application/json and application/*+json; another request media type receives 415.
The following routes retain the original /api prefix and response shapes:
| Route group | Operations |
|---|---|
/api/authors |
Create and list authors; read, update, or delete an author; read its public key |
/api/content |
Sign, verify, submit, and retrieve content; register occurrences; list content endorsements |
/api/claims |
Create, list, read, update, and delete claim types |
/api/directory |
Search keys and content; read key reputation and occurrences; report keys or content |
/api/endorsements |
List, submit, and delete endorsements |
/api/votes |
Submit votes; list votes; read vote statistics; delete a vote |
/api/keys and /api/signers |
Read the compatibility key and signer-reputation documents |
/api/.well-known/htmltrust |
Compatibility alias for the discovery document |
The compatibility routes use the API-key headers described below for their original protected operations. Content and endorsement submission also accept the RFC 9421 flow, while canonical root submissions require that flow. The OpenAPI file defines the long-term v1 resource shapes and media types; this implementation currently keeps author, claim-management, directory-search, reporting, and voting operations under /api.
POST /api/content/verify is deprecated and returns Deprecation: true as specified by RFC 9745. Signature verification belongs in the client, using a public key retrieved from the directory and a local cryptographic API such as SubtleCrypto. The route remains for legacy clients and will be removed in a future major version. There is no canonical root /content/verify route.
Use RFC 9421 signatures for canonical writes. The compatibility surface supports these headers when its API-key authentication is enabled:
| Header | Use |
|---|---|
X-API-KEY |
General compatibility operations, including author creation, occurrence registration, reporting, and demo submissions |
X-AUTHOR-API-KEY |
Author-specific operations and the v1 convenience signing helper |
X-ADMIN-API-KEY |
Claim-type administration and endorsement takedown |
Author API keys are returned once by POST /api/authors. The server stores an HMAC-SHA-256 digest under AUTHOR_API_KEY_PEPPER. A deployment that still has plaintext keys must migrate them using the procedure in src/utils/apiKeys.js, then remove the plaintext field.
- Hashes, signatures, and key bytes use unpadded standard Base64.
sourceURLvalues sent toPOST /api/content/signmust be final HTTPS URLs. Thescopevalue (urlororigin) determines the derivedlocationbound into the v1 signing payload.POST /api/content/signcomputesclaimsHashfrom the strict v1 claims array and signs the RFC 8785 JCS object containing the frozen profile, algorithm, keyid, content hash, claims hash, timestamp, scope, and location. It returns the complete attribute set needed for a<signed-section>.- The convenience signer uses the newest active directory-held key. It returns the stored artifact for an identical retry. A changed payload for the same content, location, and key returns
409 Conflictbecause signed artifacts are immutable. - The convenience route rejects the legacy
domain,authorId, and caller-suppliedclaimsHashfields. Use canonicalPOST /contentwhen the author holds the private key and submits an RFC 9421-authenticated signature. - Endorsement signatures cover the RFC 8785 JCS serialization of the endorsement document with
signatureomitted. New endorsements are served as signed, and the 201 response supplies the stored identifier inLocation. - Endorsements are append-only. An identical retry on canonical
POST /endorsementsreturns201with the existing resource inLocation; the/api/endorsementscompatibility route returns200. A different document from the same endorser and content hash is stored as another record.
POST /api/authors accepts an optional SPKI PEM publicKey. Supplying one registers a key held by the caller and leaves its private key outside the directory. Omitting it asks the server to generate and hold a key pair for the convenience registry flow. The public key document uses an opaque id in /keys/{id} URLs; existing pre-v1 rows remain readable through their legacy ObjectId URL until migrated. Content signed by a caller-held key is submitted through POST /content or the compatibility POST /api/content route.
src/
├── server.js Express application entry point
├── config/ MongoDB connection setup
├── controllers/ Request handlers
├── middleware/ Authentication and content negotiation
├── models/ Mongoose schemas
├── public/ Demo web UI
├── routes/ Compatibility route definitions
└── utils/ Cryptography and protocol helpers
conformance/
├── fixtures/ YAML API scenarios
└── runner/ Implementation-agnostic conformance runner
scripts/test-in-docker.sh Docker-only unit and conformance entrypoint
openapi.yaml API contract and response schemas
This project is licensed under the PolyForm Noncommercial License 1.0.0. Commercial use requires a separate agreement with the licensor.
Issues and pull requests are welcome. Contributions may include code, specification text, documentation, or conformance fixtures. Please keep changes focused on improving the protocol and its implementations.