@payez/next-mvp 4.10.0 → 4.12.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 (123) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/api-handlers/account/recovery/forward.js +2 -1
  3. package/dist/api-handlers/account/recovery/initiate.js +2 -1
  4. package/dist/api-handlers/account/reset-password.js +2 -1
  5. package/dist/api-handlers/account/send-code.js +2 -1
  6. package/dist/api-handlers/account/update-phone.js +2 -1
  7. package/dist/api-handlers/account/validate-password.js +2 -1
  8. package/dist/api-handlers/account/verify-email.js +2 -1
  9. package/dist/api-handlers/account/verify-sms.js +2 -1
  10. package/dist/api-handlers/anon/preferences.js +3 -2
  11. package/dist/api-handlers/auth/credential-login-email-code.js +3 -2
  12. package/dist/api-handlers/auth/credential-login.js +6 -5
  13. package/dist/api-handlers/auth/refresh.d.ts +17 -58
  14. package/dist/api-handlers/auth/refresh.js +73 -773
  15. package/dist/api-handlers/auth/signout.js +43 -6
  16. package/dist/api-handlers/mfa/backup-code-verify.js +2 -1
  17. package/dist/api-handlers/mfa/totp-verify.js +2 -1
  18. package/dist/api-handlers/session/refresh-viability.js +3 -2
  19. package/dist/api-handlers/session/viability.js +2 -1
  20. package/dist/auth/better-auth.js +7 -6
  21. package/dist/auth/idp-oauth.js +4 -2
  22. package/dist/auth/utils/idp-client.js +5 -4
  23. package/dist/auth/utils/token-utils.d.ts +1 -1
  24. package/dist/auth/utils/token-utils.js +7 -5
  25. package/dist/config/logger.js +2 -1
  26. package/dist/config/logging-config.js +4 -3
  27. package/dist/config/vibe-log-transport.js +2 -1
  28. package/dist/lib/api-handler.js +4 -3
  29. package/dist/lib/ensure-fresh-access-token.d.ts +24 -1
  30. package/dist/lib/ensure-fresh-access-token.js +195 -28
  31. package/dist/lib/geolocation.js +5 -22
  32. package/dist/lib/idp-client-config.js +4 -3
  33. package/dist/lib/idp-fetch.d.ts +11 -0
  34. package/dist/lib/idp-fetch.js +6 -3
  35. package/dist/lib/idp-logout.d.ts +10 -0
  36. package/dist/lib/idp-logout.js +73 -1
  37. package/dist/lib/jwt-decode-client.js +2 -1
  38. package/dist/lib/jwt-decode.js +2 -1
  39. package/dist/lib/log-safe.d.ts +19 -0
  40. package/dist/lib/log-safe.js +32 -0
  41. package/dist/lib/redis.js +2 -1
  42. package/dist/lib/refresh-in-doubt.d.ts +31 -0
  43. package/dist/lib/refresh-in-doubt.js +81 -0
  44. package/dist/lib/refresh-token-validator.js +2 -1
  45. package/dist/lib/refresh-window.d.ts +16 -0
  46. package/dist/lib/refresh-window.js +19 -0
  47. package/dist/lib/safe-error-response.d.ts +5 -1
  48. package/dist/lib/safe-error-response.js +7 -2
  49. package/dist/lib/safe-trace-id.d.ts +11 -0
  50. package/dist/lib/safe-trace-id.js +23 -0
  51. package/dist/lib/session-store.js +16 -5
  52. package/dist/lib/site-logger.d.ts +2 -8
  53. package/dist/lib/site-logger.js +11 -5
  54. package/dist/lib/standardized-client-api.js +3 -2
  55. package/dist/lib/token-lifecycle.js +6 -4
  56. package/dist/middleware/create-middleware.js +2 -1
  57. package/dist/middleware/rbac-check.d.ts +0 -13
  58. package/dist/middleware/rbac-check.js +6 -5
  59. package/dist/models/SessionModel.d.ts +7 -0
  60. package/dist/routes/auth/logout.js +6 -2
  61. package/dist/routes/auth/refresh.d.ts +1 -15
  62. package/dist/routes/auth/settings.js +2 -1
  63. package/dist/server/auth-guard.js +4 -3
  64. package/dist/server/auth.d.ts +1 -1
  65. package/dist/server/auth.js +5 -9
  66. package/dist/server/decode-session.js +3 -2
  67. package/dist/server/with-auth.js +2 -1
  68. package/dist/stores/authStore.js +3 -2
  69. package/package.json +1 -1
  70. package/scripts/verify-release-provenance.mjs +1 -0
  71. package/src/api-handlers/account/recovery/forward.ts +2 -1
  72. package/src/api-handlers/account/recovery/initiate.ts +2 -1
  73. package/src/api-handlers/account/reset-password.ts +2 -1
  74. package/src/api-handlers/account/send-code.ts +2 -1
  75. package/src/api-handlers/account/update-phone.ts +2 -1
  76. package/src/api-handlers/account/validate-password.ts +2 -1
  77. package/src/api-handlers/account/verify-email.ts +2 -1
  78. package/src/api-handlers/account/verify-sms.ts +2 -1
  79. package/src/api-handlers/anon/preferences.ts +3 -2
  80. package/src/api-handlers/auth/credential-login-email-code.ts +3 -2
  81. package/src/api-handlers/auth/credential-login.ts +6 -5
  82. package/src/api-handlers/auth/refresh.ts +74 -813
  83. package/src/api-handlers/auth/signout.ts +54 -7
  84. package/src/api-handlers/mfa/backup-code-verify.ts +2 -1
  85. package/src/api-handlers/mfa/totp-verify.ts +2 -1
  86. package/src/api-handlers/session/refresh-viability.ts +3 -2
  87. package/src/api-handlers/session/viability.ts +2 -1
  88. package/src/auth/better-auth.ts +7 -6
  89. package/src/auth/idp-oauth.ts +7 -2
  90. package/src/auth/utils/idp-client.ts +5 -4
  91. package/src/auth/utils/token-utils.ts +7 -5
  92. package/src/config/logger.ts +2 -1
  93. package/src/config/logging-config.ts +4 -3
  94. package/src/config/vibe-log-transport.ts +2 -1
  95. package/src/lib/api-handler.ts +4 -3
  96. package/src/lib/ensure-fresh-access-token.ts +214 -29
  97. package/src/lib/geolocation.ts +5 -4
  98. package/src/lib/idp-client-config.ts +4 -3
  99. package/src/lib/idp-fetch.ts +14 -3
  100. package/src/lib/idp-logout.ts +36 -1
  101. package/src/lib/jwt-decode-client.ts +3 -1
  102. package/src/lib/jwt-decode.ts +2 -1
  103. package/src/lib/log-safe.ts +30 -0
  104. package/src/lib/redis.ts +2 -1
  105. package/src/lib/refresh-in-doubt.ts +79 -0
  106. package/src/lib/refresh-token-validator.ts +2 -1
  107. package/src/lib/refresh-window.ts +16 -0
  108. package/src/lib/safe-error-response.ts +7 -2
  109. package/src/lib/safe-trace-id.ts +21 -0
  110. package/src/lib/session-store.ts +17 -4
  111. package/src/lib/site-logger.ts +13 -7
  112. package/src/lib/standardized-client-api.ts +3 -2
  113. package/src/lib/token-lifecycle.ts +6 -4
  114. package/src/middleware/create-middleware.ts +2 -1
  115. package/src/middleware/rbac-check.ts +2 -1
  116. package/src/models/SessionModel.ts +7 -0
  117. package/src/routes/auth/logout.ts +9 -2
  118. package/src/routes/auth/settings.ts +2 -1
  119. package/src/server/auth-guard.ts +4 -3
  120. package/src/server/auth.ts +6 -11
  121. package/src/server/decode-session.ts +3 -2
  122. package/src/server/with-auth.ts +2 -1
  123. package/src/stores/authStore.ts +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog — @payez/next-mvp
2
2
 
3
+ ## 4.12.0 (to be published under dist-tag `next` only, after 4.11.0; `latest` stays 4.8.9 until Jon says otherwise)
4
+
5
+ A minor: PII-in-logs groups 1, 2, 3a and 3b, the GOV-023 signout BREAKING change, the logout-401 FIX (PAY-2396), and the BREAKING site-logger signatures (3b: callers of `siteEvents.loginFailed` / `siteEvents.error` must change).
6
+
7
+
8
+ - **BREAKING: `siteEvents.loginFailed` and `siteEvents.error` (`lib/site-logger`) lose their free-text parameters (PII-in-logs group 3b, BAPert 76553 Q2).** `loginFailed({ reason, url?, user_agent?, ip_address? })` no longer takes `email`; the `reason` is stored only if it is a closed UPPER_SNAKE code, anything else is stored as `UNSPECIFIED`. `error({ code, user_id?, url?, user_agent?, ip_address? })` takes a closed `code` instead of `message`, `error` and `stack`; the stored message is the fixed sentence `Application error` and the code goes in `context.code`. The parameters are removed from the TYPE, so a TypeScript caller that still passes `email`, `message`, `error` or `stack` fails to compile, which is the point. **Migration:** replace `loginFailed({ reason: 'bad password', email })` with `loginFailed({ reason: 'BAD_PASSWORD' })` (look the user up by `user_id` elsewhere if support needs it), and `error({ message: err.message, stack: err.stack })` with `error({ code: 'YOUR_CLOSED_CODE' })`. Callers found: the package itself calls only `siteEvents.logout`; VibeID-Customer-Portal 0; idealvibe.online 0 `siteEvents.*` (it has its own copy of `lib/site-logger.ts`, which keeps the old shapes until its own caller PR); UMI-Website 0 (not verified against its repo). Row: `test/next-mvp-pii-logs-group3b.test.cjs`.
9
+
10
+ - **BREAKING: `api-handlers/auth/signout` now ends the IdP grant, not only the local session (GOV-023, BAPert 76687).** It used to delete the local record only and make no IdP call, so an app that mounted it left the IdP refresh token live until it expired (`routes/auth/logout` already revoked first). It now runs the same IdP-first path: `revokeSessionAtIdp`, then the local delete, then the cookies. If the IdP revoke fails, or cannot resolve the session while a refresh token is held, or the local delete fails, it answers **HTTP 500** `{ success: false, code: 'SESSION_NOT_ENDED', cause: 'idp_unconfirmed' | 'local_delete_failed', message }` (the same code, causes and sentences as `routes/auth/logout`; both failing: the local cause) instead of `success: true`; the local delete and the cookie clearing still happen. A caller that treated every signout answer as success must now read `success` / `cause`. Ships in the release after 4.11.0 (with the site-logger signature change). Rows: `test/next-mvp-gov023-signout-ends-idp-grant.test.cjs`.
11
+ - **CHANGE: a caught error is logged by its NAME and a closed code/status, never its message (PII-in-logs group 1, BAPert 76553).** New `lib/log-safe` (`errForLog`, `closedCodeForLog`, internal, not in the exports map): `error=<Name> [status=<100-599>] [code=<CLOSED_TOKEN>]`; `message`, `stack`, `cause` and `details` never appear, and a name, status or code that is not a closed shape is dropped. Applied to the shared sites with the widest reach: `logExceptionWithCorrelation` (14 handlers; the line keeps the `correlationId`, the logged `error` is now the string above instead of the raw error object), the four catches in `auth/utils/idp-client` (login, refresh, verify-2FA, send-2FA-code), `api-handlers/session/refresh-viability`'s catch (it logged the exception message next to the correlation id; found by the Mac run of `user-facing-error-not-returned`), and `server/auth`'s `SESSION_READ_SWALLOWED` line (it used to log a redacted, 80-character-truncated message; it now logs `error=<Name>` only). Operators lose the exception text in these log lines on purpose; the correlation id still ties a response to its line. Rows: `test/next-mvp-pii-logs-group1.test.cjs`; `test/pay-2136-no-session-origin.test.cjs` and `test/user-facing-error-not-returned.test.cjs` now assert the opposite of what they did (the message must NOT be in the log).
12
+ - **CHANGE: more caught errors are logged by NAME and closed code/status, not by message (PII-in-logs group 2, BAPert 76767).** The catches in `api-handlers/account/*` (send-code, update-phone, validate-password, verify-email, verify-sms, reset-password, recovery initiate and forward), `api-handlers/mfa/*` (backup-code-verify, totp-verify), `auth/utils/token-utils` (three sites) and `lib/session-store` now log `errForLog(err)` instead of the error object. The response bodies do not change. Row: `test/next-mvp-pii-logs-group2.test.cjs` (a source scan of those 12 files, plus a real-dist token-decode failure whose canary must not reach a log line).
13
+ - **CHANGE: the rest of the server-side caught-error logs use `errForLog` (PII-in-logs group 3a, BAPert 76955).** `api-handlers` (anon preferences, credential-login and its email-code lane, refresh, session viability), `auth` (better-auth, idp-oauth), `lib` (ensure-fresh-access-token, idp-client-config, jwt-decode, redis, site-logger, standardized-client-api, token-lifecycle), `config` (logger, logging-config, vibe-log-transport), `routes/auth` (logout, settings), `server` (auth-guard, decode-session, with-auth) and `middleware` now log the error's name and a closed status/code, not `err`, `err.message` or `String(err)`. `lib/geolocation` no longer logs the IP or the upstream's `data.message` (Q1). The `payez-idp access token refused` line now reads `error=Error code=RESPONSE_ADDRESS_MASKED` or `code=NO_USABLE_EMAIL` (a closed code on the two refusals thrown in `auth/idp-oauth`, BAPert 76998) instead of the reason sentence, so an operator still sees WHY and no message text is logged. Response bodies do not change. Browser code (client, components, hooks, stores) is untouched. Row: `test/next-mvp-pii-logs-group3a.test.cjs`.
14
+ - **FIX: a sign-out whose logout answers 401 now forces one REAL refresh, so a dead grant is no longer reported as "could not confirm the sign-out" (BAPert 77013, QAPert 77012).** After a password change, an admin revoke or a sign-out elsewhere, Core has invalidated the access token while the stored copy still looks fresh. `POST /api/Account/logout` then answered 401, the refresh the 401 triggers answered "already fresh" without asking the IdP, so no discard marker was written and `revokeSessionAtIdp` threw: the route answered `SESSION_NOT_ENDED` / `idp_unconfirmed` and logged "the IdP refresh token is STILL LIVE" for a grant that was already dead. Now `revokeSessionAtIdp` makes the refresh `idpFetchJSON` runs after a 401 a FORCED one, done in-process (`refreshOnAuthFailure` calls `ensureFreshAccessToken` with a window that cannot be "fresh", config from `IDP_URL` / `CLIENT_ID` as `oauth/resume` reads it). Nothing changes on the public `POST /api/auth/refresh`: no header can force a refresh (NightHawk 77021, BAPert 77022). The IdP's answer decides: a refresh 401 with the discard marker is `grant-already-dead` (a successful sign-out); a refresh that succeeds while the logout is still refused means the grant is live and stays `idp_unconfirmed`. The failure is a typed `IdpLogoutUnconfirmedError` (`code` `IDP_LOGOUT_UNCONFIRMED`, `grantLive`); `routes/auth/logout` and `api-handlers/auth/signout` print "STILL LIVE" only when `grantLive` is true and otherwise say the revoke could not be confirmed. `api-handlers/auth/signout` also logs its revoke and session-cleanup errors through `errForLog` now (they passed the raw error). Rows: `test/next-mvp-logout-401-forces-refresh.test.cjs`. A real, unstubbed row (a real password change must land on `notice=password-changed`) is QAPert's walk.
15
+
16
+ ## 4.11.0 (to be published under dist-tag `next` only, after 4.10.0; `latest` stays 4.8.9 until Jon says otherwise)
17
+
18
+ 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).
19
+
20
+ - **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.
21
+
22
+ - **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`.
23
+ - **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`.
24
+ - **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:**
25
+ - 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).
26
+ - 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).
27
+ - 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**.
28
+ - 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.
29
+ - 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).
30
+
31
+ - **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).**
32
+ - **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).
33
+ - **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.
34
+ - **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.
35
+ - **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.
36
+ - **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.
37
+ - **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).
38
+ - Rows: `test/next-mvp-4910-ensure-fresh-r-parity.test.cjs` (a mutant per change).
39
+
40
+ - **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).
41
+
42
+ - **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).
43
+ - **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.
44
+
45
+ - **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).
46
+
3
47
  ## 4.10.0 (to be published under dist-tag `next` only, after 4.9.0; `latest` stays 4.8.9 until Jon says otherwise)
4
48
 
5
49
  Carries PRs 1432, 1435 and 1438. A minor, not a patch, because a removed export breaks importers (BREAKING below).
@@ -7,6 +7,7 @@ const env_1 = require("../../../config/env");
7
7
  const idp_client_config_1 = require("../../../lib/idp-client-config");
8
8
  const client_key_1 = require("../../../lib/client-key");
9
9
  const safe_error_response_1 = require("../../../lib/safe-error-response");
10
+ const log_safe_1 = require("../../../lib/log-safe");
10
11
  /**
11
12
  * Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
12
13
  *
@@ -61,7 +62,7 @@ async function recoveryClientKey(where) {
61
62
  return (0, client_key_1.resolveClientKey)(config, where).key;
62
63
  }
63
64
  catch (e) {
64
- console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
65
+ console.error(`[${where}] could not resolve IDP client config for client_id:`, (0, log_safe_1.errForLog)(e));
65
66
  return null;
66
67
  }
67
68
  }
@@ -5,6 +5,7 @@ const server_1 = require("next/server");
5
5
  const idp_client_config_1 = require("../../../lib/idp-client-config");
6
6
  const client_key_1 = require("../../../lib/client-key");
7
7
  const safe_error_response_1 = require("../../../lib/safe-error-response");
8
+ const log_safe_1 = require("../../../lib/log-safe");
8
9
  function getIdpUrl() {
9
10
  const url = process.env.IDP_URL;
10
11
  if (!url)
@@ -37,7 +38,7 @@ async function recoveryClientKey() {
37
38
  return (0, client_key_1.resolveClientKey)(config, 'account/recovery/initiate').key;
38
39
  }
39
40
  catch (e) {
40
- console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
41
+ console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', (0, log_safe_1.errForLog)(e));
41
42
  return null;
42
43
  }
43
44
  }
@@ -5,6 +5,7 @@ const server_1 = require("next/server");
5
5
  const idp_client_config_1 = require("../../lib/idp-client-config");
6
6
  const client_key_1 = require("../../lib/client-key");
7
7
  const safe_error_response_1 = require("../../lib/safe-error-response");
8
+ const log_safe_1 = require("../../lib/log-safe");
8
9
  function getIdpUrl() {
9
10
  const url = process.env.IDP_URL;
10
11
  if (!url)
@@ -37,7 +38,7 @@ async function recoveryClientKey() {
37
38
  return (0, client_key_1.resolveClientKey)(config, 'account/reset-password').key;
38
39
  }
39
40
  catch (e) {
40
- console.error('[account/reset-password] could not resolve IDP client config for client_id:', e);
41
+ console.error('[account/reset-password] could not resolve IDP client config for client_id:', (0, log_safe_1.errForLog)(e));
41
42
  return null;
42
43
  }
43
44
  }
@@ -13,6 +13,7 @@ const server_1 = require("next/server");
13
13
  const idp_fetch_1 = require("../../lib/idp-fetch");
14
14
  const env_1 = require("../../config/env");
15
15
  const two_factor_verify_failure_1 = require("../../lib/two-factor-verify-failure");
16
+ const log_safe_1 = require("../../lib/log-safe");
16
17
  async function POST(req) {
17
18
  try {
18
19
  // Parse request body
@@ -47,7 +48,7 @@ async function POST(req) {
47
48
  }, { status: 200 });
48
49
  }
49
50
  catch (error) {
50
- console.error('[SEND_CODE] Error:', error);
51
+ console.error('[SEND_CODE] Error:', (0, log_safe_1.errForLog)(error));
51
52
  return server_1.NextResponse.json({
52
53
  success: false,
53
54
  error: 'Failed to send verification code',
@@ -4,6 +4,7 @@ exports.POST = POST;
4
4
  const server_1 = require("next/server");
5
5
  const idp_fetch_1 = require("../../lib/idp-fetch");
6
6
  const env_1 = require("../../config/env");
7
+ const log_safe_1 = require("../../lib/log-safe");
7
8
  /**
8
9
  * Update Phone Number API Handler
9
10
  *
@@ -63,7 +64,7 @@ async function POST(req) {
63
64
  });
64
65
  }
65
66
  catch (error) {
66
- console.error('[UPDATE_PHONE] Error:', error);
67
+ console.error('[UPDATE_PHONE] Error:', (0, log_safe_1.errForLog)(error));
67
68
  return server_1.NextResponse.json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to update phone number' } }, { status: 500 });
68
69
  }
69
70
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.POST = POST;
4
4
  const server_1 = require("next/server");
5
+ const log_safe_1 = require("../../lib/log-safe");
5
6
  async function POST(req) {
6
7
  try {
7
8
  const body = await req.json();
@@ -88,7 +89,7 @@ async function POST(req) {
88
89
  });
89
90
  }
90
91
  catch (error) {
91
- console.error('[VALIDATE_PASSWORD] Error:', error);
92
+ console.error('[VALIDATE_PASSWORD] Error:', (0, log_safe_1.errForLog)(error));
92
93
  return server_1.NextResponse.json({
93
94
  is_valid: false,
94
95
  score: 0,
@@ -16,6 +16,7 @@ const test_aware_get_token_1 = require("../../lib/test-aware-get-token");
16
16
  const session_store_1 = require("../../lib/session-store");
17
17
  const jwt_decode_1 = require("../../lib/jwt-decode");
18
18
  const two_factor_verify_failure_1 = require("../../lib/two-factor-verify-failure");
19
+ const log_safe_1 = require("../../lib/log-safe");
19
20
  async function POST(req) {
20
21
  try {
21
22
  // Parse request body
@@ -108,7 +109,7 @@ async function POST(req) {
108
109
  }, { status: 200 });
109
110
  }
110
111
  catch (error) {
111
- console.error('[VERIFY_EMAIL] Error:', error);
112
+ console.error('[VERIFY_EMAIL] Error:', (0, log_safe_1.errForLog)(error));
112
113
  return server_1.NextResponse.json({
113
114
  success: false,
114
115
  error: 'Failed to verify code',
@@ -16,6 +16,7 @@ const test_aware_get_token_1 = require("../../lib/test-aware-get-token");
16
16
  const session_store_1 = require("../../lib/session-store");
17
17
  const jwt_decode_1 = require("../../lib/jwt-decode");
18
18
  const two_factor_verify_failure_1 = require("../../lib/two-factor-verify-failure");
19
+ const log_safe_1 = require("../../lib/log-safe");
19
20
  async function POST(req) {
20
21
  try {
21
22
  // Parse request body
@@ -108,7 +109,7 @@ async function POST(req) {
108
109
  }, { status: 200 });
109
110
  }
110
111
  catch (error) {
111
- console.error('[VERIFY_SMS] Error:', error);
112
+ console.error('[VERIFY_SMS] Error:', (0, log_safe_1.errForLog)(error));
112
113
  return server_1.NextResponse.json({
113
114
  success: false,
114
115
  error: 'Failed to verify code',
@@ -13,6 +13,7 @@ exports.POST = POST;
13
13
  const server_1 = require("next/server");
14
14
  const headers_1 = require("next/headers");
15
15
  const anon_session_1 = require("../../lib/anon-session");
16
+ const log_safe_1 = require("../../lib/log-safe");
16
17
  const COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds
17
18
  /**
18
19
  * GET handler - retrieves anonymous session preferences
@@ -45,7 +46,7 @@ async function GET(request) {
45
46
  return response;
46
47
  }
47
48
  catch (error) {
48
- console.error('[ANON-PREFERENCES] GET error:', error);
49
+ console.error('[ANON-PREFERENCES] GET error:', (0, log_safe_1.errForLog)(error));
49
50
  return server_1.NextResponse.json({ success: false, error: 'Failed to get preferences' }, { status: 500 });
50
51
  }
51
52
  }
@@ -90,7 +91,7 @@ async function POST(request) {
90
91
  return response;
91
92
  }
92
93
  catch (error) {
93
- console.error('[ANON-PREFERENCES] POST error:', error);
94
+ console.error('[ANON-PREFERENCES] POST error:', (0, log_safe_1.errForLog)(error));
94
95
  return server_1.NextResponse.json({ success: false, error: 'Failed to update preferences' }, { status: 500 });
95
96
  }
96
97
  }
@@ -5,6 +5,7 @@ const server_1 = require("next/server");
5
5
  const idp_client_config_1 = require("../../lib/idp-client-config");
6
6
  const client_key_1 = require("../../lib/client-key");
7
7
  const bearer_fetch_1 = require("../../lib/bearer-fetch");
8
+ const log_safe_1 = require("../../lib/log-safe");
8
9
  // `export const runtime = 'nodejs'` must be written LITERALLY in the app's route file (Next ignores a re-exported segment config).
9
10
  /** One email-code request per this many seconds per browser (the IdP rate-limits too; this stops a double click and a loop). */
10
11
  const EMAIL_CODE_COOLDOWN_S = 30;
@@ -39,7 +40,7 @@ async function POST(req) {
39
40
  clientKey = (0, client_key_1.resolveClientKey)(config, 'graft/admin-login/email-code').key ?? '';
40
41
  }
41
42
  catch (err) {
42
- console.error('[ADMIN_LOGIN_EMAIL_CODE] IDP client config unavailable:', err instanceof Error ? err.message : String(err));
43
+ console.error('[ADMIN_LOGIN_EMAIL_CODE] IDP client config unavailable:', (0, log_safe_1.errForLog)(err));
43
44
  return server_1.NextResponse.json({ success: false, error: 'Sign-in is temporarily unavailable. This is a problem on our side. Please try again shortly.' }, { status: 503 });
44
45
  }
45
46
  let sendOk = false;
@@ -70,7 +71,7 @@ async function POST(req) {
70
71
  }
71
72
  }
72
73
  catch (err) {
73
- console.error('[ADMIN_LOGIN_EMAIL_CODE] twofa/email/send fetch failed:', err instanceof Error ? err.message : String(err));
74
+ console.error('[ADMIN_LOGIN_EMAIL_CODE] twofa/email/send fetch failed:', (0, log_safe_1.errForLog)(err));
74
75
  }
75
76
  if (!sendOk) {
76
77
  return server_1.NextResponse.json({ success: false, error: 'We could not ask for an email code just now - this is a problem on our side. Try again shortly.' }, { status: 502 });
@@ -50,6 +50,7 @@ const crypto_1 = require("crypto");
50
50
  const device_id_1 = require("../../lib/device-id");
51
51
  const twofa_rejection_1 = require("../../lib/twofa-rejection");
52
52
  const bearer_fetch_1 = require("../../lib/bearer-fetch");
53
+ const log_safe_1 = require("../../lib/log-safe");
53
54
  // `export const runtime = 'nodejs'` must be written LITERALLY in the app's route file (Next ignores a re-exported segment config).
54
55
  /**
55
56
  * The device id to forward to the IDP for THIS caller — card 240566.
@@ -171,7 +172,7 @@ async function POST(req) {
171
172
  config = await (0, idp_client_config_1.getIDPClientConfig)();
172
173
  }
173
174
  catch (err) {
174
- console.error('[ADMIN_LOGIN] IDP client config unavailable — returning 503:', err);
175
+ console.error('[ADMIN_LOGIN] IDP client config unavailable — returning 503:', (0, log_safe_1.errForLog)(err));
175
176
  return server_1.NextResponse.json({
176
177
  success: false,
177
178
  error: 'Sign-in is temporarily unavailable. This is a problem on our side, not with your details. Please try again shortly.',
@@ -265,7 +266,7 @@ async function POST(req) {
265
266
  }
266
267
  }
267
268
  catch (error) {
268
- console.error('[ADMIN-LOGIN] twofa verify fetch failed:', error instanceof Error ? error.message : String(error));
269
+ console.error('[ADMIN-LOGIN] twofa verify fetch failed:', (0, log_safe_1.errForLog)(error));
269
270
  return server_1.NextResponse.json({ success: false, error: 'Unable to reach the authentication service' }, { status: 503 });
270
271
  }
271
272
  if (!idpResponse.ok) {
@@ -350,7 +351,7 @@ async function POST(req) {
350
351
  });
351
352
  }
352
353
  catch (error) {
353
- console.error('[ADMIN-LOGIN] IDP fetch failed:', error instanceof Error ? error.message : String(error));
354
+ console.error('[ADMIN-LOGIN] IDP fetch failed:', (0, log_safe_1.errForLog)(error));
354
355
  return server_1.NextResponse.json({ success: false, error: 'Unable to reach the authentication service' }, { status: 503 });
355
356
  }
356
357
  const rawText = await idpResponse.text();
@@ -446,7 +447,7 @@ async function POST(req) {
446
447
  }
447
448
  }
448
449
  catch (err) {
449
- console.warn('[ADMIN-LOGIN] mfa/methods fetch failed, defaulting to email lane:', err instanceof Error ? err.message : String(err));
450
+ console.warn('[ADMIN-LOGIN] mfa/methods fetch failed, defaulting to email lane:', (0, log_safe_1.errForLog)(err));
450
451
  }
451
452
  }
452
453
  // PAY-2314: advertise what this user can actually use, derived from the SAME usableFactors that chose the lane
@@ -523,7 +524,7 @@ async function POST(req) {
523
524
  }
524
525
  }
525
526
  catch (err) {
526
- console.error(`[ADMIN-LOGIN] twofa/${sendLane}/send fetch failed:`, err instanceof Error ? err.message : String(err));
527
+ console.error(`[ADMIN-LOGIN] twofa/${sendLane}/send fetch failed:`, (0, log_safe_1.errForLog)(err));
527
528
  }
528
529
  }
529
530
  // 267479: `method:'email'` WAS A HARDCODED LITERAL HERE, and it was a lie for
@@ -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,38 +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
- status: number;
70
- }> | NextResponse<{
71
- refreshed: boolean;
72
- accessTokenExpires: number;
73
- hasRefreshToken: boolean;
74
- }>>;
47
+ export declare function createRefreshHandler(config: RefreshConfig): (req: NextRequest) => Promise<NextResponse<unknown>>;
75
48
  /**
76
49
  * Default POST export — drop-in for `app/api/auth/refresh/route.ts`.
77
50
  * Requires environment variables: IDP_URL, CLIENT_ID
78
51
  */
79
- export declare const POST: (req: NextRequest) => Promise<NextResponse<{
80
- error: string;
81
- code: string;
82
- }> | NextResponse<{
83
- error: any;
84
- code: any;
85
- discardToken: boolean;
86
- retryable: boolean;
87
- resolution: any;
88
- status: number;
89
- }> | NextResponse<{
90
- refreshed: boolean;
91
- accessTokenExpires: number;
92
- hasRefreshToken: boolean;
93
- }>>;
52
+ export declare const POST: (req: NextRequest) => Promise<NextResponse<unknown>>;
94
53
  export {};