@riligar/auth-elysia 1.6.2 → 1.6.4

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.
package/dist/index.esm.js CHANGED
@@ -2,21 +2,10 @@ import 'elysia';
2
2
 
3
3
  /**
4
4
  * @module @riligar/auth-elysia
5
- * @description Auth SDK for ElysiaJS with JWT and JWKS
6
- * @version See package.json
7
- * @since 1.0.0
8
- * @copyright 2024-2026 Riligar
9
- *
10
- * @license MIT
11
- * @author Riligar
12
- * @see https://github.com/riligar-solutions/auth
13
- * @see https://www.npmjs.com/package/@riligar/auth-elysia
5
+ * @description Auth SDK for ElysiaJS
14
6
  */
15
7
 
16
8
 
17
- /**
18
- * Configuração padrão do plugin
19
- */
20
9
  const DEFAULT_CONFIG = {
21
10
  prefix: '/auth',
22
11
  secretKey: process.env.AUTH_SECRET_KEY || 'your-secret-key',
@@ -26,392 +15,159 @@ const DEFAULT_CONFIG = {
26
15
  httpOnly: true,
27
16
  secure: process.env.NODE_ENV === 'production',
28
17
  sameSite: 'lax',
29
- maxAge: 604800, // 7 dias (igual ao JWT)
18
+ maxAge: 604800,
30
19
  },
31
20
  excludePaths: ['/auth/login', '/auth/register', '/auth/session'],
32
21
  onUnauthorized: set => {
33
22
  set.status = 401;
34
- return { error: 'Unauthorized', message: 'Token inválido ou expirado' }
23
+ set.headers = { ...set.headers, 'Content-Type': 'application/json' };
24
+ return {
25
+ success: false,
26
+ error: {
27
+ code: 'UNAUTHORIZED',
28
+ message: 'Sessão inválida ou expirada. Por favor, faça login novamente.',
29
+ },
30
+ }
35
31
  },
36
32
  };
37
33
 
38
- /**
39
- * Cliente Auth com verificação JWT local + JWKS
40
- */
41
34
  class RiLiGarAuthClient {
42
35
  constructor(baseUrl, secretKey) {
43
36
  this.baseUrl = baseUrl;
44
37
  this.secretKey = secretKey;
45
- // 🚀 Cache para JWKS (chaves públicas)
46
38
  this.jwksCache = null;
47
39
  this.jwksCacheExpiry = 0;
48
- this.jwksCacheTTL = 3600000; // 1 hora em ms
49
40
  }
50
41
 
51
- // 🔑 Buscar e cachear JWKS
52
42
  async getJWKS() {
53
43
  const now = Date.now();
54
-
55
- // Retorna cache se ainda válido
56
- if (this.jwksCache && now < this.jwksCacheExpiry) {
57
- return this.jwksCache
58
- }
59
-
44
+ if (this.jwksCache && now < this.jwksCacheExpiry) return this.jwksCache
60
45
  try {
61
46
  const response = await fetch(`${this.baseUrl}/.well-known/jwks.json`);
62
47
  if (response.ok) {
63
48
  this.jwksCache = await response.json();
64
- this.jwksCacheExpiry = now + this.jwksCacheTTL;
49
+ this.jwksCacheExpiry = now + 3600000;
65
50
  return this.jwksCache
66
51
  }
67
- } catch (error) {
68
- console.warn('JWKS fetch failed, falling back to remote verification:', error.message);
69
- }
70
-
52
+ } catch (e) {}
71
53
  return null
72
54
  }
73
55
 
74
- // ⚡ Verificação JWT local (usando crypto nativo do Bun)
75
56
  async verifyJWTLocal(token) {
76
57
  try {
77
58
  const jwks = await this.getJWKS();
78
- if (!jwks?.keys?.length) return null
79
-
80
- // Decode JWT header para pegar kid e alg
81
- const [headerB64] = token.split('.');
82
- const header = JSON.parse(atob(headerB64));
83
-
84
- // Encontrar a chave correspondente
59
+ if (!jwks) return null
60
+ const [h, p, s] = token.split('.');
61
+ const header = JSON.parse(atob(h));
85
62
  const jwk = jwks.keys.find(k => k.kid === header.kid || k.alg === header.alg);
86
63
  if (!jwk) return null
87
-
88
- // Preparar dados para verificação
89
- const [headerB64Url, payloadB64Url, signatureB64Url] = token.split('.');
90
- const data = new TextEncoder().encode(`${headerB64Url}.${payloadB64Url}`);
91
-
92
- const signature = new Uint8Array(Array.from(atob(signatureB64Url.replace(/-/g, '+').replace(/_/g, '/'))).map(c => c.charCodeAt(0)));
93
-
94
- let key;
95
- let algorithm;
96
-
97
- // Suporte para diferentes algoritmos
98
- if (header.alg === 'HS256' || jwk.kty === 'oct') {
99
- // HMAC com secret key
100
- const secret = new TextEncoder().encode(this.secretKey);
101
- key = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
102
- algorithm = 'HMAC';
103
- } else if (header.alg === 'RS256' || jwk.kty === 'RSA') {
104
- // RSA com chave pública JWKS
105
- if (!jwk.n || !jwk.e) return null
106
-
107
- // Converter base64url para ArrayBuffer
108
- const nBuffer = this.base64urlToArrayBuffer(jwk.n);
109
- const eBuffer = this.base64urlToArrayBuffer(jwk.e);
110
-
111
- key = await crypto.subtle.importKey(
112
- 'jwk',
113
- {
114
- kty: 'RSA',
115
- n: jwk.n,
116
- e: jwk.e,
117
- alg: 'RS256',
118
- use: 'sig',
119
- },
120
- { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
121
- false,
122
- ['verify']
123
- );
124
- algorithm = 'RSASSA-PKCS1-v1_5';
64
+ const data = new TextEncoder().encode(`${h}.${p}`);
65
+ const sig = Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
66
+ let key, algo;
67
+ if (header.alg === 'HS256') {
68
+ key = await crypto.subtle.importKey('raw', new TextEncoder().encode(this.secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
69
+ algo = 'HMAC';
125
70
  } else {
126
- console.warn('Algoritmo JWT não suportado:', header.alg);
127
- return null
128
- }
129
-
130
- // Verificar assinatura
131
- const isValid = await crypto.subtle.verify(algorithm, key, signature, data);
132
- if (!isValid) return null
133
-
134
- // Decode payload
135
- const payload = JSON.parse(atob(payloadB64Url));
136
-
137
- // Verificar expiração
138
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
139
- return null
140
- }
141
-
142
- // Map JWT standard claims to user object format
143
- // JWT uses 'sub' for subject (user ID), but clients expect 'id'
144
- return {
145
- id: payload.sub,
146
- ...payload,
71
+ key = await crypto.subtle.importKey('jwk', { kty: 'RSA', n: jwk.n, e: jwk.e, alg: 'RS256', use: 'sig' }, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
72
+ algo = 'RSASSA-PKCS1-v1_5';
147
73
  }
148
- } catch (error) {
149
- console.warn('JWT local verification failed:', error.message);
74
+ if (!(await crypto.subtle.verify(algo, key, sig, data))) return null
75
+ const payload = JSON.parse(atob(p));
76
+ if (payload.exp && payload.exp < Date.now() / 1000) return null
77
+ return { id: payload.sub, ...payload }
78
+ } catch (e) {
150
79
  return null
151
80
  }
152
81
  }
153
82
 
154
- // Utilitário para converter base64url para ArrayBuffer
155
- base64urlToArrayBuffer(base64url) {
156
- const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
157
- const padding = '='.repeat((4 - (base64.length % 4)) % 4);
158
- const base64WithPadding = base64 + padding;
159
-
160
- const binaryString = atob(base64WithPadding);
161
- const bytes = new Uint8Array(binaryString.length);
162
- for (let i = 0; i < binaryString.length; i++) {
163
- bytes[i] = binaryString.charCodeAt(i);
164
- }
165
- return bytes.buffer
166
- }
167
-
168
- // ⚡ Verificar sessão OTIMIZADA (local + fallback remoto)
169
- async verifySession(sessionToken) {
170
- // 🚀 Primeira tentativa: Verificação local com JWKS
171
- const localResult = await this.verifyJWTLocal(sessionToken);
172
- if (localResult) {
173
- return {
174
- user: localResult,
175
- verified_locally: true,
176
- cached: true,
177
- }
178
- }
179
-
180
- // 🔄 Fallback: Verificação remota
83
+ async verifySession(token) {
84
+ const local = await this.verifyJWTLocal(token);
85
+ if (local) return { user: local }
181
86
  try {
182
- const response = await fetch(`${this.baseUrl}/auth/session`, {
183
- headers: {
184
- Authorization: `Bearer ${sessionToken}`,
185
- 'X-API-Key': this.secretKey,
186
- },
87
+ const res = await fetch(`${this.baseUrl}/auth/session`, {
88
+ headers: { Authorization: `Bearer ${token}`, 'X-API-Key': this.secretKey },
187
89
  });
188
-
189
- if (response.ok) {
190
- const result = await response.json();
191
- return {
192
- ...result,
193
- verified_locally: false,
194
- cached: false,
195
- }
196
- }
197
- } catch (error) {
198
- console.warn('Remote session verification failed:', error.message);
199
- }
200
-
90
+ if (res.ok) return await res.json()
91
+ } catch (e) {}
201
92
  return null
202
93
  }
203
94
  }
204
95
 
205
- /**
206
- * Utilitário para fazer requisições HTTP
207
- */
208
96
  async function fetchAuth(url, options = {}) {
209
- try {
210
- const response = await fetch(url, {
211
- headers: {
212
- 'Content-Type': 'application/json',
213
- ...options.headers,
214
- },
215
- ...options,
216
- });
217
-
218
- if (!response.ok) {
219
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
220
- }
221
-
222
- return await response.json()
223
- } catch (error) {
224
- console.error('Auth fetch error:', error);
225
- throw error
226
- }
97
+ const res = await fetch(url, {
98
+ headers: { 'Content-Type': 'application/json', ...options.headers },
99
+ ...options,
100
+ });
101
+ if (!res.ok) throw new Error(res.status)
102
+ return await res.json()
227
103
  }
228
104
 
229
- /**
230
- * Plugin principal de autenticação
231
- */
232
105
  function authPlugin(userConfig = {}) {
233
106
  const config = { ...DEFAULT_CONFIG, ...userConfig };
234
-
235
- // Instanciar cliente Auth
236
107
  const authClient = new RiLiGarAuthClient(config.apiUrl, config.secretKey);
237
108
 
238
- return app => {
239
- app.derive(async ({ request, cookie }) => {
240
- new URL(request.url).pathname;
241
-
242
- // Buscar token no cookie ou header Authorization
243
- let token = cookie[config.cookieName]?.value;
244
-
245
- if (!token) {
246
- const authHeader = request.headers.get('authorization');
247
- if (authHeader?.startsWith('Bearer ')) {
248
- token = authHeader.substring(7);
249
- }
250
- }
251
-
252
- if (!token) {
253
- return { user: null, authMeta: null }
254
- }
255
-
256
- try {
257
- // ⚡ Verificação otimizada com JWKS local + fallback remoto
258
- const userSession = await authClient.verifySession(token);
259
-
260
- if (!userSession) {
261
- return { user: null, authMeta: null }
262
- }
263
-
264
- return {
265
- user: userSession.user,
266
- authMeta: {
267
- verified_locally: userSession.verified_locally,
268
- cached: userSession.cached,
269
- },
109
+ return app =>
110
+ app
111
+ .derive({ as: 'global' }, async ({ request, cookie }) => {
112
+ const token = cookie[config.cookieName]?.value || request.headers.get('authorization')?.split(' ')[1];
113
+ if (!token) return { user: null }
114
+ const session = await authClient.verifySession(token);
115
+ return { user: session?.user || null }
116
+ })
117
+ .onBeforeHandle({ as: 'global' }, ({ user, set, request }) => {
118
+ const path = new URL(request.url).pathname;
119
+ if (config.excludePaths.some(p => path.startsWith(p))) return
120
+ if (!user) return config.onUnauthorized(set)
121
+ })
122
+ .error(({ error, set }) => {
123
+ const errorStr = String(error);
124
+ if (errorStr.includes('user.id') || errorStr.includes('of null') || errorStr.includes('of undefined')) {
125
+ set.status = 401;
126
+ set.headers['Content-Type'] = 'application/json';
127
+ return {
128
+ success: false,
129
+ error: {
130
+ code: 'UNAUTHORIZED',
131
+ message: 'Sessão inválida ou expirada. Por favor, faça login novamente.',
132
+ },
133
+ }
270
134
  }
271
- } catch (error) {
272
- console.warn('Auth verification error:', error.message);
273
- return { user: null, authMeta: null }
274
- }
275
- }).onBeforeHandle(({ user, set, request }) => {
276
- const path = new URL(request.url).pathname;
277
-
278
- // Verificar se a rota deve ser excluída da autenticação
279
- const isExcluded = config.excludePaths.some(excluded => path.startsWith(excluded));
280
- if (isExcluded) return
281
-
282
- // Bloquear se não houver usuário
283
- if (!user) {
284
- return config.onUnauthorized(set)
285
- }
286
- });
287
-
288
- return app.group(config.prefix, app =>
289
- app
290
- // Rota de login
291
- .post('/login', async ({ body, set, cookie }) => {
292
- try {
293
- const { email, password } = body;
294
-
295
- if (!email || !password) {
296
- set.status = 400;
297
- return { error: 'Email e senha são obrigatórios' }
298
- }
299
-
300
- const response = await fetchAuth(`${config.apiUrl}/auth/sign-in/email`, {
301
- method: 'POST',
302
- body: JSON.stringify({ email, password }),
303
- headers: {
304
- 'X-API-Key': config.secretKey,
305
- },
306
- });
307
-
308
- if (response.token) {
309
- cookie[config.cookieName].set({
310
- value: response.token,
311
- ...config.cookieOptions,
135
+ })
136
+ .group(config.prefix, app =>
137
+ app
138
+ .post('/login', async ({ body, set, cookie }) => {
139
+ try {
140
+ const res = await fetchAuth(`${config.apiUrl}/auth/sign-in/email`, {
141
+ method: 'POST',
142
+ body: JSON.stringify(body),
143
+ headers: { 'X-API-Key': config.secretKey },
312
144
  });
145
+ cookie[config.cookieName].set({ value: res.token, ...config.cookieOptions });
146
+ return { message: 'OK', user: res.user, token: res.token }
147
+ } catch (e) {
148
+ set.status = 401;
149
+ return 'Falha no login.'
313
150
  }
314
-
315
- return {
316
- message: 'Login realizado com sucesso!',
317
- user: response.user,
318
- token: response.token,
319
- session: response.session,
320
- }
321
- } catch (error) {
322
- set.status = 401;
323
- return { error: 'Login failed', message: error.message }
324
- }
325
- })
326
-
327
- // Rota de registro
328
- .post('/register', async ({ body, set }) => {
329
- try {
330
- const { email, password, name, organizationId } = body;
331
-
332
- if (!email || !password || !name) {
333
- set.status = 400;
334
- return { error: 'Email, senha e nome são obrigatórios' }
335
- }
336
-
337
- const response = await fetchAuth(`${config.apiUrl}/auth/sign-up/email`, {
338
- method: 'POST',
339
- body: JSON.stringify({
340
- email,
341
- password,
342
- name,
343
- ...(organizationId && { organizationId }),
344
- }),
345
- headers: {
346
- 'X-API-Key': config.secretKey,
347
- },
348
- });
349
-
350
- return {
351
- message: 'Usuário registrado com sucesso!',
352
- user: {
353
- id: response.user?.id,
354
- email: response.user?.email,
355
- name: response.user?.name,
356
- },
357
- }
358
- } catch (error) {
359
- set.status = 400;
360
- return { error: 'Registration failed', message: error.message }
361
- }
362
- })
363
-
364
- // Rota de logout
365
- .post('/logout', async ({ cookie, set, headers }) => {
366
- try {
367
- const token = headers.authorization?.replace('Bearer ', '') || cookie[config.cookieName]?.value;
368
-
369
- if (token) {
370
- // Fazer logout no servidor de auth
371
- await fetchAuth(`${config.apiUrl}/auth/sign-out`, {
151
+ })
152
+ .post('/register', async ({ body, set }) => {
153
+ try {
154
+ const res = await fetchAuth(`${config.apiUrl}/auth/sign-up/email`, {
372
155
  method: 'POST',
373
- headers: {
374
- Authorization: `Bearer ${token}`,
375
- 'X-API-Key': config.secretKey,
376
- },
377
- }).catch(() => {
378
- // Ignorar erros do logout remoto
156
+ body: JSON.stringify(body),
157
+ headers: { 'X-API-Key': config.secretKey },
379
158
  });
159
+ return { message: 'OK', user: res.user }
160
+ } catch (e) {
161
+ set.status = 400;
162
+ return 'Falha no registro.'
380
163
  }
381
-
382
- // Remover cookie local
164
+ })
165
+ .post('/logout', async ({ cookie }) => {
383
166
  cookie[config.cookieName].remove();
384
-
385
- return { message: 'Logout realizado com sucesso!' }
386
- } catch (error) {
387
- set.status = 400;
388
- return { error: 'Erro ao fazer logout', message: error.message }
389
- }
390
- })
391
-
392
- // Verificar sessão (substitui o /refresh)
393
- .get('/session', ({ user, authMeta, set }) => {
394
- if (!user) {
395
- set.status = 401;
396
- return { error: 'Not authenticated' }
397
- }
398
- return {
399
- user,
400
- meta: authMeta,
401
- verified_at: new Date().toISOString(),
402
- }
403
- })
404
-
405
- // Status do usuário atual
406
- .get('/me', ({ user, set }) => {
407
- if (!user) {
408
- set.status = 401;
409
- return { error: 'Not authenticated' }
410
- }
411
- return { user }
412
- })
413
- )
414
- }
167
+ return 'Sessão encerrada.'
168
+ })
169
+ .get('/me', ({ user, set }) => user || config.onUnauthorized(set))
170
+ )
415
171
  }
416
172
 
417
173
  export { DEFAULT_CONFIG, RiLiGarAuthClient, authPlugin, authPlugin as default, fetchAuth };