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