@payez/next-mvp 4.9.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/api-handlers/auth/refresh.d.ts +17 -56
  3. package/dist/api-handlers/auth/refresh.js +71 -696
  4. package/dist/auth/utils/token-utils.d.ts +1 -1
  5. package/dist/auth/utils/token-utils.js +3 -2
  6. package/dist/lib/api-handler.js +4 -3
  7. package/dist/lib/ensure-fresh-access-token.d.ts +24 -1
  8. package/dist/lib/ensure-fresh-access-token.js +255 -29
  9. package/dist/lib/idp-fetch.js +3 -2
  10. package/dist/lib/idp-logout.d.ts +4 -2
  11. package/dist/lib/refresh-in-doubt.d.ts +31 -0
  12. package/dist/lib/refresh-in-doubt.js +81 -0
  13. package/dist/lib/refresh-rotation.d.ts +19 -0
  14. package/dist/lib/refresh-rotation.js +18 -0
  15. package/dist/lib/refresh-token-validator.js +2 -1
  16. package/dist/lib/refresh-window.d.ts +16 -0
  17. package/dist/lib/refresh-window.js +19 -0
  18. package/dist/lib/safe-trace-id.d.ts +11 -0
  19. package/dist/lib/safe-trace-id.js +23 -0
  20. package/dist/lib/session-store.d.ts +0 -5
  21. package/dist/lib/session-store.js +28 -1
  22. package/dist/lib/token-lifecycle.js +3 -2
  23. package/dist/models/SessionModel.d.ts +7 -0
  24. package/dist/routes/auth/refresh.d.ts +1 -14
  25. package/dist/server/auth.d.ts +1 -1
  26. package/dist/stores/authStore.js +3 -2
  27. package/package.json +3 -12
  28. package/scripts/verify-release-provenance.mjs +1 -0
  29. package/src/api-handlers/auth/refresh.ts +72 -736
  30. package/src/auth/utils/token-utils.ts +3 -2
  31. package/src/lib/api-handler.ts +4 -3
  32. package/src/lib/ensure-fresh-access-token.ts +275 -30
  33. package/src/lib/idp-fetch.ts +3 -2
  34. package/src/lib/idp-logout.ts +4 -2
  35. package/src/lib/refresh-in-doubt.ts +79 -0
  36. package/src/lib/refresh-rotation.ts +18 -0
  37. package/src/lib/refresh-token-validator.ts +2 -1
  38. package/src/lib/refresh-window.ts +16 -0
  39. package/src/lib/safe-trace-id.ts +21 -0
  40. package/src/lib/session-store.ts +31 -1
  41. package/src/lib/token-lifecycle.ts +3 -2
  42. package/src/models/SessionModel.ts +7 -0
  43. package/src/server/auth.ts +1 -1
  44. package/src/stores/authStore.ts +3 -2
  45. package/dist/api/auth-handler.d.ts +0 -64
  46. package/dist/api/auth-handler.js +0 -402
  47. package/dist/api/index.d.ts +0 -10
  48. package/dist/api/index.js +0 -19
  49. package/src/api/auth-handler.ts +0 -551
  50. package/src/api/index.ts +0 -18
package/CHANGELOG.md ADDED
@@ -0,0 +1,136 @@
1
+ # Changelog — @payez/next-mvp
2
+
3
+ ## 4.11.0 (to be published under dist-tag `next` only, after 4.10.0; `latest` stays 4.8.9 until Jon says otherwise)
4
+
5
+ Carries PRs 1443, 1444, 1445, 1446, 1451, 1452, 1460 and 1461 (and the 1450 design doc): the refresh consolidation (the refresh route is now a thin wrapper over `ensureFreshAccessToken`), the one freshness window, the fetch timeout with the in-doubt marker, and the trace-id cleanup. A minor, because response codes and bodies that callers may match on changed (BREAKING below).
6
+
7
+ - **BREAKING (for callers that match on refresh error codes, wording or bodies; BAPert 76495 item 4).** Each is detailed in the entries below: (1) a Better-Auth-only session answers 401 `NO_CANONICAL_SESSION` (terminal) where it answered `NO_REFRESH_TOKEN`; (2) a pending-2FA row answers 401 `NO_TOKEN_PENDING_2FA` and is no longer refreshed; (3) new answers `504 UPSTREAM_TIMEOUT` and `401 REFRESH_OUTCOME_UNKNOWN` (a refresh whose outcome is unknown is never replayed); (4) a non-envelope IdP answer on the refresh route is `502 Non-compliant IDP envelope` (was `Upstream non-compliance`); the route's `IDP service unavailable` and `Invalid response from IDP` wording is unchanged (only `ensureFreshAccessToken`'s own result messages changed, see its entry below); (5) the `409 CONFLICT` body gains `retryable: true`; (6) **a bare 401 whose body is not an IdP envelope no longer discards the token** (only the IdP's own verdict does: a typed error with `discard_token`, or the `IDP_DISCARD_TOKEN` path; a bare 401 comes from a proxy or gateway, and DotNetPert 76454 shows every Core refresh 401 is an envelope). Also, a refresh whose result could not be stored answers the terminal `SESSION_PERSIST_FAILED` where the route used to answer a generic 500. A caller that matched the old strings or codes must be updated.
8
+
9
+ - **FIX: the caller's trace id is cleaned before it is logged (PAY-2376).** `ensureFreshAccessToken` logged `options.requestId` and the refresh route passed the `x-request-id` header into it: attacker-chosen text in the log sink (a newline forges a line, a huge value floods it). New `lib/safe-trace-id` `sanitizeTraceId` removes control characters (C0, DEL, C1, U+2028/U+2029), caps the length at 64 and turns anything that is not a non-empty string into `undefined`; both the route and E use it. Log correlation only: it was never the lock identity. Rows: `test/next-mvp-4910-trace-id-sanitize.test.cjs`.
10
+ - **CHANGE: the last 5-minute freshness literals are on the one refresh window (PAY-2374).** `lib/api-handler.ts` (three sites), `lib/token-lifecycle.ts` (`REFRESH_THRESHOLD_MS`), `auth/utils/token-utils.ts` (`tokenNeedsRefresh`'s default `bufferMs`) and `stores/authStore.ts` (post-rehydration freshness check) and `lib/refresh-token-validator.ts` (`checkRefreshViability`'s buffer, in seconds) now use `REFRESH_WINDOW_MS` from `lib/refresh-window`, the value the refresh route, `ensureFreshAccessToken` and `idp-fetch` already use. No value changes today (all were 5 minutes); the point is that they can no longer drift apart. Cache TTLs and the circuit breaker are not freshness and are left alone. Rows: `test/next-mvp-4910-refresh-window-literals.test.cjs`.
11
+ - **CHANGE: `POST /api/auth/refresh` (`api-handlers/auth/refresh`) is now a thin wrapper over `ensureFreshAccessToken` (BAPert 76449, R-over-E PR 2).** One implementation talks to the IdP, holds the refresh lock and writes the tokens; the route keeps what only an HTTP request has: resolving the session from the Better Auth cookie, the `x-session-token` ownership rejection, the `x-request-id` trace id, and the mapping of E's typed result onto the route's existing JSON bodies and status codes. Success bodies are unchanged (`{ refreshed, [reason: 'already_fresh' | 'completed_by_concurrent_request'], accessTokenExpires, hasRefreshToken }`; the access token is never in a body), as are the bodies for `UNAUTHORIZED`, `NO_REFRESH_TOKEN`, `CONFLICT` and the upstream failures. **What changes for a caller of the route:**
12
+ - A **Better-Auth-only session** (no canonical row) now answers 401 `NO_CANONICAL_SESSION` (terminal) instead of `NO_REFRESH_TOKEN`; and a session whose row vanished while the request waited for the lock answers 401 `UNAUTHORIZED` (no session).
13
+ - A row with a refresh token but **no access token** is refreshed only when it is not 2FA-pending; a **pending-2FA** row now answers `NO_TOKEN_PENDING_2FA` instead of being refreshed (refresh is never a way around 2FA).
14
+ - The IdP envelope rule is E's (no `meta` requirement): a response that is not an envelope is a 502 `Non-compliant IDP envelope` (was `Upstream non-compliance`), and a **bare 401 whose body is not an envelope no longer discards the token**.
15
+ - The `409 CONFLICT` body gains `retryable: true`; the lock identity prefix is `ensure_fresh_` (not observable). The window (5 min), the fetch timeout, the in-doubt marker, the dropped-persist answer and the `success:false` meaning are the ones already released, now in one place.
16
+ - Tests: the rows that broke the route's source now break `ensureFreshAccessToken`'s (the same defects, loaded through the real route); new `test/next-mvp-4910-refresh-route-wrapper.test.cjs` pins the wrapper's own mapping, the ownership rejection and the refresh-token requirement. The mutants are loaded through a per-process module hook (`test/lib/swap-dist.cjs`), never written over shipped `dist` files (the test files run in parallel processes).
17
+
18
+ - **FIX + CHANGE: `ensureFreshAccessToken` brought to what the refresh route does, so the route can become a thin wrapper over it (BAPert 76449, R-over-E PR 1; E only).**
19
+ - **FIX (twin of the route's 1432 fix): the write-back could put a SPENT refresh token back.** E SENT the post-lock token but its write-back fallbacks (`idpRefreshToken` when the IdP answer carries no new refresh token, `idpRefreshTokenExpires`, `bearerKeyId`, `authenticationMethods`/`authenticationLevel` and the MFA timing fields) read the PRE-lock session, and it spread the whole pre-lock row over the canonical row. If a concurrent request rotated the token while this one waited for the lock and the IdP omitted `refresh_token`, the rotated token was overwritten by the spent one (a dead stored credential), and a concurrent writer's change to any other field was overwritten too. The fallbacks now read the post-lock canonical row, the stored token is the one that was SENT, and only the refreshed fields are written (`updateSession` merges).
20
+ - **CHANGE: the result carries a `reason`** (`'already_fresh' | 'refreshed' | 'completed_by_concurrent_request'`), additive. `refreshed` is unchanged (it is `true` for both the last two). The route's response bodies, and `lib/token-lifecycle`'s `reason === 'already_fresh'`, are built from it.
21
+ - **CHANGE: a row with a refresh token but NO access token is refreshed when it is not 2FA-pending** (as the route always did); it used to answer `NO_TOKEN`. A pending-2FA row keeps `NO_TOKEN_PENDING_2FA` even with a refresh token (refresh is never a way around 2FA); no refresh token is `NO_TOKEN` as before.
22
+ - **CHANGE: `persisted` means the canonical row.** A token served from the Better Auth fallback record is `persisted: false` (it used to say `true`). Nothing in this package reads it.
23
+ - **CHANGE: `ensureFreshAccessToken`'s result messages now use the route's wording (the route's own wording did not change).** A transport failure answers `'IDP service unavailable'` (it returned the raw error text, which can carry host names and socket details) and an unparseable non-2xx answer `'Invalid response from IDP'` (it answered `'Invalid JSON from IDP'`). Callers of `ensureFreshAccessToken` that matched the old `message` text must be updated; the `code`s are unchanged.
24
+ - **CHANGE: the IdP envelope rule (BAPert 76455 (B)): no `meta` requirement.** `success` must be a boolean; `success:true` needs `data.access_token` (a string); `success:false` needs `error.code` (a string); `meta`, `request_id` and `time_stamp` are optional; anything else (including `success:true` on a non-2xx, or a non-2xx body that is not an envelope) is a 502 `Non-compliant IDP envelope`, retryable, and DISCARDS NOTHING. Core's four hint-carrying error returns on `POST /api/ExternalAuth/refresh` carry no `meta` (they are the answers that carry `discard_token` / `retryable`), so they are mapped by their code and flags as before. A non-2xx whose body is not an envelope used to be treated by its status alone (a bare 401 discarded the token); it no longer is. Only Core's own verdict (a typed error with `discard_token`, or the `IDP_DISCARD_TOKEN` code path) proves the grant is dead; a bare 401 comes from a proxy or gateway, not from Core's refresh decision (DotNetPert 76454: every Core refresh 401 is an envelope), so it answers the 502 with the token kept and is not in doubt (BAPert 76480).
25
+ - Rows: `test/next-mvp-4910-ensure-fresh-r-parity.test.cjs` (a mutant per change).
26
+
27
+ - **FIX: the IdP refresh fetch has a timeout, and a timed-out refresh is never replayed (BAPert 76397 decision 3, corrected by 76403).** Neither the refresh route nor `ensureFreshAccessToken` had a fetch timeout. Core consumes a refresh token with one atomic update and has no reuse grace, and a replay revokes EVERY newer refresh token the user holds, on every device. So a refresh whose answer never arrived is outcome-UNKNOWN: Core may have consumed the token and minted a replacement we never saw, and the store still holds the token we sent. Now: the fetch carries `AbortSignal.timeout(10 s)` (`REFRESH_FETCH_TIMEOUT_MS`, covering the body read too); a timeout answers **504 `UPSTREAM_TIMEOUT`** (`outcomeUnknown: true`, `retryable: false`, `discardToken: false`; `ensureFreshAccessToken` returns the same code with `status: 504`) and writes onto the session row `refreshInDoubtTokenHash`, a SHA-256 of the token that was sent (never the token), and `refreshInDoubtAt`. Any later attempt that would send that same token **refuses without calling the IdP**: terminal 401 **`REFRESH_OUTCOME_UNKNOWN`** (`resolution: 'REAUTHENTICATE'`, `retryable: false`, `discardToken: false`) from both implementations. One login on one session instead of a user-wide revocation. The marker is dropped by the same `updateSession`/`setSession` write that stores a DIFFERENT refresh token; a write that stores the same token keeps it. Nothing tries to resolve the doubt without a login. This also closes the old replay: a hung fetch used to hold the lock for its 60 s TTL and then the next request re-sent the same token. **What changes for you:** a refresh that used to hang now answers 504 after 10 s; consumers that switch on `code` see `UPSTREAM_TIMEOUT` and, afterwards, `REFRESH_OUTCOME_UNKNOWN` (both new). **Any transport failure is outcome-unknown too** (BAPert 76433): a connection reset after the request was written looks the same as a timeout from here. It takes the same path (504 `UPSTREAM_TIMEOUT` plus the marker) UNLESS its cause code proves the request never left: the allowlist is exactly `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN` (read from the error, its `cause` and an AggregateError's members; EVERY code must be on the list). Those stay 503 `UPSTREAM_SERVICE_UNAVAILABLE`, retryable, with no marker. `ECONNRESET`, `UND_ERR_SOCKET`, an error with no code and a mixed list are in doubt; a failure while reading the response body (not a JSON syntax error) is in doubt as well, and so is a **2xx whose body is empty or does not parse** (BAPert 76441: a 2xx means Core processed the grant and consumed the token, so the replacement is lost). A **non-2xx** whose body is not JSON (a gateway HTML 502/503) did not mint and stays a completed failure: 502 `UPSTREAM_SERVICE_ERROR`, no marker. A false in-doubt costs one re-login; a missed one costs a user-wide revocation. Do not add a code to the list without proof it can only occur before the request is sent. The `SessionData` type gains `refreshInDoubtTokenHash` and `refreshInDoubtAt`. Rows: `test/next-mvp-4910-refresh-timeout-in-doubt.test.cjs` (no marker write -> the replay is sent; marker never cleared -> RED; signal removed -> RED).
28
+
29
+ - **CHANGE: one freshness window, 5 minutes, for the refresh route, `ensureFreshAccessToken` and `lib/idp-fetch` (BAPert 76397 decision 1).** They disagreed: the route and `idp-fetch` used 5 minutes, `ensureFreshAccessToken` used 60 seconds, so a session could be stale to one and fresh to another and two callers rotated a token the other had just rotated. They now share `REFRESH_WINDOW_MS` (`lib/refresh-window`). Core's access-token lifetime is a fixed 15 minutes, so 5 minutes is the ruled value (lifetime / 3 if it were shorter). **What changes for you:** callers of `ensureFreshAccessToken` (`getIdpToken` in `server/auth.ts`, the OAuth resume handler) now refresh up to 4 minutes earlier than before, so slightly more IdP refreshes per token lifetime and never a staler token; the `safetyWindowMs` option still exists and still wins when passed. The route's and `idp-fetch`'s behaviour is unchanged. Row: `test/next-mvp-4910-one-refresh-window.test.cjs` (each path probed at 6 and 4 minutes left, a mutant per path).
30
+ - **FIX: `ensureFreshAccessToken` no longer spends a refresh token it cannot store (BAPert 76397 Finding 1).** It READ a session through the Better Auth fallback but PERSISTED only into the canonical session row (`updateSession` returns `null` when that row is absent). For a session that lives only under the Better Auth key it therefore called the IdP, which rotated and spent the refresh token, could not store the answer, and returned `SESSION_PERSIST_FAILED` with the replacement token lost. It now requires the canonical row BEFORE the IdP call: no row -> no IdP call and a typed terminal answer, 401 `NO_CANONICAL_SESSION` (`terminal`, `discardToken: false`, `retryable: false`, `resolution: 'REAUTHENTICATE'`; the Better Auth record is left alone). `server/auth.ts` forwards `result.code`, so a consumer that switches on codes sees the new one for these sessions, where it saw `SESSION_PERSIST_FAILED` after a spent token. `SESSION_PERSIST_FAILED` remains for a row that disappears after the check. Row: `test/next-mvp-4910-ensure-fresh-canonical-row.test.cjs` (with the check removed it goes RED and the token is lost); the 320072 file now makes the row vanish during the IdP call.
31
+
32
+ - **FIX: `POST /api/auth/refresh` no longer ignores a dropped persist (BAPert 76392 item 2).** `updateSession` returns `null` when the canonical session row is gone. The refresh route ignored that, so after the IdP exchange had already spent the old refresh token it answered a generic 500 'No access token after refresh'. It now answers 401 `SESSION_PERSIST_FAILED` (`discardToken: true`, `retryable: false`, `resolution: 'REAUTHENTICATE'`), the same code and meaning `ensureFreshAccessToken` has answered since 320072, and logs the drop. Row: `test/next-mvp-4910-refresh-persist-failed.test.cjs` (with the check removed the row goes RED).
33
+
34
+ ## 4.10.0 (to be published under dist-tag `next` only, after 4.9.0; `latest` stays 4.8.9 until Jon says otherwise)
35
+
36
+ Carries PRs 1432, 1435 and 1438. A minor, not a patch, because a removed export breaks importers (BREAKING below).
37
+
38
+ - **FIX: a refresh sends only the token read AFTER it holds the lock (BAPert 76298 (b2); PR 1432).** `api-handlers/auth/refresh` read the session before the lock and sent that read's refresh token; if another request rotated the token in between and the row it left was not fresh by the handler's 5-minute test, the handler sent a SPENT token, and the IdP treats reuse as compromise. It now sends `latestAfterLock.idpRefreshToken`, as `ensureFreshAccessToken` already did. A session row that vanished while waiting for the lock answers the 'No session available' 401; a row with no refresh token (for example one the IdP just discarded) answers the terminal `NO_REFRESH_TOKEN`, with NO IdP call and the row untouched. When the IdP's answer carries no new refresh token, the write-back keeps the token that was SENT.
39
+ - **FIX: one meaning for an IdP answer of HTTP 200 with `success:false`, in both refresh implementations (BAPert 76298 (c); PR 1435).** `refresh.ts` used to answer the IdP's own status, i.e. HTTP 200 with an error body, so `internalRefresh` reported `ok` and `idpFetchJSON`'s retry trusted a refresh that had failed; it also wrote a different marker (`IDP_SUCCESS_FALSE_DISCARD`) than the 401 branch, which the logout dead-grant check does not recognise. `ensureFreshAccessToken` never discarded. Now, in both: `success:false` WITH `discard_token` discards the stored token (the same compare-and-skip as the 401 branch) with the `IDP_DISCARD_TOKEN:<code>` marker and answers 401; WITHOUT it, a retryable 502 with the token KEPT. If a concurrent request already rotated the token, the discard is skipped and the fresh result is returned. The old marker `IDP_SUCCESS_FALSE_DISCARD` is no longer written (old rows that carry it are not rewritten).
40
+ - **FIX: the "already rotated" branches no longer count an expired access token as fresh (PR 1435).** When a refresh was answered 401 or `success:false` and the stored refresh token differs from the one sent, a concurrent request rotated it. The handler used to treat any row with an access token present as a completed refresh, so a rotated row whose access token was already EXPIRED was answered as a success carrying an expired token. One shared predicate (`lib/refresh-rotation` `rotatedRowIsUsable`: access token present AND `idpAccessTokenExpires > now`) now applies to all four such branches (both implementations, each for the 401 and for `success:false`). A rotated row that is not usable (expired, or no access token, which the 401 branches used to answer as an upstream 401) now answers the retryable 502, with the rotated token kept and no marker written. A 401/`success:false` against a token that is still the stored one is unchanged.
41
+ - **FIX: storing a refresh token clears a stale discard marker (PR 1435).** `session-store` `updateSession` and `setSession` drop `refreshTokenClearedReason` and `refreshTokenClearedAt` whenever a non-empty `idpRefreshToken` is stored, so a session that was discarded and then re-established cannot be mistaken later for a dead grant by the logout check.
42
+ - **BREAKING: `@payez/next-mvp/api/auth-handler` and `@payez/next-mvp/api` are REMOVED** (PR 1438: the exports-map entries, `src/api/auth-handler.ts`, and the `createAuthHandler` / `AuthContext` / `AuthHandlerOptions` / `HandlerFunction` exports with the module's default export). It was a third refresh implementation that read the legacy session field names (`refreshToken`, `accessToken`, `accessTokenExpires`) which the session store does not write, so it could not refresh a real session; it had no `discard_token` handling and its own lock. Zero importers on the live portal head (Scout 76371), and `npx payez init` no longer names it (`@payez/cli` fix, PR 1436). Use `createRefreshHandler` (`api-handlers/auth/refresh`) or `ensureFreshAccessToken` (`lib/ensure-fresh-access-token`). The PAY-1427 "upstream body not logged" row that covered it now covers those two.
43
+ - **Packaging: `CHANGELOG.md` is now in the published `files`.** `lib/idp-logout` (new in 4.9.0) is internal: it is NOT in the exports map and is not a supported import path.
44
+ - Rows: `test/next-mvp-4900-refresh-sends-token-read-after-lock.test.cjs`, `test/next-mvp-4900-one-meaning-and-marker.test.cjs` (with mutants M1-M6), the updated `test/upstream-body-not-logged-outside-api-handlers.test.cjs`, and `packages/cli/test/init-auth-mount.test.mjs`. Core and the IdP are contract doubles in them; no live Core, Redis or IdP run.
45
+
46
+ ## 4.9.0 (to be published under dist-tag `next` only; `latest` stays 4.8.9 until Jon says otherwise)
47
+
48
+ - **BREAKING: `SESSION_NOT_ENDED_MESSAGE` is REMOVED from `routes/auth/logout` (no alias kept; an import error is the loud failure).** Use `SESSION_NOT_ENDED_IDP_MESSAGE` / `SESSION_NOT_ENDED_LOCAL_MESSAGE` and the typed `cause` (`SessionNotEndedCause`) on the `SESSION_NOT_ENDED` 500. This is why the release is a minor, not a patch: a patch must not break an importer.
49
+ - **The sentence in the 4.8.9 entry below ("...Try signing out again.") is SUPERSEDED by this release.** The 4.8.9 entry is left as it was.
50
+ - **FIX: the `ensureFreshAccessToken` refresh lock id is server-generated (BAPert 76298 (b); PR 1430).** It used to be the caller's `requestId`, and `server/auth.ts` passed the request's `x-request-id` header, with a branch that proceeded WITHOUT the lock when the lock holder's id equalled it, so a request header could skip the lock that protects against refresh-token reuse (the IdP treats reuse as compromise). The lock id is now `ensure_fresh_<randomUUID>`, the equality branch is deleted, `server/auth.ts` no longer passes the header, and the caller's `requestId` option is kept for log correlation only (`traceId`). Rows: `test/next-mvp-4810-ensure-fresh-lock-id.test.cjs`; with the old code restored, two concurrent callers sharing one id both refresh at the IdP.
51
+ - **FIX: `POST /api/auth/logout` now ends the IdP grant, not only the portal session (BAPert 76255 item 1; PR 1428).** Until 4.8.9 the route deleted the local session and made NO call to the IdP although its own comment said it revoked tokens there, so the IdP refresh token stayed valid until it expired. It now calls Core `POST /api/Account/logout` with the session's refresh token in the body (`{refresh_token}`, Bearer) BEFORE deleting the local session. The token is always sent: Core revokes the caller's NEWEST session when it is omitted. A session with no IdP refresh token (a provisional, pre-2FA session) makes no call.
52
+ - **Outcomes.** The IdP answered 2xx: signed out. The revoke was refused AND the refresh that followed was answered 401 AND the session carries the IdP's own discard marker (`refreshTokenClearedReason` starting `IDP_DISCARD_TOKEN`): the grant is already dead, which is a successful sign-out. Anything else unknown (transport error, 5xx, a refresh that failed any other way, an empty token without the marker) is a failure.
53
+ - **CHANGE: the `SESSION_NOT_ENDED` answer.** 4.8.9 exported `SESSION_NOT_ENDED_MESSAGE` from `routes/auth/logout` and its sentence promised a retry. That export is REMOVED. The 500 body keeps `code: 'SESSION_NOT_ENDED'` and gains a typed `cause`: `'idp_unconfirmed'` (the IdP outcome is unknown; `SESSION_NOT_ENDED_IDP_MESSAGE` = "We could not confirm the sign-out with the identity service.") or `'local_delete_failed'` (the local store delete failed, also when both fail; `SESSION_NOT_ENDED_LOCAL_MESSAGE` = "We could not finish signing you out on this device."). Neither sentence promises a retry. The local session and the browser's cookies are cleared on every path. Neither success answer (clean, or dead grant) carries a `cause`. Exports from `routes/auth/logout`: `SESSION_NOT_ENDED_CODE`, `SESSION_NOT_ENDED_IDP_MESSAGE`, `SESSION_NOT_ENDED_LOCAL_MESSAGE`, type `SessionNotEndedCause`. A consumer keyed on `code` alone keeps working; one that imported `SESSION_NOT_ENDED_MESSAGE` must switch to the two constants or to `cause`.
54
+ - **FIX (inside the same change): a refresh no longer retries with a stale body.** `idpFetchJSON` takes an optional fourth argument `{ initFor(sessionToken) }` that builds the request AFTER its own pre-call refresh and again after the refresh that follows a 401/403, because a refresh ROTATES the refresh token; its result gains `skipped` and `refreshStatus`. Absent the option, behaviour is unchanged. New module `lib/idp-logout` (`revokeSessionAtIdp`).
55
+ - Rows: `test/next-mvp-4810-logout-revokes-at-idp.test.cjs`, `test/next-mvp-4810-logout-retry-names-new-token.test.cjs`, and the updated `test/next-mvp-489-logout-honest.test.cjs` (30 in all). Core is a contract double in them; no live Core run. Mutants RED: revoke skipped, a failed revoke swallowed, the body built once, a status-only dead-grant check, an empty-token dead-grant check, the cause hard-coded either way.
56
+ - Known, not in this release: `api-handlers/auth/signout.ts` still makes no IdP call.
57
+
58
+ ## 4.8.9 (to be published under dist-tag `next` only)
59
+
60
+ - **FIX: `POST /api/auth/logout` no longer says success when the server session was not deleted (BAPert 76110 item 3).** A failed `deleteSessionEverywhere` was caught, logged as a warning, and the route still answered `{success:true,message:'Logged out successfully'}` although the server-side session was still live. Now the cookies are still cleared (the browser is signed out either way), but the answer is a typed failure: status 500, `{success:false, code:'SESSION_NOT_ENDED', message:'You were signed out on this device, but we could not end your session on the server. Try signing out again.'}`, and the log line is an error that says the server session is STILL LIVE. The thrown text is never returned. `SESSION_NOT_ENDED_CODE` and `SESSION_NOT_ENDED_MESSAGE` are exported from the route module. A caller that read only `success` now sees `false`; the no-session answer is unchanged.
61
+ - Rows: `test/next-mvp-489-logout-honest.test.cjs` (7): the real route, a store that throws, status + code + the fixed sentence and nothing else, every session cookie expired, logged at error, recovery with a working store, no-session unchanged. Mutants RED: the failure swallowed again, cookies not cleared on failure, the log downgraded to a warning, the sentence changed.
62
+
63
+ ## 4.8.8 (item 24; to be published under dist-tag `next` only)
64
+
65
+ - **CHANGE: the app requires `IDP_URL` and refuses to START without it (Jon, item 24: "fast and loud are how i like my config errors").** New leaf export `@payez/next-mvp/lib/require-idp-url` with `requireIdpUrl()`: when `IDP_URL` is missing, blank, or not a full http(s) address it prints one fixed sentence ("IDP_URL is not set to a full http(s) address. This app has no default sign-in service: set IDP_URL in .env.local and start again.") and exits 1; with a valid value it returns true and prints nothing. There is no default in code and there never was one in the runtime (`src/config/env.ts` is a bare passthrough); four route headers that claimed "default: http://localhost:32785" now say REQUIRED. The quickstart generator (next release of `@payez/create-next-app`) writes the production sign-in service address into the generated app's `.env.local` at generation time and adds `src/instrumentation.ts`, which calls this helper, so a generated app with the value removed does not come up. Rigs override the file with their own `IDP_URL`.
66
+ - **Consumers (idealresume, idealvibe, the portal):** nothing changes until you call `requireIdpUrl()` from your own `instrumentation.ts`; the package does not call it for you. Calling it is the way to get the loud start.
67
+ - Rows: `test/next-mvp-488-require-idp-url.test.cjs` (8): a real child process refuses with exit 1, the fixed sentence and nothing else for unset / empty / blank / scheme-less / ftp / host-less values, and starts (exit 0, silent) with a valid one; no source file falls back to a host. Mutants RED: guard removed, a scheme-less value accepted, exit not called, the sentence changed.
68
+
69
+ ## 4.8.7 (to be published under dist-tag `next` only; the first publish under the new health check)
70
+
71
+ - **FIX: the onboarding proxies forwarded an EMPTY body (QAPert 75864; present since 4.5.0, PAY-2121, through 4.8.6).** `api-handlers/onboarding/{verify-welcome-email,send-email-code,verify-code,set-password}` read the request body with `req.json()` to check their fields, then `lib/onboarding-proxy` read the SAME request again with `req.text()`. A Request body is single-read: the second read threw "Body has already been read", the proxy swallowed it and POSTed `{}` to the IdP, so every onboarding step from a generated app reached Core with no token (Core answered 400 "The OnboardToken field is required") and no generated app could onboard an invitee end to end. `onboardingProxy(req, idpPath, body)` now takes the handler's already-parsed body and never re-reads the request; a caller that does not pass an object body gets a loud 500 `BODY_NOT_FORWARDED` and nothing reaches the IdP (it used to become `{}`).
72
+ - **FIX: the onboarding code step no longer claims a code was emailed when none was.** It says "We emailed a 6-digit code" only after `send-email-code` answered 2xx (`codeStepSentence`); a failed send says "We could not send a code" and points at "Send a new code".
73
+ - Rows: `test/next-mvp-487-onboarding-body.test.cjs` runs all four handlers on a REAL `Request` (a double whose `text()` still works after `json()` cannot catch this class, and did not, through four versions) plus a row that the body-forwarding helpers never hand a double to a handler; the older `pay-2121` handler rows now use a real Request too. Mutants RED: the proxy re-reading the consumed request, the swallow restored, a handler that stops passing its body, a failed send that reads as sent. Package suite 1210 tests, 1208 pass, 0 fail, 2 skipped.
74
+
75
+ ### Tooling in 4.8.7
76
+
77
+ - **TOOLING: the postpublish health check now polls the registry for the dist-tag AND the integrity (PR 1386, BAPert 75831).** It passes only when the dist-tag points at the version just published and the registry's `dist.integrity` equals the integrity of `npm pack --dry-run --json --ignore-scripts`; a different integrity fails at once, a timeout fails and prints what it saw and what it wanted, an unreadable registry exits 2 (inconclusive). The bound defaults to 10 minutes (the registry was stale 5-7 minutes after every publish 4.8.3-4.8.6): `--timeout-ms <n>` or `PUBLISH_HEALTH_TIMEOUT_MS` change it, 0 means one read. The `postversion` hook no longer runs the check (nothing is published at that point, so any read there is a false alarm).
78
+ - **Run the check only in the tree that published.** The packed integrity is deterministic per tree but differs between trees (a CRLF Windows checkout versus the clean LF worktree the release is published from, or a different `dist`). A hand run of `npm run check:publish-health` from another checkout will say "the registry's <version> is NOT the tarball packed here" for a good publish; that is not a bad publish, do not publish again (NightHawk, PR 1386).
79
+
80
+ ## 4.8.6 (published under dist-tag `next` only)
81
+
82
+ - **NEW: the "I was invited" entry (PR 1382).** `@payez/next-mvp/pages/invited` (mount at `app/account-auth/signup/page.tsx`) takes the address the invitation went to and posts it to the app's own `/api/onboarding/signup` (`@payez/next-mvp/routes/onboarding/signup`), which forwards EXACTLY `{ email, client_id }` to the IdP's anonymous `Onboarding/signup` (the client id is the app's own `IDP_CLIENT_ID`/`CLIENT_ID`, never the browser's; any other key is dropped) and answers ONE fixed 202 whatever the IdP said. The page shows the same sentence afterwards and never reads the answer, so it is not an oracle for which addresses were invited. `@payez/next-mvp/components/auth/InvitedEntryLink` is the link for the sign-in page; the generator mounts all of it.
83
+ - **CHANGE: the onboarding page requests its own code.** It asks the IdP to email the code once per token on load, offers "Send a new code", and shows `DevCodeNotice` only from that answer's `dev_verification_code` (no environment flag; it shows once Core passes the echo through, Core PR 1379). Its text no longer says the code is in the invitation email.
84
+ - Rows: `test/next-mvp-486-invited-entry.test.cjs` (15; mutants RED: extra field forwarded, client id from the browser, an outage or the IdP status changing the answer, the page reading the answer, an env-gated echo, a second code source, a re-send on every render). Package suite 1181 tests, 1180 pass, 0 fail, 1 skipped.
85
+
86
+ ## 4.8.5 (AUTH-045; published under dist-tag `next` only)
87
+
88
+ - **`CredentialSignInForm` shows the development sign-in code when the identity service echoes it.** On a dev identity service the emailed code goes to nobody; when the IdP runs with `Email:DevCodeEcho` (Development only) it returns the code as `DevVerificationCode`, the app's sign-in door passes it through as `devVerificationCode` (it already did), and the form now prints `Development sign-in code: <code>` under the code box. It is a pure passthrough of that field: the notice renders nothing unless it is given a non-empty string, only a string is ever read as a dev code, and there is NO environment flag in the app, so an IdP with the echo off (production) can never make it appear. `credentialSignIn` results of kind `two_factor` gain `devCode: string | null`.
89
+ - Rows: `test/auth-045-dev-code-echo.test.cjs` (6; mutants RED: render when absent, a non-string accepted as a code, rendering from another source, an environment flag). Package suite 1166 tests, 1164 pass, 0 fail, 2 skipped.
90
+
91
+ ## 4.8.4 (REG-002; published under dist-tag `next` only)
92
+
93
+ - **NEW: `@payez/next-mvp/agents`, the SDK half of REG-002 (PR 1335, follow-ups PR 1343).** `createAgentRegistryClient({ baseUrl, accessToken })` with `register({ user_name, owner_user_id, host_class })`, `list()` and `get(id)` over the tenant-admin agent door (`POST/GET api/client/agents`, PayEz-Core `ClientAgentsController` @ 22d44a34c). The client comes from the caller's token: there is no client id to pass, and the body is exactly the three fields.
94
+ - Refusals are thrown as `AgentRegistryError` with the server's `code` as-is (`VALIDATION_ERROR`, `OWNER_NOT_ELIGIBLE`, `OWNER_NOT_MEMBER`, `AGENT_NAME_TAKEN`, `AUDIT_WRITE_FAILED_NOT_APPLIED`, ...). **Nothing is retried, of any status.** A transport failure on `register` says the outcome is unknown (read the list); `get(id)` percent-encodes the id so `get('1/2')` requests `/api/client/agents/1%2F2`. The bearer goes through `bearerFetch` (never follows a redirect) and never appears in an error.
95
+ - A newly registered agent is PENDING until an administrator activates it; its first mint is refused `AGENT_PENDING`.
96
+ - Additive: a new export subpath and a README example. No existing export changes. Rows: `test/reg-002-agent-registry-client.test.cjs` (19; mutants RED: forward-all body, code mapping, token in message, 409 retry, get without encoding, old register text).
97
+
98
+ ## 4.8.3 (PAY-1810, PAY-2041; published under dist-tag `next` only)
99
+
100
+ - **FIX: the admin handlers `admin/sessions.ts` (list search and `revoke_all`), `admin/users.ts` and `admin/audit.ts` send filters in a shape Core accepts.** They sent `{ operator, conditions: [...] }` for two or more conditions; Core's compound form is `{ operator, filters: [...] }`, and with no `filters` key Core read `operator` / `conditions` as column names and returned ZERO rows: a broken user search, an audit filter with two or more fields that returned nothing, and `revoke_all` that found no sessions, revoked 0 and answered success. They also sent operator `like` (not a Core operator; in a flat filter it is dropped and every row comes back) and `orderBy` as a string (binds nothing). Now: compound with `filters`, substring as `contains` (case-insensitive), `orderBy` as a `{ field: dir }` map, via the new internal helper `src/vibe/filters.ts` (`eq`, `gte`, `lte`, `contains`, `allOf`, `anyOf`, `orderByMap`). `admin/stats.ts` `orderBy` is a map too.
101
+ - **`revoke_all` now revokes EVERY active session of the user, not the first page.** Core returns one page per `/query` call (default 20). The handler re-reads page 1 of 50 (revoked sessions leave the `status = active` filter) until a read is empty, stops if a round revokes nothing (a failing write never loops) or after 20 rounds, and the response adds `complete` (true only when the last read was empty); `success` now equals `complete`, so a total or partial write failure is no longer reported as success. A failing FIRST read is still a plain upstream error.
102
+ - **The session writes in `admin/sessions.ts` are PATCH, not PUT** (single `revoke` and each `revoke_all` write). A PUT to a Vibe document REPLACES it (Core `UpdateAsync` sets `data` to the body), so the 3-field revoke body erased `idp_user_id`, `session_token_hash` and the rest of the row; once the filter fix made `revoke_all` find rows, it would have destroyed them (NightHawk 75394). The helper's method type gains `PATCH`.
103
+ - Behaviour change for consumers: an admin session search by email is now a case-insensitive substring match that really filters (it used to return every session), and a user search or a multi-filter audit query returns rows (it used to return none).
104
+ - Rows: `test/pay-1810-admin-filter-shapes.test.cjs` (12, against dist; mutants 1-10 RED). Package suite 1137 tests, 0 fail, 2 skipped.
105
+ - **PAY-2041: `VibeTable.update`, the generic `GenericTableDelegate.update` and the `admin/users.ts` `update_tier` / `update_credits` writes are PATCH, not PUT.** Same defect as above: a PUT replaces the Vibe document, and these send a Partial or a single field, so each erased every field it did not name (a tier change wiped the rest of the user row). PATCH merges (Core `DocumentsController.Patch`; the proxy forwards PATCH; the signature is over PATCH). The request method type gains `PATCH`. Behaviour change for consumers: `update({ where, data })` now MERGES `data` into the document; a field can no longer be removed by omitting it from `data`.
106
+ - Deliberately unchanged: `admin/vibe-data.ts` `createUpdateRecordHandler` stays a PUT (an admin editor save is a replace; documented in the handler, BAPert 75440).
107
+ - Rows: `test/pay-2041-vibe-update-patch.test.cjs` (4; 3 RED on a PUT mutant). Package suite 1141 tests, 0 fail, 2 skipped (PR 1333).
108
+
109
+ ## 4.8.2 (PAY-2362; published under dist-tag `next` only)
110
+
111
+ - **FIX: the per-session revocation decision (`findRevokingSession`, behind `getRevocationStatus` and `checkSessionRevocation`) now treats a session as revoked if ANY `login_sessions` row carrying its `session_token_hash` is revoked.** It used to take the FIRST row for the hash (`find`). A second record-login for the same session (another tab, a cleared sessionStorage, every open session when an app first runs a version that changes its login-recording key) writes a second row with the same hash, and an admin revoke PATCHes ONE row by id, so an active duplicate listed first hid the revoke (a per-session revocation bypass, PAY-1882). The verdict reason is still `own_row_revoked`.
112
+ - Unchanged: a session whose hash matches only active rows is `active`; another session's revoked row (a different hash) still never revokes this one when this one has its own row; the legacy rules (no row carries the hash) are untouched.
113
+ - Rows: `test/pay-2362-any-row-for-hash-revoked.test.cjs` (6; RED on 4.8.1: A, D, F). Package suite 1119 tests, 0 fail, 2 skipped.
114
+ - **WRITE SIDE: `createLoginSession` is idempotent by `session_token_hash`.** When a row for the hash already exists it PATCHes `last_activity` on that row (never `status`, so it cannot un-revoke) and returns it instead of inserting a second row; a failed lookup falls through to the insert, so a login is never blocked. It costs one extra `login_sessions` read when `sessionTokenHash` is passed. Rows: `test/pay-2362-create-login-session-by-hash.test.cjs` (6; RED on the first 4.8.2 commit: A, B). Existing duplicate rows are not cleaned up; the any-row read covers them.
115
+ - **Consumers with their own copy of this decision must change it too.** idealresume.online `lib/vibe-sessions.ts` `checkSessionRevocation` has its own `find` (separate PR).
116
+
117
+ ## 4.8.1 (PAY-2345; published under dist-tag `next` only)
118
+
119
+ - **DEFAULT CHANGE: the viability-FAILURE arm (viability read answered 500 / timed out / threw, or the circuit breaker is open or refuses a refresh) now answers a page request inline too: `503` HTML with a "Try again" link, `cache-control: no-store`, `retry-after: 30`, no `Set-Cookie`. It no longer 307s to `/service-unavailable`, which an app that never mounted that page served as a 404 during an outage.** To keep a redirect, name your page with `unavailablePage` (now honoured by BOTH arms). `/api` is unchanged: `503 { error: 'ServiceUnavailable', unavailable: true, check: 'viability' }`.
120
+ - The inline sentence depends on the check: the revocation hold keeps "We could not check your session just now. You are still signed in."; the viability-failure arm says "We could not reach the sign-in service just now. Please try again." (it can be shown to a visitor with no session, and a session it could not verify is not one it can call signed in). `revocationFailMode` governs revocation answers only: naming `'closed'` or `'open'` does NOT keep the old redirect on this arm; only `unavailablePage` does (BAPert 74739 D1/D2).
121
+ - The call sites are unchanged; the two arms still differ only by `check`, so they can be diffed independently. The session is still never touched from this arm (PAY-2147).
122
+
123
+ ## 4.8.0 (PAY-2339 Phase 1; published under dist-tag `next` only)
124
+
125
+ - **DEFAULT CHANGE: when the revocation check cannot answer (`revocationStatus: 'unknown'`), the package no longer signs the member out; it holds. To keep the old sign-out, pass `revocationFailMode: 'closed'` to `createMvpMiddleware`.**
126
+ - Held request: a page request is answered inline by the middleware with a minimal `503` HTML page ("Temporarily unavailable", a "Try again" link to the same-site callback, no `Set-Cookie`; no page needs to exist in the app, so no 404 is possible). To redirect to your own page instead, name it: `unavailablePage: '/service-unavailable'` (then mount `export { default } from '@payez/next-mvp/pages/service-unavailable';` or your own page at that route). An `/api` request gets `503 { error: 'ServiceUnavailable', unavailable: true, check: 'revocation' }`. No cookie is touched, nothing protected is served, and the member is still signed in when they come back.
127
+ - Both viability producers (`api-handlers/session/viability`, `routes/auth/viability`) stop setting `forceInvalid` for `'unknown'` (and for any unrecognised string) and emit `unavailable: true`; `revocationStatus` is unchanged. `forceInvalid` is now true only for `'revoked'`. The exported `makeAuthDecision` (root and `/edge`) holds on `sessionStatus.unavailable` (returns `service_error`, `check: 'revocation'`) instead of signing out.
128
+ - Optional page `@payez/next-mvp/pages/service-unavailable` for apps that set `unavailablePage` (mount: `export { default } from '@payez/next-mvp/pages/service-unavailable';` in `app/service-unavailable/page.tsx`). `callbackUrl` is followed only when it is a same-site relative path. (PAY-2345: the viability `service_error` arm now answers inline as well; see Unreleased.)
129
+ - Unchanged: `'revoked'` signs out; `'active'`, an absent status, and `'not_configured'` behave as before; an app naming `'closed'` or `'open'` keeps exactly that.
130
+ - Log: one `revocation_unavailable` warn line (`{ check: 'revocation' }`, no session value) per held request. No sampling in Phase 1, so a backend outage logs one line per protected request.
131
+ - Existing 503 bodies from the viability `service_error` arm now also carry `unavailable: true, check: 'viability'`.
132
+ - **Move together:** producers and decision paths must be on 4.8.0 together. A 4.8.0 producer no longer sets `forceInvalid` for `'unknown'`, so a consumer on an older boolean-only reader of the viability JSON now reads it as viable; read `unavailable` (or `revocationStatus`) instead.
133
+ - `unavailablePage` affects the revocation hold on 4.8.0 (PAY-2345 extends it to the viability-failure arm); register the named route with `configurePublicRoutes`; a held request for the named page itself is answered inline, never redirected to itself.
134
+ - The inline 503 carries `cache-control: no-store` and `retry-after: 30`.
135
+ - `'open'` by name is unchanged and is broader than 'unknown': it keeps BOTH `'unknown'` and `'not_configured'` viable, exactly as on 4.7.10.
136
+ - `unavailablePage` is accepted only as a plain absolute path (`/` then letters, digits, `_`, `-`, `/`): a query, fragment, `%`-encoding, dot segment or `//` is ignored and the inline 503 is used; a trailing slash on the request is normalised when comparing.
@@ -1,37 +1,24 @@
1
1
  /**
2
- * CRITICAL REFRESH TOKEN API HANDLER
2
+ * REFRESH TOKEN API HANDLER - a thin HTTP wrapper over `ensureFreshAccessToken` (R-over-E, BAPert 76449; design: docs/refresh-r-over-e-delta.md)
3
3
  *
4
- * ASK BEFORE EDITING - TESTED AND WORKING SYSTEM
4
+ * ASK BEFORE EDITING - this is the route every browser, middleware and `internalRefresh` caller reaches.
5
5
  *
6
- * This handler manages the server-side refresh token cycle with:
7
- * - Better Auth session extraction (the ONLY way a session is established here)
8
- * - PayEz IDP refresh token exchange
9
- * - Session state updates with new tokens
10
- * - Proper error handling and logging
11
- * - Single-use semantics enforcement
6
+ * `lib/ensure-fresh-access-token` is the ONE implementation that talks to the IdP, holds the refresh lock and writes the tokens (post-lock
7
+ * token, in-doubt marker, fetch timeout, envelope rule, dropped-persist and canonical-row handling all live there). This file keeps what only
8
+ * an HTTP request has:
9
+ * - resolving the session from the request's own Better Auth cookie,
10
+ * - the `x-session-token` ownership rejection (below),
11
+ * - the `x-request-id` trace id (log correlation only; it is never the lock identity),
12
+ * - mapping E's typed result onto this route's existing JSON bodies and status codes.
13
+ * The bodies did not change except where E's rules did (see CHANGELOG): the browser, `middleware/create-middleware`, `lib/token-lifecycle`
14
+ * (which reads `reason === 'already_fresh'`) and `lib/idp-fetch` / `lib/api-handler` (via `internalRefresh`) depend on them.
12
15
  *
13
16
  * ── 246007: THE `x-session-token` FALLBACK IS GONE. IT NOW HARD-DENIES. ────
14
- * This list previously advertised an x-session-token fallback for internal
15
- * callers as a current capability. That behaviour has been REMOVED: a request presenting `x-session-token` with
16
- * no cookie-resolved session of its own is rejected 401 UNAUTHORIZED, because
17
- * there is no session to establish ownership of the token it names. Accepting
18
- * it let a caller refresh a session they did not own.
17
+ * A request presenting `x-session-token` with no cookie-resolved session of its own is rejected 401 UNAUTHORIZED, because there is no
18
+ * session to establish ownership of the token it names. Accepting it let a caller refresh a session they did not own. This docstring
19
+ * ships (it is compiled into dist and the .d.ts), so it describes what the code does.
19
20
  *
20
- * The line is corrected here rather than merely deleted, and it is called out
21
- * rather than quietly dropped, because THIS DOCSTRING SHIPS. It is compiled
22
- * into dist and into the .d.ts, so it reaches every consumer's editor as hover
23
- * text. QAPert-NightHawk caught that the fixed build was still describing the
24
- * removed capability as a current feature (34973) — her verifier tests code
25
- * properties and reads no comments, so a correct build could have published
26
- * documentation asserting the opposite of what the code does.
27
- *
28
- * That matters twice over: an internal caller relying on the fallback gets a
29
- * silent behavioural break while the docs promise it still works, and a
30
- * published package that misdescribes its own auth behaviour is the same
31
- * amplifier that put other defects onto three surfaces — shipped code carrying
32
- * instructions for using it wrongly.
33
- *
34
- * @version 2.1
21
+ * @version 3.0
35
22
  */
36
23
  import { NextRequest, NextResponse } from 'next/server';
37
24
  interface RefreshConfig {
@@ -57,36 +44,10 @@ interface RefreshConfig {
57
44
  * });
58
45
  * ```
59
46
  */
60
- export declare function createRefreshHandler(config: RefreshConfig): (req: NextRequest) => Promise<NextResponse<{
61
- error: string;
62
- code: string;
63
- }> | NextResponse<{
64
- error: any;
65
- code: any;
66
- discardToken: boolean;
67
- retryable: boolean;
68
- resolution: any;
69
- }> | NextResponse<{
70
- refreshed: boolean;
71
- accessTokenExpires: number;
72
- hasRefreshToken: boolean;
73
- }>>;
47
+ export declare function createRefreshHandler(config: RefreshConfig): (req: NextRequest) => Promise<NextResponse<unknown>>;
74
48
  /**
75
49
  * Default POST export — drop-in for `app/api/auth/refresh/route.ts`.
76
50
  * Requires environment variables: IDP_URL, CLIENT_ID
77
51
  */
78
- export declare const POST: (req: NextRequest) => Promise<NextResponse<{
79
- error: string;
80
- code: string;
81
- }> | NextResponse<{
82
- error: any;
83
- code: any;
84
- discardToken: boolean;
85
- retryable: boolean;
86
- resolution: any;
87
- }> | NextResponse<{
88
- refreshed: boolean;
89
- accessTokenExpires: number;
90
- hasRefreshToken: boolean;
91
- }>>;
52
+ export declare const POST: (req: NextRequest) => Promise<NextResponse<unknown>>;
92
53
  export {};