diff --git a/CHANGELOG.md b/CHANGELOG.md index 775f160..0632523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,30 +7,27 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ## [Unreleased] +## [0.5.0] - 2026-10-01 + ### Added -- `@authplane/sdk` — new `resourceMetadataUrl` option on `client.resource(...)` (and on every adapter) pointing the `resource_metadata` challenge parameter at a PRM document hosted elsewhere, such as the one authserver 0.2.0 serves per registered Resource; the default stays the URL derived from `resource`, and the locally served document and its route are unchanged. Validated at construction with a `TypeError`: absolute with a scheme and a host, `http`/`https` only, no fragment, no userinfo, an RFC 3986 §3.4 query, and no `"`, `\`, whitespace or C0 control anywhere — the value is advertised exactly as typed, so those octets would reach the `WWW-Authenticate` quoted-string. -- `@authplane/sdk` — new `@authplane/sdk/core` helper `validateResourceIndicator(resource)`: the RFC 8707 §2 resource-indicator gate the `AuthplaneResource` constructor runs, exported for callers that want to check a configured identifier ahead of construction. Throws `TypeError`; see the corresponding entry under **Changed** for what it rejects. -- `@authplane/sdk` — new `@authplane/sdk/core` helper `validateIssuerIdentifier(issuer)`, the issuer-side counterpart to `validateResourceIndicator`. -- `@authplane/sdk` — new `AccessDeniedError` (`access_denied`, 403: the exchanging client is not allowlisted on the target Resource — an operator fix, unlike `ConsentRequiredError`) and `InvalidTargetError` (`invalid_target`, 400, RFC 8707 §2.2: `resource` does not match a granted resource byte for byte), mapped by `mapOAuthError` and exported from `@authplane/sdk/core` and `@authplane/sdk/auth`. -- `@authplane/sdk` — new `@authplane/sdk/core` helper `wwwAuthenticateChallenges(error, { schemes, algs, ... })`: one RFC 6750 §3 header value per scheme, so a resource accepting both `Bearer` and `DPoP` can advertise both (RFC 9449 §7.2) and emit the §7.1 `algs` parameter, whose values are validated against the supported set rather than escaped. +- `@authplane/sdk` — new `resourceMetadataUrl` option on `client.resource(...)` and every adapter, pointing the `resource_metadata` challenge at a PRM document hosted elsewhere, such as authserver 0.2.0's per-Resource one. Validated at construction; the default stays the URL derived from `resource`. +- `@authplane/sdk` — new `@authplane/sdk/core` helpers `validateResourceIndicator(resource)` and `validateIssuerIdentifier(issuer)`, the construction-time gates, for checking configuration ahead of construction. +- `@authplane/sdk` — new `AccessDeniedError` (`access_denied`, 403: client not allowlisted on the target Resource) and `InvalidTargetError` (`invalid_target`, 400, RFC 8707 §2.2), mapped by `mapOAuthError`. +- `@authplane/sdk` — new `@authplane/sdk/core` helper `wwwAuthenticateChallenges(error, { schemes, algs, ... })`: one header value per scheme, so a resource can advertise both `Bearer` and `DPoP` with `algs` (RFC 9449 §7.1). - `@authplane/sdk` — new `@authplane/sdk/core` helper `errorResponseBody(error, options)`: the RFC 6750 §3 JSON error body, built from the error code and fixed description `wwwAuthenticate()` already uses. ### Changed -- `@authplane/sdk` — a resource indicator carrying a URI fragment is now rejected with a `TypeError` at construction (RFC 8707 §2) instead of being silently dropped, where it survived into the PRM document's `resource` member and made conformant clients discard it. **Migration**: drop the fragment before upgrading — a deployment configured with one now fails at startup. -- `@authplane/sdk` — a resource identifier must now be an absolute URL with a scheme and a host, rejected with a `TypeError` at construction otherwise (RFC 8707 §2, RFC 9728 §3). A relative `/mcp`, a scheme-relative `//api.example.com/mcp` or an opaque `urn:example:api` previously derived a malformed document URL. **Migration**: configure `resource` as a full URL; `http://localhost:8080/mcp` is still accepted. -- `@authplane/sdk` — the derived PRM document URL now preserves the resource identifier's query (RFC 9728 §3), and a query outside the RFC 3986 §3.4 grammar is rejected at construction, since those octets would corrupt the `resource_metadata` quoted-string. **Migration**: update any hard-coded expectation of the query-less URL, and percent-encode brackets, spaces and quotes in the configured `resource`. -- `@authplane/sdk` — a resource identifier whose authority carries userinfo is now rejected with a `TypeError` at construction (RFC 9110 §4.2.4). The credential was published to unauthenticated callers in the PRM document and the 401 challenge, and the derivations drop userinfo, so the document never matched the URL it was fetched from. A port, an IPv6 literal and an `@` in the path stay accepted. **Migration**: carry the credential in a header instead. -- `@authplane/sdk` — `buildPrm(issuer, resource, scopes)` now runs the same resource-identifier gate the `AuthplaneResource` constructor runs. It was the one public path that could still emit a document whose `resource` member conformant clients discard. Only direct callers of the export change. **Migration**: correct a fragment-bearing, relative, opaque, out-of-grammar or userinfo-bearing identifier before upgrading. -- `@authplane/sdk` — `buildPrm()` and `AuthplaneClient.create()` now reject an issuer carrying a query, fragment or userinfo component, or that is not an absolute URL with a scheme and a host (RFC 8414 §2/§3.1, RFC 9110 §4.2.4). **Migration**: `buildPrm` previously published the issuer verbatim in `authorization_servers`, credentials included — correct any such issuer before upgrading. -- **BREAKING** `@authplane/sdk` — an issuer identifier containing whitespace or a control character is now rejected with a `TypeError` by `AuthplaneClient.create()`, `buildPrm()` and `buildMetadataUrl()` (RFC 3986 §2). `new URL` silently trimmed or stripped the byte, so the `.well-known` location was derived from the cleaned string while the raw one was published in `authorization_servers` and compared as the expected `iss` (RFC 8414 §3.3) — every token rejected on an identity comparison that reads as identical in a log. The message names the codepoint and its offset, never the value. **Migration**: remove the stray byte before upgrading — a trailing newline in an environment variable is the usual source. -- `@authplane/sdk` — `access_denied` and `invalid_target` are now listed explicitly as OAuth answers that never trip the circuit breaker (a healthy AS enforcing policy is not an outage). -- `@authplane/sdk` — introspection revocation configured without complete AS credentials (`asCredentials`, or `clientId` + `clientSecret`) now warns at construction that authserver ≥ 0.1.2 answers unauthenticated introspection with `active: false` and every token will be rejected; the first `active: false` on a token that passed local JWT verification logs the runtime-client requirement, once per resource. -- Docs — token-exchange sections explain `access_denied` vs `consent_required` and the `PATCH /admin/resources/{id}` allowlist step; introspection sections state the RS client must be confidential and the issuing client or a runtime-client of the Resource; README gains a Compatibility section (tested against authserver 0.2.0, introspection-based revocation needs ≥ 0.1.2); demo docs and `scripts/manual-e2e-setup.sh` drop `AUTHPLANE_*_ENABLED=true` (on by default since authserver 0.2.0) and the setup script accepts `AUTHSERVER_REF`. -- `@authplane/sdk` — **BREAKING**: `wwwAuthenticate(error, options)` now emits a fixed `error_description` chosen by the `error=` code instead of the exception message, which disclosed the unknown `kid`, the rejected `typ` or the expected `aud` to an unauthenticated caller; sanitising prevented header injection, not disclosure. **Migration**: log `error.message` server-side, or pass `verboseDescription: true` to restore the old text for local debugging only. -- **BREAKING** `@authplane/mcp`, `@authplane/hono`, `@authplane/nestjs` — the JSON error body now carries the fixed `error_description` the challenge carries, not the exception message, and names `invalid_dpop_proof` where it previously said `invalid_token`. **Migration**: log `error.message` server-side; these adapters do not accept `verboseDescription`, which is a core-only escape hatch. -- **BREAKING** `@authplane/mcp` — `tokenVerifier.verifyAccessToken()` now rejects with the fixed per-code sentence instead of core's message, which the MCP SDK's `requireBearerAuth` splices into `error_description=`. The original error stays on `.cause`. +- **BREAKING** `@authplane/sdk` — a resource identifier must be an absolute URL with a scheme and host, with no fragment, no userinfo and an RFC 3986 query; anything else throws `TypeError` at construction, in `buildPrm()` too. **Migration**: configure the full URL clients use; `http://localhost:8080/mcp` is still accepted. +- `@authplane/sdk` — the derived PRM document URL now keeps the resource identifier's query (RFC 9728 §3). **Migration**: update any hard-coded expectation of the query-less URL. +- **BREAKING** `@authplane/sdk` — `AuthplaneClient.create()`, `buildPrm()` and `buildMetadataUrl()` reject an issuer with a query, fragment, userinfo, whitespace or control character, or without a scheme and host. **Migration**: fix the issuer; a trailing newline in an env var is the usual cause. +- `@authplane/sdk` — `access_denied` and `invalid_target` never trip the circuit breaker. +- `@authplane/sdk` — introspection revocation without complete AS credentials warns at construction, and the first `active: false` on a locally valid token logs the runtime-client requirement (authserver ≥ 0.1.2). +- Docs — token exchange explains `access_denied` vs `consent_required` and the allowlist step; introspection states the confidential / runtime-client requirement; README gains a Compatibility section; e2e scripts drop the `AUTHPLANE_*_ENABLED=true` flags and accept `AUTHSERVER_REF`. +- **BREAKING** `@authplane/sdk` — `wwwAuthenticate(error, options)` emits a fixed `error_description` per error code instead of the exception message. **Migration**: log `error.message` server-side, or pass `verboseDescription: true` for local debugging. +- **BREAKING** `@authplane/mcp`, `@authplane/hono`, `@authplane/nestjs` — the JSON error body carries the same fixed `error_description` as the challenge, and says `invalid_dpop_proof` where it said `invalid_token`. **Migration**: log `error.message` server-side. +- **BREAKING** `@authplane/mcp` — `tokenVerifier.verifyAccessToken()` rejects with the fixed per-code sentence; the original error stays on `.cause`. ### Deprecated @@ -38,12 +35,12 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Fixed -- `@authplane/sdk` — the SSRF guard no longer rejects every address when the SDK is loaded by Node's own ESM loader: `ipaddr.js` is CommonJS, so the namespace import resolved to an object without `parse`, the resulting `TypeError` was swallowed, and `isIpAllowed()` returned `false` for all issuers (only `ssrfProtection: false` got past it). The import is now a default import, a `TypeError` from the import shape propagates instead of being read as "blocked", and a test loads the built module under `node` itself, since vitest's interop hid the defect. -- `@authplane/sdk` — a failed metadata or JWKS fetch now opens a retry floor of `fetchFailureBackoffSeconds` (new `AuthplaneClient.create()` option, default `30`, clamped to `[1s, the cache's refresh interval]`; opt out with a refresh interval of `0`). **Impact:** on a cold start against an unreachable AS every `verify()` now fails fast with the stored error until the window closes, even if the AS recovers inside it; cached documents keep being served, and the first suppressed attempt per window logs a warning naming the document. -- `@authplane/sdk` — a resource server that only verifies tokens now re-reads the authorization server metadata once `metadataRefreshSeconds` elapses, and follows a rotated `jwks_uri`. Previously metadata was read once at `create()` and the JWKS kept being fetched from the URI captured then, until the AS withdrew it and every token failed. A `kid` miss forces a re-read, floored at one per `min(metadataRefreshSeconds, 60)` seconds; `metadataRefreshSeconds: 0` opts out of both the refresh interval and that floor. -- `@authplane/sdk` — a server expiry at or before the moment the document was cached (`Cache-Control: max-age=0`, or a stale `Expires:` header) is now treated as no preference, so the configured refresh interval governs. Previously it left the metadata or JWKS document permanently expired, so every verification took the synchronous re-fetch path — one upstream fetch per request, driven by an unauthenticated caller, since the cache is read before the signature is checked. A future server expiry still shortens the interval. -- `@authplane/sdk` — the issuer and resource gates now quote and escape the identifier they echo, so a rejection can no longer carry a raw control character into a startup log; the redaction itself is unchanged, and a `\` or `"` in an echoed identifier now appears as its JSON escape. -- `@authplane/mcp` — `requireScope(scope, authInfo)` now throws core `InsufficientScope` carrying the missing scope instead of a plain `Error`, so a host that maps `AuthplaneError` answers 403 with an `insufficient_scope` challenge naming that scope rather than a JSON-RPC internal error or a generic 500. +- `@authplane/sdk` — the SSRF guard no longer rejects every address when the SDK is loaded by Node's native ESM loader. +- `@authplane/sdk` — a failed metadata or JWKS fetch opens a retry floor of `fetchFailureBackoffSeconds` (new option, default `30`). **Impact:** against an unreachable AS, `verify()` fails fast until the window closes; cached documents keep serving. +- `@authplane/sdk` — a verify-only resource server re-reads AS metadata every `metadataRefreshSeconds` and follows a rotated `jwks_uri`; a `kid` miss forces a re-read at most once per `min(metadataRefreshSeconds, 60)` seconds. +- `@authplane/sdk` — a server expiry at or before caching time (`max-age=0`, stale `Expires:`) no longer leaves the document permanently expired, which caused one upstream fetch per request. +- `@authplane/sdk` — the issuer and resource gates quote and escape the identifier they echo, so a rejection cannot carry a raw control character into a log. +- `@authplane/mcp` — `requireScope(scope, authInfo)` throws core `InsufficientScope`, so hosts answer 403 `insufficient_scope` naming the scope instead of a generic 500. - `@authplane/sdk` — `InsufficientScope` now carries the optional `requiredScopes` it was raised for, and `wwwAuthenticate()` falls back to it for `scope="…"` when the caller passes none; an explicit `scope` option still wins. - `@authplane/sdk` — a DPoP proof nonce mismatch no longer names the expected or the received nonce, so the RFC 9449 §9 resource-server nonce is no longer handed back in the 401 challenge it was rejected by.