Skip to content
Merged
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
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
# HTMLTrust Server Reference
# HTMLTrust Server Reference (Node.js)

Reference implementation of the HTMLTrust trust directory API — a server that manages author identities, cryptographic key pairs, content signing/verification, and a federated trust directory with reputation tracking.

This is a companion to the [HTMLTrust specification](https://github.com/ArcadeLabsInc/htmltrust-spec).

## Personality: the "permissive community directory"

The HTMLTrust protocol is federated, meaning multiple trust directories MAY coexist with different curatorial philosophies. This Node.js implementation is the baseline reference: full-featured, permissive, and neutral -- suitable for general-purpose deployment and as a canonical implementation of every endpoint in the OpenAPI spec.

The sibling reference implementations demonstrate alternative curatorial philosophies using the same protocol:

- **[`htmltrust-server-reference-python`](../htmltrust-server-reference-python/)** -- curated journalism directory. Admin-approval queue, Article/News scope, punitive reputation formula. Simulates EFF/ProPublica/Poynter-style deployments.
- **[`htmltrust-server-reference-rust`](../htmltrust-server-reference-rust/)** -- rapid-flag public-safety directory. Time-decayed reputation, whitelisted-researcher fatal flagging, PostgreSQL backend. Simulates Internet Archive / security research collective deployments.

All three conform to the same OpenAPI spec. Clients don't need per-directory logic -- they simply subscribe to one or more directories and weight the returned scores according to their own trust policy.

## What It Does

This server implements the **Trust Directory** component of the HTMLTrust system:
Expand Down Expand Up @@ -59,11 +70,18 @@ Full API documentation is in [`openapi.yaml`](openapi.yaml). Key endpoint groups
| `POST /api/authors` | Create author + key pair | General API key |
| `GET /api/authors/:id/public-key` | Get author's public key | Public |
| `POST /api/content/sign` | Sign a content hash | Author API key |
| `POST /api/content/verify` | Verify a signature | Public |
| `POST /api/content/verify` | Verify a signature (deprecated, see below) | Public |
| `GET /api/directory/keys` | Search public keys | Public |
| `GET /api/directory/content` | Search signed content | Public |
| `GET /api/endorsements?content-hash=...` | List endorsements for a content hash | Public |
| `POST /api/endorsements` | Submit a signed endorsement | General API key |
| `DELETE /api/endorsements/:id` | Delete an endorsement | General API key |
| `POST /api/votes` | Vote trust/distrust | General API key |

### Deprecated endpoints

`POST /api/content/verify` is deprecated. Per [HTMLTrust spec §3.1](https://htmltrust.dev/spec#section-3-1), cryptographic verification is a local operation: clients MUST verify signatures themselves (e.g. via `SubtleCrypto`) using public keys resolved through the directory's key endpoints. A remote yes/no answer from the directory is by definition not a cryptographic guarantee since the directory is not part of the trust root. The endpoint remains as a low-trust convenience for legacy clients, returns the `Deprecation: true` header (RFC 9745), and will be removed in a future major version. The directory's role is to serve public keys, endorsements, and reputation data — not to act as an oracle for signature validity.

### Authentication

Three tiers of API key auth via headers:
Expand All @@ -86,6 +104,7 @@ src/
│ ├── claimController.js
│ ├── contentController.js
│ ├── directoryController.js
│ ├── endorsementController.js
│ └── voteController.js
├── middleware/
│ └── auth.js # API key authentication
Expand All @@ -94,6 +113,7 @@ src/
│ ├── Claim.js
│ ├── ContentOccurrence.js
│ ├── ContentSignature.js
│ ├── Endorsement.js
│ ├── Key.js
│ └── Vote.js
├── public/ # Demo web UI
Expand All @@ -104,6 +124,7 @@ src/
│ ├── claims.js
│ ├── content.js
│ ├── directory.js
│ ├── endorsements.js
│ └── votes.js
└── utils/
└── crypto.js # Key generation, signing, verification
Expand Down
201 changes: 199 additions & 2 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ tags:
description: Operations for managing claim types and assertions
- name: Directory
description: Operations for the Network of Trust directory services
- name: Endorsements
description: |
Operations for storing and retrieving third-party endorsements per
HTMLTrust spec §2.5. Endorsements are standalone signed JSON blobs
that attest a third party's opinion about a specific content hash.
The directory is a passive store; verification is performed locally
by the client.

security:
- {} # No default security
Expand Down Expand Up @@ -303,6 +310,54 @@ components:
authorId: "123e4567-e89b-12d3-a456-426614174000"
signatureValid: true

Endorsement:
type: object
description: |
A standalone signed endorsement of a specific content hash, per
HTMLTrust spec §2.5. Verification is performed locally by the
client; the directory is a passive store.
required:
- endorser
- contentHash
- signature
- timestamp
properties:
_id:
type: string
description: Server-assigned identifier
endorser:
type: string
description: Opaque endorser keyid (e.g. "did:web:publisher.org")
contentHash:
type: string
description: The targeted content hash (e.g. "sha256:...")
signature:
type: string
description: Base64 signature over "{contentHash}:{timestamp}"
timestamp:
type: string
description: ISO-8601 timestamp at which the endorsement was issued
algorithm:
type: string
description: Signature algorithm (default ed25519)
default: ed25519
rawBlob:
type: string
description: |
The exact bytes that were signed. Clients SHOULD use this for
byte-identical re-verification.
createdAt:
type: string
format: date-time
description: When the endorsement was stored by this directory
example:
endorser: "did:web:publisher.org"
contentHash: "sha256:RAyBCvKTW5KNnGZSyXZYe+8V8DEEnUMRxjk5LSgCHo4"
signature: "BASE64_SIG"
timestamp: "2025-05-01T00:00:00Z"
algorithm: "ed25519"
rawBlob: "{\"endorser\":\"did:web:publisher.org\",\"endorsement\":\"sha256:RAyBCvKTW5KNnGZSyXZYe+8V8DEEnUMRxjk5LSgCHo4\",\"signature\":\"BASE64_SIG\",\"timestamp\":\"2025-05-01T00:00:00Z\"}"

paths:
/authors:
post:
Expand Down Expand Up @@ -646,8 +701,14 @@ paths:
post:
tags:
- Content
summary: Verify content signature
description: Verifies the signature of content
summary: Verify content signature (DEPRECATED)
description: |
DEPRECATED: cryptographic verification is a local operation per
HTMLTrust spec §3.1. This endpoint is retained as a low-trust
convenience and will be removed in a future major version. Clients
MUST verify signatures locally (e.g. via SubtleCrypto). Responses
include the `Deprecation: true` header per RFC 9745.
deprecated: true
operationId: verifyContent
requestBody:
required: true
Expand Down Expand Up @@ -1209,3 +1270,139 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"

/endorsements:
get:
tags:
- Endorsements
summary: List endorsements for a content hash
description: |
Returns all endorsements on file for a given content hash. The
directory is a passive store; clients MUST verify each endorsement's
signature locally using the endorser's public key (per spec §2.5)
before treating it as trustworthy. Unverified endorsements MUST NOT
contribute to any trust decision.
operationId: listEndorsements
parameters:
- name: content-hash
in: query
required: true
description: The content hash to look up endorsements for (e.g. "sha256:...")
schema:
type: string
responses:
"200":
description: Array of endorsements
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Endorsement"
"400":
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Endorsements
summary: Submit a signed endorsement for storage
description: |
Stores a signed endorsement blob. The directory MAY opportunistically
verify the signature against any locally-known public key for the
endorser as a sanity check, but storage does not depend on
verification — clients verify locally per spec §2.5.
operationId: createEndorsement
security:
- GeneralApiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- endorser
- contentHash
- signature
- timestamp
properties:
endorser:
type: string
description: Opaque endorser keyid (e.g. "did:web:publisher.org")
contentHash:
type: string
description: The targeted content hash (e.g. "sha256:...")
signature:
type: string
description: Base64 signature over "{contentHash}:{timestamp}"
timestamp:
type: string
description: ISO-8601 timestamp at which the endorsement was issued
algorithm:
type: string
description: Signature algorithm (default ed25519)
default: ed25519
rawBlob:
type: string
description: |
The exact bytes the client signed over. Clients SHOULD
supply this so verifiers can re-verify byte-identically.
If omitted, the server constructs a canonical blob in a
stable key order.
responses:
"201":
description: Endorsement stored
content:
application/json:
schema:
$ref: "#/components/schemas/Endorsement"
"400":
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

/endorsements/{id}:
delete:
tags:
- Endorsements
summary: Delete an endorsement
description: |
Removes an endorsement from the directory. MVP: gated behind the
general API key only. Production deployments MUST additionally
require that the caller's authenticated identity match the
endorsement's `endorser` keyid.
operationId: deleteEndorsement
security:
- GeneralApiKey: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"204":
description: Endorsement deleted
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Endorsement not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
Loading
Loading