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