@allanfsouza/aether-sdk 2.4.6 → 2.4.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.
@@ -1,9 +1,27 @@
1
- import { type AxiosInstance } from "axios";
1
+ import { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from "axios";
2
2
  import type { PlataformaClient } from "./index.js";
3
+ export interface RetryConfig {
4
+ /** Número máximo de tentativas (padrão: 3) */
5
+ maxRetries: number;
6
+ /** Delay base em ms para exponential backoff (padrão: 1000) */
7
+ baseDelay: number;
8
+ /** Delay máximo em ms (padrão: 30000) */
9
+ maxDelay: number;
10
+ /** Status codes que devem ser retried (padrão: [408, 429, 500, 502, 503, 504]) */
11
+ retryableStatuses: number[];
12
+ /** Métodos HTTP que podem ser retried (padrão: GET, HEAD, OPTIONS, PUT, DELETE) */
13
+ retryableMethods: string[];
14
+ /** Habilita retry para erros de rede (padrão: true) */
15
+ retryOnNetworkError: boolean;
16
+ /** Callback chamado antes de cada retry */
17
+ onRetry?: (retryCount: number, error: AxiosError, config: AxiosRequestConfig) => void;
18
+ }
19
+ export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
3
20
  /**
4
- * Cria uma instância do Axios pré-configurada.
5
- * - Injeta automaticamente headers de Auth e Projeto.
6
- * - Renova token automaticamente quando expira (401).
7
- * - Converte erros em AetherError.
21
+ * Cria uma instância do Axios pré-configurada com:
22
+ * - Injeção automática de headers (Auth, Project)
23
+ * - Refresh automático de token (401)
24
+ * - Retry automático com exponential backoff
25
+ * - Tratamento de erros padronizado
8
26
  */
9
- export declare function createHttpClient(client: PlataformaClient): AxiosInstance;
27
+ export declare function createHttpClient(client: PlataformaClient, retryConfig?: Partial<RetryConfig>): AxiosInstance;
@@ -1,13 +1,88 @@
1
1
  // src/http-client.ts
2
+ // [MELHORIA] SDK com Retry Automático e Exponential Backoff
3
+ // Data: 09/12/2025
2
4
  import axios from "axios";
3
5
  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 = [];
6
+ export const DEFAULT_RETRY_CONFIG = {
7
+ maxRetries: 3,
8
+ baseDelay: 1000,
9
+ maxDelay: 30000,
10
+ retryableStatuses: [408, 429, 500, 502, 503, 504],
11
+ retryableMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
12
+ retryOnNetworkError: true,
13
+ };
14
+ // =============================================================================
15
+ // HELPERS
16
+ // =============================================================================
17
+ /**
18
+ * Calcula o delay com exponential backoff + jitter
19
+ */
20
+ function calculateDelay(retryCount, config) {
21
+ // Exponential: baseDelay * 2^retryCount
22
+ const exponentialDelay = config.baseDelay * Math.pow(2, retryCount);
23
+ // Adiciona jitter (±25%) para evitar thundering herd
24
+ const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
25
+ // Limita ao maxDelay
26
+ return Math.min(exponentialDelay + jitter, config.maxDelay);
27
+ }
28
+ /**
29
+ * Verifica se o erro é retryable
30
+ */
31
+ function isRetryable(error, config) {
32
+ // Erro de rede (sem response)
33
+ if (!error.response && config.retryOnNetworkError) {
34
+ // Verifica se é erro de rede real (não cancelamento)
35
+ if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT" ||
36
+ error.code === "ENOTFOUND" || error.code === "ENETUNREACH" ||
37
+ error.message === "Network Error") {
38
+ return true;
39
+ }
40
+ }
41
+ // Verifica status code
42
+ const status = error.response?.status;
43
+ if (status && config.retryableStatuses.includes(status)) {
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ /**
49
+ * Verifica se o método HTTP pode ser retried
50
+ */
51
+ function isMethodRetryable(method, config) {
52
+ if (!method)
53
+ return false;
54
+ return config.retryableMethods.includes(method.toUpperCase());
55
+ }
56
+ /**
57
+ * Extrai Retry-After header (em ms)
58
+ */
59
+ function getRetryAfterMs(error) {
60
+ const retryAfter = error.response?.headers?.["retry-after"];
61
+ if (!retryAfter)
62
+ return null;
63
+ // Se for número, é segundos
64
+ const seconds = parseInt(retryAfter, 10);
65
+ if (!isNaN(seconds)) {
66
+ return seconds * 1000;
67
+ }
68
+ // Se for data HTTP, calcula diferença
69
+ const date = new Date(retryAfter);
70
+ if (!isNaN(date.getTime())) {
71
+ return Math.max(0, date.getTime() - Date.now());
72
+ }
73
+ return null;
74
+ }
8
75
  /**
9
- * Processa a fila de requisições após o refresh.
76
+ * Aguarda um tempo determinado
10
77
  */
78
+ function sleep(ms) {
79
+ return new Promise(resolve => setTimeout(resolve, ms));
80
+ }
81
+ // =============================================================================
82
+ // FLAGS GLOBAIS (Refresh Token)
83
+ // =============================================================================
84
+ let isRefreshing = false;
85
+ let failedQueue = [];
11
86
  const processQueue = (error, token = null) => {
12
87
  failedQueue.forEach((prom) => {
13
88
  if (error) {
@@ -19,15 +94,21 @@ const processQueue = (error, token = null) => {
19
94
  });
20
95
  failedQueue = [];
21
96
  };
97
+ // =============================================================================
98
+ // CRIAR HTTP CLIENT
99
+ // =============================================================================
22
100
  /**
23
- * Cria uma instância do Axios pré-configurada.
24
- * - Injeta automaticamente headers de Auth e Projeto.
25
- * - Renova token automaticamente quando expira (401).
26
- * - Converte erros em AetherError.
101
+ * Cria uma instância do Axios pré-configurada com:
102
+ * - Injeção automática de headers (Auth, Project)
103
+ * - Refresh automático de token (401)
104
+ * - Retry automático com exponential backoff
105
+ * - Tratamento de erros padronizado
27
106
  */
28
- export function createHttpClient(client) {
107
+ export function createHttpClient(client, retryConfig = {}) {
108
+ const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
29
109
  const http = axios.create({
30
110
  baseURL: `${client.apiUrl}/v1`,
111
+ timeout: 30000, // 30 segundos
31
112
  headers: {
32
113
  "Content-Type": "application/json",
33
114
  },
@@ -36,37 +117,84 @@ export function createHttpClient(client) {
36
117
  // INTERCEPTOR DE REQUEST
37
118
  // Injeta token e projectId em todas as requisições
38
119
  // ===========================================================================
39
- http.interceptors.request.use((config) => {
120
+ http.interceptors.request.use((reqConfig) => {
40
121
  const token = client.getToken();
41
122
  if (token) {
42
- config.headers.Authorization = `Bearer ${token}`;
123
+ reqConfig.headers.Authorization = `Bearer ${token}`;
43
124
  }
44
125
  if (client.projectId) {
45
- config.headers["X-Project-ID"] = client.projectId;
126
+ reqConfig.headers["X-Project-ID"] = client.projectId;
127
+ }
128
+ // Inicializa contador de retry
129
+ if (reqConfig._retryCount === undefined) {
130
+ reqConfig._retryCount = 0;
46
131
  }
47
- return config;
132
+ return reqConfig;
48
133
  });
49
134
  // ===========================================================================
50
135
  // INTERCEPTOR DE RESPONSE
51
- // Detecta 401 e tenta refresh automático do token
136
+ // 1. Retry automático para erros de rede/servidor
137
+ // 2. Refresh automático de token (401)
52
138
  // ===========================================================================
53
139
  http.interceptors.response.use((response) => response, async (error) => {
54
140
  const originalRequest = error.config;
55
- // Verifica se é erro 401 (não autorizado) e não é retry
141
+ if (!originalRequest) {
142
+ return handleAxiosError(error);
143
+ }
144
+ // -----------------------------------------------------------------
145
+ // PARTE 1: RETRY AUTOMÁTICO (erros de rede e servidor)
146
+ // -----------------------------------------------------------------
147
+ const retryCount = originalRequest._retryCount || 0;
148
+ const canRetry = retryCount < config.maxRetries;
149
+ const isRetryableError = isRetryable(error, config);
150
+ const isRetryableMethod = isMethodRetryable(originalRequest.method, config);
151
+ // Não faz retry para 401 (tratado pelo refresh token)
152
+ const is401 = error.response?.status === 401;
153
+ if (canRetry && isRetryableError && isRetryableMethod && !is401) {
154
+ originalRequest._retryCount = retryCount + 1;
155
+ // Calcula delay (respeita Retry-After se presente)
156
+ const delay = getRetryAfterMs(error) || calculateDelay(retryCount, config);
157
+ // Callback de retry (para logging/métricas)
158
+ if (config.onRetry) {
159
+ config.onRetry(originalRequest._retryCount, error, originalRequest);
160
+ }
161
+ // Emite evento para monitoramento
162
+ if (typeof window !== "undefined") {
163
+ window.dispatchEvent(new CustomEvent("aether:retry", {
164
+ detail: {
165
+ attempt: originalRequest._retryCount,
166
+ maxRetries: config.maxRetries,
167
+ delay,
168
+ url: originalRequest.url,
169
+ method: originalRequest.method,
170
+ status: error.response?.status,
171
+ error: error.message,
172
+ }
173
+ }));
174
+ }
175
+ // Log em desenvolvimento
176
+ if (process.env.NODE_ENV === "development") {
177
+ console.log(`[Aether SDK] Retry ${originalRequest._retryCount}/${config.maxRetries}`, `em ${delay}ms para ${originalRequest.method} ${originalRequest.url}`, `(${error.response?.status || error.message})`);
178
+ }
179
+ // Aguarda e refaz a requisição
180
+ await sleep(delay);
181
+ return http(originalRequest);
182
+ }
183
+ // -----------------------------------------------------------------
184
+ // PARTE 2: REFRESH TOKEN (erro 401)
185
+ // -----------------------------------------------------------------
56
186
  const isUnauthorized = error.response?.status === 401;
57
- const isRetry = originalRequest?._retry;
58
- const isAuthRoute = originalRequest?.url?.includes("/auth/");
187
+ const isRetry = originalRequest._retry;
188
+ const isAuthRoute = originalRequest.url?.includes("/auth/");
59
189
  // Não tenta refresh se:
60
190
  // - Não é 401
61
- // - Já é um retry
191
+ // - Já é um retry de refresh
62
192
  // - É uma rota de auth (login, register, refresh)
63
- // - Não tem refresh token disponível
64
193
  if (!isUnauthorized || isRetry || isAuthRoute) {
65
194
  return handleAxiosError(error);
66
195
  }
67
196
  const refreshToken = client.getRefreshToken();
68
197
  if (!refreshToken) {
69
- // Sem refresh token, limpa sessão e propaga erro
70
198
  client.clearSession();
71
199
  return handleAxiosError(error);
72
200
  }
@@ -85,26 +213,20 @@ export function createHttpClient(client) {
85
213
  originalRequest._retry = true;
86
214
  isRefreshing = true;
87
215
  try {
88
- // Chama endpoint de refresh diretamente (sem interceptor)
89
216
  const { data } = await axios.post(`${client.apiUrl}/v1/auth/refresh`, { refreshToken }, { headers: { "Content-Type": "application/json" } });
90
217
  const newAccessToken = data.accessToken;
91
218
  const newRefreshToken = data.refreshToken;
92
- // Atualiza tokens no client (persiste no localStorage)
93
219
  client.setToken(newAccessToken);
94
220
  if (newRefreshToken) {
95
221
  client.setRefreshToken(newRefreshToken);
96
222
  }
97
- // Processa fila de requisições pendentes
98
223
  processQueue(null, newAccessToken);
99
- // Refaz a requisição original com novo token
100
224
  originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
101
225
  return http(originalRequest);
102
226
  }
103
227
  catch (refreshError) {
104
- // Refresh falhou - sessão expirada
105
228
  processQueue(refreshError, null);
106
229
  client.clearSession();
107
- // Emite evento de sessão expirada (se houver listener)
108
230
  if (typeof window !== "undefined") {
109
231
  window.dispatchEvent(new CustomEvent("aether:session-expired"));
110
232
  }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { DatabaseModule } from "./database.js";
4
4
  import { StorageModule } from "./storage.js";
5
5
  import { FunctionsModule } from "./functions.js";
6
6
  import { PushModule } from "./push.js";
7
+ import { TenantAuthModule } from "./tenant-auth.js";
7
8
  /**
8
9
  * Configuração usada para criar o cliente principal da plataforma.
9
10
  */
@@ -24,6 +25,7 @@ export declare class PlataformaClient {
24
25
  storage: StorageModule;
25
26
  functions: FunctionsModule;
26
27
  push: PushModule;
28
+ tenantAuth: TenantAuthModule;
27
29
  database: DatabaseModule;
28
30
  apiUrl: string;
29
31
  projectId: string;
@@ -76,3 +78,4 @@ export { AetherError } from "./errors.js";
76
78
  export type { LoginResponse, Session, User } from "./auth.js";
77
79
  export type { ListOptions } from "./database.js";
78
80
  export type { PushPlatform, PushEnvironment, PushDevice, RegisterDeviceParams, SendPushResponse, PushStatus, PushLogEntry, ListPushLogsOptions, PushStats, } from "./push.js";
81
+ export type { TenantUser, TenantLoginResponse, TenantRegisterCredentials, TenantLoginCredentials, } from "./tenant-auth.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { DatabaseModule } from "./database.js";
4
4
  import { StorageModule } from "./storage.js";
5
5
  import { FunctionsModule } from "./functions.js";
6
6
  import { PushModule } from "./push.js";
7
+ import { TenantAuthModule } from "./tenant-auth.js";
7
8
  // =============================================================================
8
9
  // CONSTANTES DE STORAGE
9
10
  // Chaves padronizadas para localStorage - evita conflito com outros SDKs
@@ -44,6 +45,7 @@ export class PlataformaClient {
44
45
  this.storage = new StorageModule(this, this.http);
45
46
  this.functions = new FunctionsModule(this, this.http);
46
47
  this.push = new PushModule(this, this.http);
48
+ this.tenantAuth = new TenantAuthModule(this, this.http);
47
49
  // Cria o alias que o Showcase App espera
48
50
  this.database = this.db;
49
51
  }
@@ -0,0 +1,172 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import type { PlataformaClient } from "./index.js";
3
+ /**
4
+ * Representa um usuário tenant (end-user de um projeto)
5
+ */
6
+ export interface TenantUser {
7
+ id: string;
8
+ email: string;
9
+ name: string;
10
+ data: Record<string, any>;
11
+ emailVerified: boolean;
12
+ status: "active" | "suspended" | string;
13
+ createdAt: string;
14
+ }
15
+ /**
16
+ * Resposta de login do tenant
17
+ */
18
+ export interface TenantLoginResponse {
19
+ accessToken: string;
20
+ user: TenantUser;
21
+ }
22
+ /**
23
+ * Credenciais para registro de tenant
24
+ */
25
+ export interface TenantRegisterCredentials {
26
+ email: string;
27
+ password: string;
28
+ name?: string;
29
+ data?: Record<string, any>;
30
+ }
31
+ /**
32
+ * Credenciais para login de tenant
33
+ */
34
+ export interface TenantLoginCredentials {
35
+ email: string;
36
+ password: string;
37
+ }
38
+ /**
39
+ * Módulo de Autenticação de Tenants
40
+ *
41
+ * Permite que usuários finais (clientes) de projetos Aether
42
+ * se registrem, façam login e gerenciem seus perfis.
43
+ *
44
+ * Os dados são isolados por projeto (tabela prj_{id}_users).
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const aether = new PlataformaClient({ ... });
49
+ *
50
+ * // Registrar usuário no projeto
51
+ * const { user, error } = await aether.tenantAuth.signUp('project-id', {
52
+ * email: 'user@example.com',
53
+ * password: '123456',
54
+ * name: 'John Doe',
55
+ * });
56
+ *
57
+ * // Login
58
+ * const { accessToken, user } = await aether.tenantAuth.signIn('project-id', {
59
+ * email: 'user@example.com',
60
+ * password: '123456',
61
+ * });
62
+ *
63
+ * // Obter perfil
64
+ * const profile = await aether.tenantAuth.getProfile('project-id');
65
+ * ```
66
+ */
67
+ export declare class TenantAuthModule {
68
+ private client;
69
+ private http;
70
+ private currentToken;
71
+ private currentUser;
72
+ constructor(client: PlataformaClient, http: AxiosInstance);
73
+ /**
74
+ * Registra um novo usuário tenant no projeto.
75
+ * A tabela de usuários é criada automaticamente se não existir.
76
+ *
77
+ * @param projectId - ID do projeto
78
+ * @param credentials - Email, senha, nome e dados opcionais
79
+ */
80
+ register(projectId: string, credentials: TenantRegisterCredentials): Promise<{
81
+ user: TenantUser;
82
+ message: string;
83
+ }>;
84
+ /**
85
+ * Realiza login de usuário tenant.
86
+ * Armazena o token para uso em requisições subsequentes.
87
+ *
88
+ * @param projectId - ID do projeto
89
+ * @param credentials - Email e senha
90
+ */
91
+ login(projectId: string, credentials: TenantLoginCredentials): Promise<TenantLoginResponse>;
92
+ /**
93
+ * Solicita reset de senha.
94
+ * Retorna mensagem genérica para prevenir enumeração de usuários.
95
+ *
96
+ * @param projectId - ID do projeto
97
+ * @param email - Email do usuário
98
+ */
99
+ forgotPassword(projectId: string, email: string): Promise<{
100
+ message: string;
101
+ }>;
102
+ /**
103
+ * Redefine a senha usando o token recebido por email.
104
+ *
105
+ * @param projectId - ID do projeto
106
+ * @param email - Email do usuário
107
+ * @param token - Token de reset (recebido por email)
108
+ * @param newPassword - Nova senha
109
+ */
110
+ resetPassword(projectId: string, email: string, token: string, newPassword: string): Promise<{
111
+ message: string;
112
+ }>;
113
+ /**
114
+ * Obtém o perfil do usuário autenticado.
115
+ * Requer que login() tenha sido chamado antes.
116
+ *
117
+ * @param projectId - ID do projeto (opcional, usa do token se não fornecido)
118
+ */
119
+ getProfile(projectId?: string): Promise<TenantUser>;
120
+ /**
121
+ * Atualiza o perfil do usuário autenticado.
122
+ *
123
+ * @param projectId - ID do projeto
124
+ * @param updates - Campos a atualizar (name, data)
125
+ */
126
+ updateProfile(projectId: string, updates: {
127
+ name?: string;
128
+ data?: Record<string, any>;
129
+ }): Promise<{
130
+ user: TenantUser;
131
+ message: string;
132
+ }>;
133
+ /**
134
+ * Faz logout do tenant (limpa token local).
135
+ */
136
+ logout(): void;
137
+ /**
138
+ * Alias para register com retorno { user, error }
139
+ */
140
+ signUp(projectId: string, credentials: TenantRegisterCredentials): Promise<{
141
+ user: TenantUser | null;
142
+ error: string | null;
143
+ }>;
144
+ /**
145
+ * Alias para login com retorno { user, accessToken, error }
146
+ */
147
+ signIn(projectId: string, credentials: TenantLoginCredentials): Promise<{
148
+ user: TenantUser | null;
149
+ accessToken: string | null;
150
+ error: string | null;
151
+ }>;
152
+ /**
153
+ * Alias para logout
154
+ */
155
+ signOut(): void;
156
+ /**
157
+ * Retorna o token atual do tenant
158
+ */
159
+ getToken(): string | null;
160
+ /**
161
+ * Define o token do tenant (útil para restaurar sessão)
162
+ */
163
+ setToken(token: string | null): void;
164
+ /**
165
+ * Retorna o usuário atual do tenant
166
+ */
167
+ getCurrentUser(): TenantUser | null;
168
+ /**
169
+ * Verifica se há um usuário autenticado
170
+ */
171
+ isAuthenticated(): boolean;
172
+ }