@@ -8,6 +8,110 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88
99### Added
1010
11+ - ** Client API auth v2: multi-token, scoped, mTLS-capable.** New
12+ [ ` pkg/httpauth/ ` ] ( pkg/httpauth/ ) package with ` Policy ` ,
13+ ` TokenIdentity ` , ` CertIdentity ` , ` Scope ` types and a
14+ scope-enforcing fiber middleware. Replaces the single-token
15+ bearerAuth helper in ` cmd/hypercache-server/main.go ` . Three
16+ credential classes resolved in priority order (bearer → mTLS
17+ cert → ServerVerify hook), with constant-time multi-token
18+ compare that visits every configured token even on early match
19+ to prevent token-cardinality timing leaks. Per-route scope
20+ enforcement: ` GET ` /` HEAD ` /owners-lookup/` batch-get ` require
21+ ` ScopeRead ` ; ` PUT ` /` DELETE ` /` batch-put ` /` batch-delete ` require
22+ ` ScopeWrite ` . Anonymous identity (with ` AllowAnonymous: true ` )
23+ receives all scopes — used by the binary to preserve the
24+ zero-config dev posture.
25+ - ** YAML auth config + legacy env-var coexistence.**
26+ ` HYPERCACHE_AUTH_CONFIG=/etc/hypercache/auth.yaml ` (new) loads
27+ a multi-token policy with per-identity scopes:
28+
29+ ``` yaml
30+ tokens :
31+ - id : app-prod
32+ token : " <secret>"
33+ scopes : [read, write]
34+ - id : ops
35+ token : " <secret>"
36+ scopes : [admin]
37+ cert_identities :
38+ - subject_cn : app.internal
39+ scopes : [read]
40+ allow_anonymous : false
41+ ` ` `
42+
43+ The legacy ` HYPERCACHE_AUTH_TOKEN` keeps working byte-identical:
44+ one synthesized identity with all three scopes. The two env
45+ vars are NOT mutually exclusive — `HYPERCACHE_AUTH_CONFIG`
46+ governs the client API, `HYPERCACHE_AUTH_TOKEN` continues to
47+ drive the dist transport's symmetric peer auth (single trust
48+ domain). Both can be set in the same deployment without
49+ conflict. Missing or malformed config files exit the binary
50+ non-zero rather than fall through to permissive open mode —
51+ fail-closed by design.
52+ - **mTLS on the client API.** New env vars
53+ ` HYPERCACHE_API_TLS_CERT` , `HYPERCACHE_API_TLS_KEY`, and
54+ ` HYPERCACHE_API_TLS_CLIENT_CA` wrap the listener with
55+ ` tls.NewListener` . With CA set, `RequireAndVerifyClientCert`
56+ is enabled and the verified peer cert's Subject CN is matched
57+ against the policy's `CertIdentities` to resolve the calling
58+ identity. Plaintext, standard-TLS, and mTLS shapes all share
59+ one listener path. End-to-end coverage at
60+ [cmd/hypercache-server/mtls_e2e_test.go](cmd/hypercache-server/mtls_e2e_test.go)
61+ drives a real handshake against an in-process CA / server-cert
62+ / client-cert chain and asserts CN-to-identity resolution
63+ works in both directions (matching CN → 200, non-matching
64+ CN → 401).
65+
66+ # ## Security
67+
68+ - **Constant-time bearer-token compare on the client API.** Replaced
69+ the plaintext `got != want` check at
70+ [cmd/hypercache-server/main.go](cmd/hypercache-server/main.go) with
71+ ` crypto/subtle.ConstantTimeCompare` to defeat timing side-channels.
72+ A naive string compare returns as soon as the first differing byte
73+ is found, leaking per-byte equality of `HYPERCACHE_AUTH_TOKEN` to a
74+ remote attacker who can measure response time. The fix mirrors the
75+ dist transport's existing constant-time check at
76+ [pkg/backend/dist_http_server.go:144-152](pkg/backend/dist_http_server.go#L144-L152).
77+ No public API change; the env-var contract and "empty token →
78+ open mode" back-compatible behavior are unchanged. New auth-test suite
79+ at [cmd/hypercache-server/auth_test.go](cmd/hypercache-server/auth_test.go)
80+ pins the contract : missing/wrong/malformed/lowercase/wrong-length
81+ bearer headers all return 401, public meta routes (`/healthz`,
82+ ` /v1/openapi.yaml` ) stay reachable without credentials, every
83+ protected route fires the wrapper. The new `newAuthedServer`
84+ helper drives `registerClientRoutes` directly so future wiring
85+ regressions are caught (the existing `handlers_test.go::newTestServer`
86+ deliberately bypasses auth for handler-correctness coverage).
87+
88+ # ## Added
89+
90+ - **OpenAPI 3.1 specification + drift-detection.** The
91+ ` hypercache-server` binary now embeds its own contract via
92+ [`cmd/hypercache-server/openapi.yaml`](cmd/hypercache-server/openapi.yaml)
93+ (`//go:embed`) and serves it at `GET /v1/openapi.yaml` — every
94+ running node is self-describing. The spec covers all nine client
95+ routes (single-key PUT/GET/HEAD/DELETE, owners lookup, three
96+ batch operations, plus the `/healthz` and `/v1/openapi.yaml`
97+ meta endpoints), with reusable `ErrorResponse`, `ItemEnvelope`,
98+ and batch-operation schemas, the `bearerAuth` security scheme,
99+ and `operationId` on every operation for codegen-friendliness.
100+ A drift detector at
101+ [cmd/hypercache-server/openapi_test.go](cmd/hypercache-server/openapi_test.go)
102+ drives `registerClientRoutes` directly and asserts every
103+ fiber-registered route has a matching path in the spec — and
104+ vice-versa — so the contract cannot silently fall out of sync
105+ with the binary. Two CI workflows back this up at
106+ [.github/workflows/openapi.yml](.github/workflows/openapi.yml) :
107+ ` redocly lint` validates the schema against the OpenAPI 3.1
108+ meta-spec, and the Go drift test runs on every change to
109+ ` main.go` or the spec. The docs site renders the same spec
110+ inline at the new
111+ [API Reference](docs/api.md) page via the
112+ ` mkdocs-swagger-ui-tag` plugin — a single source of truth for
113+ the binary, the docs, and any client codegen that points at a
114+ live cluster.
11115- **Documentation site on GitHub Pages**, built with MkDocs Material
12116 and published automatically on every push to `main`. Eight
13117 navigated pages — landing, quickstart, 5-node cluster tutorial,
@@ -31,24 +135,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
31135- **Richer client API — metadata inspection, JSON envelopes, batch
32136 operations.** Three additions to the
33137 `cmd/hypercache-server` HTTP surface :
34- - ` HEAD /v1/cache/:key ` returns the value's metadata in
35- ` X-Cache-* ` response headers (Version, Origin, Last-Updated,
36- TTL-Ms, Expires-At, Owners, Node) with no body — fast
37- existence + TTL inspection without paying the value-transfer
38- cost. 200 if present, 404 if not.
39- - ` GET /v1/cache/:key ` now honors ` Accept: application/json `
40- and returns an ` itemEnvelope ` with the same metadata as
41- HEAD plus the base64-encoded value. The bare-` curl ` default
42- remains raw bytes via ` application/octet-stream ` — current
43- clients are unaffected.
44- - ` POST /v1/cache/batch/{get,put,delete} ` enable bulk operations
45- in a single round-trip. Each request carries an array; the
46- response carries one result entry per item with per-item
47- status, owners, and error reporting. ` batch-put ` items
48- accept either UTF-8 strings (default) or base64-encoded byte
49- payloads via ` value_encoding: "base64" ` . Per-item errors are
50- surfaced in ` error ` + ` code ` fields without failing the
51- whole batch.
138+ - ` HEAD /v1/cache/:key` returns the value's metadata in
139+ ` X-Cache-*` response headers (Version, Origin, Last-Updated,
140+ TTL-Ms, Expires-At, Owners, Node) with no body — fast
141+ existence + TTL inspection without paying the value-transfer
142+ cost. 200 if present, 404 if not.
143+ - `GET /v1/cache/:key` now honors `Accept : application/json`
144+ and returns an `itemEnvelope` with the same metadata as
145+ HEAD plus the base64-encoded value. The bare-`curl` default
146+ remains raw bytes via `application/octet-stream` — current
147+ clients are unaffected.
148+ - ` POST /v1/cache/batch/{get,put,delete}` enable bulk operations
149+ in a single round-trip. Each request carries an array; the
150+ response carries one result entry per item with per-item
151+ status, owners, and error reporting. `batch-put` items
152+ accept either UTF-8 strings (default) or base64-encoded byte
153+ payloads via `value_encoding : " base64" ` . Per-item errors are
154+ surfaced in ` error` + `code` fields without failing the
155+ whole batch.
52156 Six unit tests at
53157 [cmd/hypercache-server/handlers_test.go](cmd/hypercache-server/handlers_test.go)
54158 pin the contracts : HEAD present/missing, Accept-JSON envelope
@@ -57,26 +161,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
57161- **SWIM self-refutation + cross-process gossip dissemination.**
58162 Closes the last `experimental` marker on the heartbeat path.
59163 Three pieces :
60- - ** ` acceptGossip ` self-refute** — incoming entries that
61- reference the local node as Suspect or Dead at incarnation
62- ≥ ours now bump the local incarnation and re-mark Alive.
63- Higher-incarnation-wins propagation in the same function
64- disseminates the refutation cluster-wide, so a falsely-
65- suspected node can clear suspicion through gossip alone
66- (pre-fix the only path was a fresh probe).
67- - ** HTTP gossip wire** — new ` Gossip(ctx, targetID, members) `
68- method on ` DistTransport ` , new
69- ` POST /internal/gossip ` server endpoint (auth-wrapped),
70- new ` GossipMember ` wire DTO. ` runGossipTick ` now falls
71- through to the HTTP path when the transport isn't an
72- ` InProcessTransport ` , so cross-process clusters disseminate
73- membership state — pre-Phase-E this was an in-process-only
74- no-op.
75- - The ` experimental ` qualifier is removed from
76- ` heartbeatLoop ` 's comment + the heartbeat-section field
77- doc; SWIM-style indirect probes (Phase B.1) and
78- self-refutation (this round) together provide the SWIM
79- properties the marker was tracking.
164+ - **`acceptGossip` self-refute** — incoming entries that
165+ reference the local node as Suspect or Dead at incarnation
166+ ≥ ours now bump the local incarnation and re-mark Alive.
167+ Higher-incarnation-wins propagation in the same function
168+ disseminates the refutation cluster-wide, so a falsely-
169+ suspected node can clear suspicion through gossip alone
170+ (pre-fix the only path was a fresh probe).
171+ - **HTTP gossip wire** — new `Gossip(ctx, targetID, members)`
172+ method on `DistTransport`, new
173+ ` POST /internal/gossip` server endpoint (auth-wrapped),
174+ new `GossipMember` wire DTO. `runGossipTick` now falls
175+ through to the HTTP path when the transport isn't an
176+ ` InProcessTransport` , so cross-process clusters disseminate
177+ membership state — pre-Phase-E this was an in-process-only
178+ no-op.
179+ - The `experimental` qualifier is removed from
180+ ` heartbeatLoop` ' s comment + the heartbeat-section field
181+ doc; SWIM-style indirect probes (Phase B.1) and
182+ self-refutation (this round) together provide the SWIM
183+ properties the marker was tracking.
80184 Regression coverage at
81185 [tests/integration/dist_swim_refute_test.go](tests/integration/dist_swim_refute_test.go):
82186 `TestDistSWIM_HTTPGossipExchange` exercises the wire (A pushes
0 commit comments