@adonis-agora/authkit-client 0.18.8 → 0.18.9

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.
@@ -28,6 +28,13 @@ export declare class AuthkitClientManager {
28
28
  * Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
29
29
  * sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
30
30
  *
31
+ * Dedup por sessionId: requests concorrentes da mesma sessão (ex.: autosave + vários
32
+ * GETs chegando juntos quando o access token vence) disparam UM refresh só. Quem
33
+ * chegou depois espera o refresh in-flight e grava o TokenSet renovado na SUA
34
+ * PRÓPRIA sessão — cada request tem seu objeto de sessão, e sem isso ele seguiria
35
+ * com o access token vencido (→ 401) e com o refresh token antigo, já invalidado
36
+ * pela rotação, que ele poderia gravar de volta e envenenar a sessão.
37
+ *
31
38
  * Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
32
39
  * lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
33
40
  */
@@ -23,7 +23,11 @@ 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). */
26
+ /**
27
+ * Refreshes in-flight por sessionId (dedup thundering herd). A promise resolve com o
28
+ * TokenSet renovado (já mesclado) ou `null` se falhou — nunca rejeita. Guarda também
29
+ * o refresh token de origem, pra quem espera saber se é da mesma linhagem.
30
+ */
27
31
  #refreshPromises = new Map();
28
32
  constructor(config) {
29
33
  this.config = config;
@@ -60,6 +64,13 @@ export class AuthkitClientManager {
60
64
  * Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
61
65
  * sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
62
66
  *
67
+ * Dedup por sessionId: requests concorrentes da mesma sessão (ex.: autosave + vários
68
+ * GETs chegando juntos quando o access token vence) disparam UM refresh só. Quem
69
+ * chegou depois espera o refresh in-flight e grava o TokenSet renovado na SUA
70
+ * PRÓPRIA sessão — cada request tem seu objeto de sessão, e sem isso ele seguiria
71
+ * com o access token vencido (→ 401) e com o refresh token antigo, já invalidado
72
+ * pela rotação, que ele poderia gravar de volta e envenenar a sessão.
73
+ *
63
74
  * Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
64
75
  * lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
65
76
  */
@@ -79,14 +90,27 @@ export class AuthkitClientManager {
79
90
  // juntos fariam N refreshes + N-1 falhas com refresh token já inválido.
80
91
  const sessionId = session.sessionId;
81
92
  if (sessionId) {
82
- const pending = this.#refreshPromises.get(sessionId);
83
- if (pending) {
84
- await pending.catch(() => { });
93
+ const inFlight = this.#refreshPromises.get(sessionId);
94
+ // Só aproveita o refresh in-flight se ele partiu do MESMO refresh token que esta
95
+ // request tem — senão o TokenSet renovado é de outra linhagem (ex.: a sessão
96
+ // trocou de token set no meio do caminho) e não pode ser gravado aqui.
97
+ if (inFlight && inFlight.refreshToken === tokenSet.refreshToken) {
98
+ const refreshed = await inFlight.promise;
99
+ if (refreshed) {
100
+ session.put(this.config.sessionKey, {
101
+ ...refreshed,
102
+ // O vínculo é por sessão (mesmo sessionId → mesmo vínculo); mantém o desta
103
+ // request e só cai no do refresh se esta não tiver.
104
+ impersonationBinding: tokenSet.impersonationBinding ?? refreshed.impersonationBinding,
105
+ });
106
+ }
85
107
  return;
86
108
  }
109
+ // Linhagem diferente: segue e faz o próprio refresh (a entrada do mapa passa a
110
+ // ser a desta request; a anterior não a apaga — ver o `finally`).
87
111
  }
88
- // Promise que SEMPRE resolve — o catch silencioso mantém o TokenSet original
89
- // em caso de falha, igual ao comportamento do maybeRefresh original.
112
+ // Promise que NUNCA rejeita: resolve com o TokenSet gravado, ou `null` se a
113
+ // renovação falhou — aí o TokenSet original fica como está (o resolver decide).
90
114
  const promise = refreshTokens({
91
115
  issuer: this.config.issuer,
92
116
  clientId: this.config.clientId,
@@ -96,22 +120,31 @@ export class AuthkitClientManager {
96
120
  resilience: this.config.resilience,
97
121
  })
98
122
  .then((next) => {
99
- session.put(this.config.sessionKey, {
123
+ const merged = {
100
124
  idToken: next.idToken || tokenSet.idToken,
101
125
  accessToken: next.accessToken,
102
126
  refreshToken: next.refreshToken ?? tokenSet.refreshToken,
103
127
  expiresAt: next.expiresAt,
104
128
  impersonationBinding: tokenSet.impersonationBinding,
105
- });
129
+ };
130
+ session.put(this.config.sessionKey, merged);
131
+ return merged;
106
132
  })
107
- .catch(() => {
108
- // Renovação falhou — deixa o TokenSet como está; o resolver decide a sessão.
109
- });
110
- if (sessionId)
111
- this.#refreshPromises.set(sessionId, promise);
112
- await promise;
113
- if (sessionId)
114
- this.#refreshPromises.delete(sessionId);
133
+ .catch(() => null);
134
+ if (!sessionId) {
135
+ await promise;
136
+ return;
137
+ }
138
+ const entry = { refreshToken: tokenSet.refreshToken, promise };
139
+ this.#refreshPromises.set(sessionId, entry);
140
+ try {
141
+ await promise;
142
+ }
143
+ finally {
144
+ // Só remove se a entrada ainda é a nossa.
145
+ if (this.#refreshPromises.get(sessionId) === entry)
146
+ this.#refreshPromises.delete(sessionId);
147
+ }
115
148
  }
116
149
  /**
117
150
  * 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.8",
3
+ "version": "0.18.9",
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",
@@ -83,7 +83,7 @@
83
83
  "jose": "6.2.12"
84
84
  },
85
85
  "devDependencies": {
86
- "@adonis-agora/authkit-server": "0.74.0",
86
+ "@adonis-agora/authkit-server": "0.80.0",
87
87
  "@adonis-agora/resilience": "0.3.7",
88
88
  "@adonisjs/auth": "10.1.0",
89
89
  "@adonisjs/core": "7.5.0",