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
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,24 @@ Set `MONGO_URI` in `.env` to the database used by the server. The default develo

`npm run dev` uses nodemon. Use `npm start` for a regular Node process.

To start the documented MongoDB 7 development dependency with Docker and wait
until it accepts connections:

```sh
docker rm -f htmltrust-mongo >/dev/null 2>&1 || true
docker run -d --name htmltrust-mongo -p 127.0.0.1:27017:27017 mongo:7
until docker exec htmltrust-mongo mongosh --quiet --eval "db.adminCommand('ping').ok" >/dev/null 2>&1; do sleep 1; done
MONGO_URI=mongodb://localhost:27017/content-signing npm run dev
```

Remove that container when finished with `docker rm -f htmltrust-mongo`. The
repository's `npm run conformance:docker` command performs the same MongoDB 7
startup and readiness check for a disposable conformance run.

### Run tests

Unit tests use Node's built-in test runner and require no database:
Most unit tests use Node's built-in test runner without a database. The signer
opinion integration tests use `mongodb-memory-server`:

```sh
npm test
Expand All @@ -52,7 +67,7 @@ npm --prefix conformance/runner ci
npm run conformance
```

The 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 conformance command runs every fixture and the canonical v1 smoke checks. Set `SERVER_PORT` or `MONGO_PORT` when the defaults are occupied. The first test or conformance run can download a MongoDB binary for `mongodb-memory-server`; that requires network access. Its cache can be relocated with `MONGOMS_DOWNLOAD_DIR` when a persistent cache is preferred.

## Test in Docker

Expand All @@ -64,6 +79,10 @@ The repository script runs unit and conformance tests inside a disposable Node 2

This 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.

The end-to-end repository uses Docker Compose, not this server test script. Check
that Compose v2 is installed with `docker compose version` before running that
workflow.

## Deployment

1. Provision MongoDB 7, create a database for this service, and set `MONGO_URI` with credentials appropriate for the deployment.
Expand Down Expand Up @@ -116,14 +135,18 @@ The following routes retain the original `/api` prefix and response shapes:
| `/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/directory` | Search keys and content; read key reputation and occurrences; report keys, signers, or content; submit signer votes |
| `/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/directory/signer-votes` accepts `{ "signerId": "<exact keyid>", "voteType": "TRUST|DISTRUST", "reason": "<optional text>" }`. The signer ID may name a key hosted by another directory. An RFC 9421 signature keys the vote to its resolved `keyid`; the development `X-API-KEY` fallback uses one shared voter identity. A voter can submit one current vote per signer. The first submission returns `201`, and a repeat or vote change returns `200`. Reputation reads derive the current vote contribution from these records.

`POST /api/directory/signer-reports` records a report against an exact local or foreign keyid. Send an `Idempotency-Key` header when a caller may retry the request. A repeat with the same authenticated reporter, signer ID, and idempotency key returns the original report with `200`; a new report returns `201`. Reputation reads derive report counts and score adjustments from stored reports. The original local author/content vote API remains available at `POST /api/votes`.

### Deprecated route

`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.
Expand Down Expand Up @@ -176,7 +199,10 @@ openapi.yaml API contract and response schemas
## Related repositories

- [HTMLTrust specification](https://github.com/HTMLTrust/htmltrust-spec)
- [Canonicalization library](https://github.com/HTMLTrust/htmltrust-canonicalization), pinned to `760593d4a02e9fffa56dc4d002eb52ab2ade1b49` by this package
- [Browser reference](https://github.com/HTMLTrust/htmltrust-browser-reference)
- [Browser client](https://github.com/HTMLTrust/htmltrust-browser-client)
- [End-to-end harness](https://github.com/HTMLTrust/htmltrust-e2e)
- [CMS reference](https://github.com/HTMLTrust/htmltrust-cms-reference)
- [Project website](https://github.com/HTMLTrust/htmltrust-website)

Expand Down
179 changes: 170 additions & 9 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,14 @@ components:
type: number
minimum: 0
maximum: 1
reports:
type: integer
minimum: 0
description: Number of reports held by this directory for the signer.
verifiedSignatures:
type: integer
minimum: 0
description: Number of signatures this directory has verified for the signer.
asOf:
type: string
format: date-time
Expand All @@ -401,6 +409,34 @@ components:
type: string
format: uri

SignerVoteResult:
type: object
required: [voteId, voterId, signerId, voteType]
properties:
voteId: { type: string }
voterId: { type: string }
signerId: { type: string, maxLength: 2048 }
voteType:
type: string
enum: [TRUST, DISTRUST]
previousVoteType:
type: [string, "null"]
enum: [TRUST, DISTRUST, null]
updatedAt:
type: string
format: date-time

SignerReportResult:
type: object
required: [reportId, status]
properties:
reportId:
type: string
format: uuid
status:
type: string
enum: [PENDING, UNDER_REVIEW, ACCEPTED, REJECTED]

Claim:
type: object
required:
Expand Down Expand Up @@ -1740,12 +1776,21 @@ paths:
schema:
type: string
pattern: "^(k_[A-Za-z0-9_-]{20,64}|[0-9A-Fa-f]{24})$"
- name: Idempotency-Key
in: header
required: false
description: Reuse the same visible ASCII value when retrying one report.
schema:
type: string
minLength: 1
maxLength: 128
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- reason
properties:
Expand All @@ -1755,26 +1800,26 @@ paths:
description: Reason for reporting
details:
type: string
maxLength: 4096
description: Additional details about the report
evidence:
type: string
format: uri
maxLength: 2048
description: URL to evidence supporting the report
responses:
"201":
description: Report submitted successfully
content:
application/json:
schema:
type: object
properties:
reportId:
type: string
format: uuid
status:
type: string
enum: [PENDING, UNDER_REVIEW, ACCEPTED, REJECTED]
default: PENDING
$ref: "#/components/schemas/SignerReportResult"
"200":
description: A retry with the same idempotency key returned the existing report
content:
application/json:
schema:
$ref: "#/components/schemas/SignerReportResult"
"400":
description: Invalid input
content:
Expand All @@ -1794,6 +1839,122 @@ paths:
schema:
$ref: "#/components/schemas/Error"

/api/directory/signer-reports:
post:
tags:
- Directory
summary: Report a signer by exact keyid
description: Records this directory's opinion about a signer that may be published by another directory.
operationId: reportSigner
security:
- GeneralApiKey: []
parameters:
- name: Idempotency-Key
in: header
required: false
description: Reuse the same visible ASCII value when retrying one report.
schema:
type: string
minLength: 1
maxLength: 128
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [signerId, reason]
properties:
signerId:
type: string
maxLength: 2048
description: Exact signer keyid, including a foreign directory URL when applicable.
reason:
type: string
enum: [IMPERSONATION, MISINFORMATION, SPAM, OTHER]
details:
type: string
maxLength: 4096
evidence:
type: string
format: uri
maxLength: 2048
responses:
"201":
description: Report submitted successfully
content:
application/json:
schema:
$ref: "#/components/schemas/SignerReportResult"
"200":
description: A retry with the same idempotency key returned the existing report
content:
application/json:
schema:
$ref: "#/components/schemas/SignerReportResult"
"400":
description: Invalid input
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
description: Unauthorized

/api/directory/signer-votes:
post:
tags:
- Directory
summary: Record an authenticated opinion about a signer
description: Stores one current vote per authenticated voter and exact signer keyid.
operationId: submitSignerVote
security:
- HttpSignatureInput: []
HttpMessageSignature: []
- GeneralApiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [signerId, voteType]
properties:
signerId:
type: string
minLength: 1
maxLength: 2048
description: Exact signer keyid, including a foreign directory URL when applicable.
voteType:
type: string
enum: [TRUST, DISTRUST]
reason:
type: string
maxLength: 4096
responses:
"201":
description: First vote recorded
content:
application/json:
schema:
$ref: "#/components/schemas/SignerVoteResult"
"200":
description: Existing vote updated or repeated idempotently
content:
application/json:
schema:
$ref: "#/components/schemas/SignerVoteResult"
"400":
description: Invalid signer identifier, vote type, or reason
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
"401":
description: Authentication required

/api/directory/content:
get:
tags:
Expand Down
9 changes: 6 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"author": "Jason Grey <jason@jason-grey.com>",
"license": "LicenseRef-PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@htmltrust/canonicalization": "https://github.com/HTMLTrust/htmltrust-canonicalization/archive/5e51040dcaaf50935e245702bdefbc18a1d542ce.tar.gz",
"@htmltrust/canonicalization": "https://github.com/HTMLTrust/htmltrust-canonicalization/archive/760593d4a02e9fffa56dc4d002eb52ab2ade1b49.tar.gz",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^5.1.0",
Expand Down
12 changes: 8 additions & 4 deletions scripts/migrate-v1-indexes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const crypto = require('crypto');
const ContentSignature = require('../src/models/ContentSignature');
const Endorsement = require('../src/models/Endorsement');
const Key = require('../src/models/Key');
const SignerReport = require('../src/models/SignerReport');
const SignerVote = require('../src/models/SignerVote');

const CURRENT_INDEX_MODELS = [Key, ContentSignature, Endorsement, SignerVote, SignerReport];

const LEGACY_INDEXES = [
[ContentSignature, 'contentHash_1_domain_1_authorId_1'],
Expand Down Expand Up @@ -74,9 +78,9 @@ const migrate = async () => {
}
// Recreate the current partial/non-unique definitions without touching
// unrelated indexes owned by an operator or another application.
await Key.createIndexes();
await ContentSignature.createIndexes();
await Endorsement.createIndexes();
for (const model of CURRENT_INDEX_MODELS) {
await model.createIndexes();
}
console.log('v1 index migration complete');
} finally {
await mongoose.disconnect();
Expand All @@ -90,4 +94,4 @@ if (require.main === module) {
});
}

module.exports = { LEGACY_INDEXES, backfillPublicIds, dropIfPresent, migrate };
module.exports = { CURRENT_INDEX_MODELS, LEGACY_INDEXES, backfillPublicIds, dropIfPresent, migrate };
Loading
Loading