@allanfsouza/aether-sdk 2.4.5 → 2.4.6

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.
@@ -3,6 +3,7 @@ import type { PlataformaClient } from "./index.js";
3
3
  /**
4
4
  * Cria uma instância do Axios pré-configurada.
5
5
  * - Injeta automaticamente headers de Auth e Projeto.
6
+ * - Renova token automaticamente quando expira (401).
6
7
  * - Converte erros em AetherError.
7
8
  */
8
9
  export declare function createHttpClient(client: PlataformaClient): AxiosInstance;
@@ -1,20 +1,41 @@
1
1
  // src/http-client.ts
2
2
  import axios from "axios";
3
3
  import { handleAxiosError } from "./errors.js";
4
+ // Flag para evitar múltiplos refreshes simultâneos
5
+ let isRefreshing = false;
6
+ // Fila de requisições aguardando o refresh
7
+ let failedQueue = [];
8
+ /**
9
+ * Processa a fila de requisições após o refresh.
10
+ */
11
+ const processQueue = (error, token = null) => {
12
+ failedQueue.forEach((prom) => {
13
+ if (error) {
14
+ prom.reject(error);
15
+ }
16
+ else {
17
+ prom.resolve(token);
18
+ }
19
+ });
20
+ failedQueue = [];
21
+ };
4
22
  /**
5
23
  * Cria uma instância do Axios pré-configurada.
6
24
  * - Injeta automaticamente headers de Auth e Projeto.
25
+ * - Renova token automaticamente quando expira (401).
7
26
  * - Converte erros em AetherError.
8
27
  */
9
28
  export function createHttpClient(client) {
10
29
  const http = axios.create({
11
- // Adiciona o /v1 automaticamente em todas as chamadas do SDK
12
30
  baseURL: `${client.apiUrl}/v1`,
13
31
  headers: {
14
32
  "Content-Type": "application/json",
15
33
  },
16
34
  });
17
- // Interceptor de REQUEST
35
+ // ===========================================================================
36
+ // INTERCEPTOR DE REQUEST
37
+ // Injeta token e projectId em todas as requisições
38
+ // ===========================================================================
18
39
  http.interceptors.request.use((config) => {
19
40
  const token = client.getToken();
20
41
  if (token) {
@@ -25,7 +46,73 @@ export function createHttpClient(client) {
25
46
  }
26
47
  return config;
27
48
  });
28
- // Interceptor de RESPONSE
29
- http.interceptors.response.use((response) => response, (error) => handleAxiosError(error));
49
+ // ===========================================================================
50
+ // INTERCEPTOR DE RESPONSE
51
+ // Detecta 401 e tenta refresh automático do token
52
+ // ===========================================================================
53
+ http.interceptors.response.use((response) => response, async (error) => {
54
+ const originalRequest = error.config;
55
+ // Verifica se é erro 401 (não autorizado) e não é retry
56
+ const isUnauthorized = error.response?.status === 401;
57
+ const isRetry = originalRequest?._retry;
58
+ const isAuthRoute = originalRequest?.url?.includes("/auth/");
59
+ // Não tenta refresh se:
60
+ // - Não é 401
61
+ // - Já é um retry
62
+ // - É uma rota de auth (login, register, refresh)
63
+ // - Não tem refresh token disponível
64
+ if (!isUnauthorized || isRetry || isAuthRoute) {
65
+ return handleAxiosError(error);
66
+ }
67
+ const refreshToken = client.getRefreshToken();
68
+ if (!refreshToken) {
69
+ // Sem refresh token, limpa sessão e propaga erro
70
+ client.clearSession();
71
+ return handleAxiosError(error);
72
+ }
73
+ // Se já está fazendo refresh, aguarda na fila
74
+ if (isRefreshing) {
75
+ return new Promise((resolve, reject) => {
76
+ failedQueue.push({ resolve, reject });
77
+ })
78
+ .then((token) => {
79
+ originalRequest.headers.Authorization = `Bearer ${token}`;
80
+ return http(originalRequest);
81
+ })
82
+ .catch((err) => handleAxiosError(err));
83
+ }
84
+ // Marca como retry e inicia refresh
85
+ originalRequest._retry = true;
86
+ isRefreshing = true;
87
+ try {
88
+ // Chama endpoint de refresh diretamente (sem interceptor)
89
+ const { data } = await axios.post(`${client.apiUrl}/v1/auth/refresh`, { refreshToken }, { headers: { "Content-Type": "application/json" } });
90
+ const newAccessToken = data.accessToken;
91
+ const newRefreshToken = data.refreshToken;
92
+ // Atualiza tokens no client (persiste no localStorage)
93
+ client.setToken(newAccessToken);
94
+ if (newRefreshToken) {
95
+ client.setRefreshToken(newRefreshToken);
96
+ }
97
+ // Processa fila de requisições pendentes
98
+ processQueue(null, newAccessToken);
99
+ // Refaz a requisição original com novo token
100
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
101
+ return http(originalRequest);
102
+ }
103
+ catch (refreshError) {
104
+ // Refresh falhou - sessão expirada
105
+ processQueue(refreshError, null);
106
+ client.clearSession();
107
+ // Emite evento de sessão expirada (se houver listener)
108
+ if (typeof window !== "undefined") {
109
+ window.dispatchEvent(new CustomEvent("aether:session-expired"));
110
+ }
111
+ return handleAxiosError(refreshError);
112
+ }
113
+ finally {
114
+ isRefreshing = false;
115
+ }
116
+ });
30
117
  return http;
31
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allanfsouza/aether-sdk",
3
- "version": "2.4.5",
3
+ "version": "2.4.6",
4
4
  "description": "SDK do Cliente para a Plataforma Aether",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,4 +32,4 @@
32
32
  "@types/ws": "^8.5.10",
33
33
  "typescript": "^5.3.0"
34
34
  }
35
- }
35
+ }
@@ -1,23 +1,53 @@
1
1
  // src/http-client.ts
2
- import axios, { type AxiosInstance } from "axios";
2
+ import axios, {
3
+ type AxiosInstance,
4
+ type AxiosError,
5
+ type InternalAxiosRequestConfig,
6
+ } from "axios";
3
7
  import type { PlataformaClient } from "./index.js";
4
8
  import { handleAxiosError } from "./errors.js";
5
9
 
10
+ // Flag para evitar múltiplos refreshes simultâneos
11
+ let isRefreshing = false;
12
+
13
+ // Fila de requisições aguardando o refresh
14
+ let failedQueue: Array<{
15
+ resolve: (token: string) => void;
16
+ reject: (error: any) => void;
17
+ }> = [];
18
+
19
+ /**
20
+ * Processa a fila de requisições após o refresh.
21
+ */
22
+ const processQueue = (error: any, token: string | null = null) => {
23
+ failedQueue.forEach((prom) => {
24
+ if (error) {
25
+ prom.reject(error);
26
+ } else {
27
+ prom.resolve(token!);
28
+ }
29
+ });
30
+ failedQueue = [];
31
+ };
32
+
6
33
  /**
7
34
  * Cria uma instância do Axios pré-configurada.
8
35
  * - Injeta automaticamente headers de Auth e Projeto.
36
+ * - Renova token automaticamente quando expira (401).
9
37
  * - Converte erros em AetherError.
10
38
  */
11
39
  export function createHttpClient(client: PlataformaClient): AxiosInstance {
12
40
  const http = axios.create({
13
- // Adiciona o /v1 automaticamente em todas as chamadas do SDK
14
41
  baseURL: `${client.apiUrl}/v1`,
15
42
  headers: {
16
43
  "Content-Type": "application/json",
17
44
  },
18
45
  });
19
46
 
20
- // Interceptor de REQUEST
47
+ // ===========================================================================
48
+ // INTERCEPTOR DE REQUEST
49
+ // Injeta token e projectId em todas as requisições
50
+ // ===========================================================================
21
51
  http.interceptors.request.use((config) => {
22
52
  const token = client.getToken();
23
53
  if (token) {
@@ -31,10 +61,92 @@ export function createHttpClient(client: PlataformaClient): AxiosInstance {
31
61
  return config;
32
62
  });
33
63
 
34
- // Interceptor de RESPONSE
64
+ // ===========================================================================
65
+ // INTERCEPTOR DE RESPONSE
66
+ // Detecta 401 e tenta refresh automático do token
67
+ // ===========================================================================
35
68
  http.interceptors.response.use(
36
69
  (response) => response,
37
- (error) => handleAxiosError(error)
70
+ async (error: AxiosError) => {
71
+ const originalRequest = error.config as InternalAxiosRequestConfig & {
72
+ _retry?: boolean;
73
+ };
74
+
75
+ // Verifica se é erro 401 (não autorizado) e não é retry
76
+ const isUnauthorized = error.response?.status === 401;
77
+ const isRetry = originalRequest?._retry;
78
+ const isAuthRoute = originalRequest?.url?.includes("/auth/");
79
+
80
+ // Não tenta refresh se:
81
+ // - Não é 401
82
+ // - Já é um retry
83
+ // - É uma rota de auth (login, register, refresh)
84
+ // - Não tem refresh token disponível
85
+ if (!isUnauthorized || isRetry || isAuthRoute) {
86
+ return handleAxiosError(error);
87
+ }
88
+
89
+ const refreshToken = client.getRefreshToken();
90
+ if (!refreshToken) {
91
+ // Sem refresh token, limpa sessão e propaga erro
92
+ client.clearSession();
93
+ return handleAxiosError(error);
94
+ }
95
+
96
+ // Se já está fazendo refresh, aguarda na fila
97
+ if (isRefreshing) {
98
+ return new Promise((resolve, reject) => {
99
+ failedQueue.push({ resolve, reject });
100
+ })
101
+ .then((token) => {
102
+ originalRequest.headers.Authorization = `Bearer ${token}`;
103
+ return http(originalRequest);
104
+ })
105
+ .catch((err) => handleAxiosError(err));
106
+ }
107
+
108
+ // Marca como retry e inicia refresh
109
+ originalRequest._retry = true;
110
+ isRefreshing = true;
111
+
112
+ try {
113
+ // Chama endpoint de refresh diretamente (sem interceptor)
114
+ const { data } = await axios.post(
115
+ `${client.apiUrl}/v1/auth/refresh`,
116
+ { refreshToken },
117
+ { headers: { "Content-Type": "application/json" } }
118
+ );
119
+
120
+ const newAccessToken = data.accessToken;
121
+ const newRefreshToken = data.refreshToken;
122
+
123
+ // Atualiza tokens no client (persiste no localStorage)
124
+ client.setToken(newAccessToken);
125
+ if (newRefreshToken) {
126
+ client.setRefreshToken(newRefreshToken);
127
+ }
128
+
129
+ // Processa fila de requisições pendentes
130
+ processQueue(null, newAccessToken);
131
+
132
+ // Refaz a requisição original com novo token
133
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
134
+ return http(originalRequest);
135
+ } catch (refreshError: any) {
136
+ // Refresh falhou - sessão expirada
137
+ processQueue(refreshError, null);
138
+ client.clearSession();
139
+
140
+ // Emite evento de sessão expirada (se houver listener)
141
+ if (typeof window !== "undefined") {
142
+ window.dispatchEvent(new CustomEvent("aether:session-expired"));
143
+ }
144
+
145
+ return handleAxiosError(refreshError);
146
+ } finally {
147
+ isRefreshing = false;
148
+ }
149
+ }
38
150
  );
39
151
 
40
152
  return http;