@adonis-agora/authkit-client 0.18.1 → 0.18.2

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.
@@ -23,6 +23,8 @@ function readParked(value) {
23
23
  export class AuthkitClientManager {
24
24
  config;
25
25
  #resolverCache;
26
+ /** Mapa de promessas de refresh in-flight por sessionId (dedup thundering herd). */
27
+ #refreshPromises = new Map();
26
28
  constructor(config) {
27
29
  this.config = config;
28
30
  }
@@ -72,15 +74,28 @@ export class AuthkitClientManager {
72
74
  // Sem expiresAt conhecido, não renova proativamente (evita refresh a cada request).
73
75
  if (!tokenSet.expiresAt || tokenSet.expiresAt - now() > REFRESH_SKEW_MS)
74
76
  return;
75
- try {
76
- const next = await refreshTokens({
77
- issuer: this.config.issuer,
78
- clientId: this.config.clientId,
79
- clientSecret: this.config.clientSecret,
80
- refreshToken: tokenSet.refreshToken,
81
- fetchImpl: deps.fetchImpl,
82
- resilience: this.config.resilience,
83
- });
77
+ // Dedup: se já há um refresh in-flight pra esta sessão, espera ele ao invés de
78
+ // disparar outro. O refresh token ROTACIONA — N requests concorrentes chegando
79
+ // juntos fariam N refreshes + N-1 falhas com refresh token já inválido.
80
+ const sessionId = session.sessionId;
81
+ if (sessionId) {
82
+ const pending = this.#refreshPromises.get(sessionId);
83
+ if (pending) {
84
+ await pending.catch(() => { });
85
+ return;
86
+ }
87
+ }
88
+ // Promise que SEMPRE resolve — o catch silencioso mantém o TokenSet original
89
+ // em caso de falha, igual ao comportamento do maybeRefresh original.
90
+ const promise = refreshTokens({
91
+ issuer: this.config.issuer,
92
+ clientId: this.config.clientId,
93
+ clientSecret: this.config.clientSecret,
94
+ refreshToken: tokenSet.refreshToken,
95
+ fetchImpl: deps.fetchImpl,
96
+ resilience: this.config.resilience,
97
+ })
98
+ .then((next) => {
84
99
  session.put(this.config.sessionKey, {
85
100
  idToken: next.idToken || tokenSet.idToken,
86
101
  accessToken: next.accessToken,
@@ -88,10 +103,15 @@ export class AuthkitClientManager {
88
103
  expiresAt: next.expiresAt,
89
104
  impersonationBinding: tokenSet.impersonationBinding,
90
105
  });
91
- }
92
- catch {
106
+ })
107
+ .catch(() => {
93
108
  // Renovação falhou — deixa o TokenSet como está; o resolver decide a sessão.
94
- }
109
+ });
110
+ if (sessionId)
111
+ this.#refreshPromises.set(sessionId, promise);
112
+ await promise;
113
+ if (sessionId)
114
+ this.#refreshPromises.delete(sessionId);
95
115
  }
96
116
  /**
97
117
  * Recebe o POST `application/x-www-form-urlencoded` de OIDC Back-Channel Logout. Lê o
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -96,7 +96,7 @@
96
96
  "ioredis-mock": "8.13.1",
97
97
  "@types/node": "25.9.1",
98
98
  "typescript": "6.0.3",
99
- "@adonis-agora/authkit-server": "0.61.0"
99
+ "@adonis-agora/authkit-server": "0.61.3"
100
100
  },
101
101
  "scripts": {
102
102
  "build": "tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\"",