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
11 changes: 11 additions & 0 deletions .changeset/optional-service-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@hypercerts/hypercerts-feed-service": minor
---

Feed requests can now use AT Protocol service authentication. If a request includes a valid token, the token's issuer becomes the feed viewer. Requests without a token still need `params.viewerDid`. Authenticated tokens must include a non-empty `jti` of no more than 256 UTF-8 bytes and can be used only once per running process; retries require a fresh token, including after downstream failures.

Replay protection allows up to 512 live entries per verified issuer and 4,096 live entries per process. It is not shared across replicas or restarts. A full live issuer quota or replay store returns HTTP 503 until entries expire, preserving capacity for other issuers until the global bound is reached.

Before upgrading, set `SERVICE_DID` to this service's DID, even if you do not use authentication. It must be a bare DID, without `#serviceId`. If it is a hostname-level `did:web` and the matching hostname points to this service, `GET /.well-known/did.json` now publishes a `#hypercerts_feed` service entry at that hostname for discovery.

Node HTTP requests now retain working service authentication after their POST body finishes. The service upgrades `@atproto/lex-server` to 0.1.19 and avoids passing its prematurely aborted request signal to DID resolution. The `did#serviceId` format from AT Protocol Proposal 0014 is not supported yet because `@atproto/lex-server` does not support it.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
DATABASE_URL=postgres://hypercerts_feed_reader:change-me@postgres:5432/hyperindex
SERVICE_DID=did:web:feed.example
SERVICE_AUTH_MAX_AGE_SECONDS=300
DID_RESOLUTION_TIMEOUT_MS=2000
PORT=3000
HOST=0.0.0.0
METRICS_HOST=0.0.0.0
Expand Down
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

## Read this first

This repository is a standalone, read-only TypeScript service exposing two unauthenticated POST procedures:
This repository is a standalone, read-only TypeScript service exposing two POST procedures with optional AT Protocol service authentication:

```text
org.hypercerts.feed.getFeedSkeleton
org.hypercerts.feed.getFeed
```

The skeleton returns URI-only generic feed subjects. The hydrated endpoint returns generic feed entries with validated feed-specific views and actor summaries. Hyperindex is the only supported database owner. The service reads its current PostgreSQL state directly; it does not ingest, authenticate, write records, own migrations, cache across requests, call Hyperindex/PDS/AppView APIs, download blobs, hydrate target records, or provide immutable history.
The skeleton returns URI-only generic feed subjects. The hydrated endpoint returns generic feed entries with validated feed-specific views and actor summaries. Hyperindex is the only supported database owner. The service reads its current PostgreSQL state directly; it does not ingest, write records, own migrations, cache feed results across requests, call Hyperindex/PDS/AppView APIs, download blobs, hydrate target records, or provide immutable history. Optional service auth verifies the issuer's `#atproto` signing key, audience, expiry, endpoint binding, and DID document over a bounded secure resolver; the verified issuer, never the body, supplies an authenticated viewer. It retains only bounded, process-local replay-protection state through the JWT `jti`; that state is not a feed-result cache.

Use **npm**, not pnpm. `package-lock.json` is authoritative. Node.js 22.13+ is supported; CI and Docker use Node.js 24. PostgreSQL 16+ is required.

Expand Down Expand Up @@ -89,7 +89,7 @@ src/server.ts

Ownership:

- `src/server.ts` is the composition root. It creates the registered Hypercerts feed, one shared registry, separate endpoint services, and one identity adapter; it also owns listener settings, initial readiness, and graceful shutdown.
- `src/server.ts` is the composition root. It creates the registered Hypercerts feed, one shared registry, separate endpoint services, one identity adapter, and the configured optional service-auth verifier; that verifier owns one replay map shared by both endpoints; it also owns listener settings, initial readiness, and graceful shutdown.
- `src/app.ts` is the fetch-compatible boundary. It owns fixed route metadata, POST enforcement, the 64 KiB body limit, malformed JSON, routed validation messages, and bounded request metrics.
- `src/api/get-feed-skeleton.ts` and `src/api/get-feed.ts` register the procedures, run generated output validation inside the error boundary, and translate expected `FeedError` values.
- `src/feed/service.ts` projects registry-selected metadata rows into the public skeleton. It does not own dispatch, cursor, or pagination policy.
Expand Down Expand Up @@ -141,7 +141,8 @@ A request, response, event kind, view, or public-error change normally requires
Preserve these unless the public contract is intentionally revised and documented:

- Both procedures accept the same `{ feedId, params?, limit?, cursor? }` wrapper. `limit` and `cursor` are generic top-level pagination controls; `params` contains only algorithm-specific values and may be omitted for feeds that declare no params contract. The public params union remains open for future feeds. Runtime dispatch rejects an unregistered `feedId` with `UnsupportedFeed`, while the current Hypercerts feed rejects missing params or a mismatched params discriminator with `InvalidRequest` before querying.
- The base scope always resolves from the viewer's current Certified follows. There is no caller-supplied author override.
- The base scope always resolves from the normalized viewer's current Certified follows. Anonymous requests require `params.viewerDid`; authenticated requests may omit it, but a supplied value must exactly match the verified service-auth issuer. There is no caller-supplied author override or body-based override of authenticated identity.
- Authenticated tokens must carry a non-empty string `jti` of 1–256 UTF-8 bytes. After successful signature and exact `lxm` verification, each issuer-and-`jti` pair is consumed once in the shared process-local replay map, with a maximum of 512 live entries per verified issuer and 4,096 live entries total. Expired entries are purged without evicting live entries; a live full issuer quota or global map fails closed with HTTP 503. The map is cleared on restart and is not replica-global. Downstream failures do not restore a consumed token.
- Deduplicate request lists before enforcing semantic limits: 64 evaluators, 16 kinds, and 1–50 page items.
- Evaluator endorsement subjects are unioned after base-author resolution. Remove the viewer and deduplicate candidates. Do not query actor status: Hyperindex purges source records for explicitly deleted, deactivated, suspended, or taken-down identities, and actors absent from `actor` remain eligible.
- Omitted or empty `kinds` means all supported kinds. Unknown kinds fail with the generic `InvalidRequest` error and an actionable Hypercerts-parameter message.
Expand Down Expand Up @@ -181,7 +182,7 @@ Rate limiting belongs at the gateway. Keep `/health` and `/ready` private.

## MVP exclusions

Do not add ingestion, writes, authentication, cross-request caching, immutable history, Hyperindex/PDS/AppView API calls, blob downloads/proxying, target-record reads, target previews, recursive/detail hydration, activity-label hydration, preference persistence, or migrations/indexes.
Do not add ingestion, writes, stronger authentication modes, feed-result caching across requests, immutable history, Hyperindex/PDS/AppView API calls, blob downloads/proxying, target-record reads, target previews, recursive/detail hydration, activity-label hydration, preference persistence, or migrations/indexes. Service-auth verification is limited to the maintained AT Protocol verifier and the bounded DID-resolution boundary described above; retain only bounded, process-local replay-protection state through the JWT `jti`, and never log authorization credentials, JWTs, or claims.

## Change checklist

Expand Down
Loading
Loading