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