create-fluxstack 1.12.0 → 1.13.0

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.
Files changed (82) hide show
  1. package/LLMD/INDEX.md +8 -1
  2. package/LLMD/agent.md +867 -0
  3. package/LLMD/config/environment-vars.md +30 -0
  4. package/LLMD/resources/live-auth.md +447 -0
  5. package/LLMD/resources/live-components.md +79 -21
  6. package/LLMD/resources/live-logging.md +158 -0
  7. package/LLMD/resources/live-upload.md +1 -1
  8. package/LLMD/resources/rest-auth.md +290 -0
  9. package/README.md +520 -340
  10. package/app/client/src/App.tsx +11 -0
  11. package/app/client/src/components/AppLayout.tsx +1 -0
  12. package/app/client/src/live/AuthDemo.tsx +332 -0
  13. package/app/client/src/live/RoomChatDemo.tsx +24 -105
  14. package/app/server/auth/AuthManager.ts +213 -0
  15. package/app/server/auth/DevAuthProvider.ts +66 -0
  16. package/app/server/auth/HashManager.ts +123 -0
  17. package/app/server/auth/JWTAuthProvider.example.ts +101 -0
  18. package/app/server/auth/RateLimiter.ts +106 -0
  19. package/app/server/auth/contracts.ts +192 -0
  20. package/app/server/auth/guards/SessionGuard.ts +167 -0
  21. package/app/server/auth/guards/TokenGuard.ts +202 -0
  22. package/app/server/auth/index.ts +174 -0
  23. package/app/server/auth/middleware.ts +163 -0
  24. package/app/server/auth/providers/InMemoryProvider.ts +162 -0
  25. package/app/server/auth/sessions/SessionManager.ts +164 -0
  26. package/app/server/cache/CacheManager.ts +81 -0
  27. package/app/server/cache/MemoryDriver.ts +112 -0
  28. package/app/server/cache/contracts.ts +49 -0
  29. package/app/server/cache/index.ts +42 -0
  30. package/app/server/index.ts +14 -0
  31. package/app/server/live/LiveAdminPanel.ts +173 -0
  32. package/app/server/live/LiveCounter.ts +1 -0
  33. package/app/server/live/LiveLocalCounter.ts +13 -8
  34. package/app/server/live/LiveProtectedChat.ts +150 -0
  35. package/app/server/live/LiveRoomChat.ts +45 -203
  36. package/app/server/routes/auth.routes.ts +278 -0
  37. package/app/server/routes/index.ts +2 -0
  38. package/config/index.ts +8 -0
  39. package/config/system/auth.config.ts +49 -0
  40. package/config/system/session.config.ts +33 -0
  41. package/core/client/LiveComponentsProvider.tsx +76 -5
  42. package/core/client/components/Live.tsx +2 -1
  43. package/core/client/hooks/useLiveComponent.ts +47 -4
  44. package/core/client/index.ts +2 -1
  45. package/core/framework/server.ts +36 -4
  46. package/core/plugins/built-in/live-components/commands/create-live-component.ts +15 -8
  47. package/core/plugins/built-in/monitoring/index.ts +10 -3
  48. package/core/plugins/built-in/vite/index.ts +95 -18
  49. package/core/plugins/config.ts +5 -4
  50. package/core/plugins/discovery.ts +11 -2
  51. package/core/plugins/manager.ts +11 -5
  52. package/core/plugins/module-resolver.ts +1 -1
  53. package/core/plugins/registry.ts +53 -25
  54. package/core/server/live/ComponentRegistry.ts +79 -24
  55. package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
  56. package/core/server/live/LiveLogger.ts +111 -0
  57. package/core/server/live/LiveRoomManager.ts +5 -4
  58. package/core/server/live/StateSignature.ts +644 -643
  59. package/core/server/live/auth/LiveAuthContext.ts +71 -0
  60. package/core/server/live/auth/LiveAuthManager.ts +304 -0
  61. package/core/server/live/auth/index.ts +19 -0
  62. package/core/server/live/auth/types.ts +179 -0
  63. package/core/server/live/auto-generated-components.ts +8 -2
  64. package/core/server/live/index.ts +16 -0
  65. package/core/server/live/websocket-plugin.ts +92 -16
  66. package/core/templates/create-project.ts +0 -3
  67. package/core/types/types.ts +133 -13
  68. package/core/utils/index.ts +17 -17
  69. package/core/utils/logger/index.ts +5 -2
  70. package/core/utils/version.ts +1 -1
  71. package/package.json +1 -8
  72. package/plugins/crypto-auth/index.ts +6 -0
  73. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
  74. package/plugins/crypto-auth/server/index.ts +24 -21
  75. package/rest-tests/README.md +57 -0
  76. package/rest-tests/auth-token.http +113 -0
  77. package/rest-tests/auth.http +112 -0
  78. package/rest-tests/rooms-token.http +69 -0
  79. package/rest-tests/users-token.http +62 -0
  80. package/.dockerignore +0 -81
  81. package/Dockerfile +0 -70
  82. package/LIVE_COMPONENTS_REVIEW.md +0 -781
@@ -0,0 +1,213 @@
1
+ /**
2
+ * FluxStack Auth - Auth Manager
3
+ *
4
+ * Orquestrador central do sistema de autenticação.
5
+ * Factory pattern com lazy resolution e cache de guards.
6
+ *
7
+ * Inspirado no AuthManager do Laravel:
8
+ * - Resolve guards por nome (ou default)
9
+ * - Extensível com extend() para guards customizados
10
+ * - Resolve providers automaticamente da config
11
+ *
12
+ * ```ts
13
+ * // Usar guard padrão
14
+ * const user = await auth.guard().user()
15
+ *
16
+ * // Usar guard específico
17
+ * const apiUser = await auth.guard('api').user()
18
+ *
19
+ * // Registrar guard customizado
20
+ * auth.extend('jwt', (name, config, provider) => new JWTGuard(...))
21
+ * ```
22
+ */
23
+
24
+ import type {
25
+ Guard,
26
+ UserProvider,
27
+ GuardConfig,
28
+ GuardFactory,
29
+ RequestContext,
30
+ } from './contracts'
31
+ import { SessionGuard } from './guards/SessionGuard'
32
+ import { TokenGuard } from './guards/TokenGuard'
33
+ import { SessionManager } from './sessions/SessionManager'
34
+ import { cacheManager } from '@server/cache'
35
+
36
+ export interface AuthManagerConfig {
37
+ defaults: {
38
+ guard: string
39
+ provider: string
40
+ }
41
+ guards: Record<string, GuardConfig>
42
+ providers: Record<string, ProviderConfig>
43
+ }
44
+
45
+ export interface ProviderConfig {
46
+ driver: string
47
+ [key: string]: unknown
48
+ }
49
+
50
+ export class AuthManager {
51
+ private config: AuthManagerConfig
52
+ private guards = new Map<string, Guard>()
53
+ private customGuardFactories = new Map<string, GuardFactory>()
54
+ private providerInstances = new Map<string, UserProvider>()
55
+ private customProviderFactories = new Map<string, (config: ProviderConfig) => UserProvider>()
56
+ private sessionManager: SessionManager
57
+
58
+ constructor(config: AuthManagerConfig, sessionManager: SessionManager) {
59
+ this.config = config
60
+ this.sessionManager = sessionManager
61
+ }
62
+
63
+ /**
64
+ * Retorna um guard por nome (ou o default).
65
+ * Guards são criados uma vez e reutilizados.
66
+ */
67
+ guard(name?: string): Guard {
68
+ const guardName = name ?? this.config.defaults.guard
69
+
70
+ if (this.guards.has(guardName)) {
71
+ return this.guards.get(guardName)!
72
+ }
73
+
74
+ const guard = this.resolve(guardName)
75
+ this.guards.set(guardName, guard)
76
+ return guard
77
+ }
78
+
79
+ /**
80
+ * Inicializa todos os guards com o contexto da request.
81
+ * Deve ser chamado no middleware, antes de qualquer uso.
82
+ */
83
+ setRequest(context: RequestContext): void {
84
+ for (const guard of this.guards.values()) {
85
+ guard.setRequest(context)
86
+ }
87
+ // Também resetar guards não-instanciados para forçar re-resolve com novo context
88
+ }
89
+
90
+ /**
91
+ * Cria um guard novo (sem cache) com o context da request.
92
+ * Útil quando precisa de um guard fresco para cada request.
93
+ */
94
+ freshGuard(name?: string, context?: RequestContext): Guard {
95
+ const guardName = name ?? this.config.defaults.guard
96
+ const guard = this.resolve(guardName)
97
+ if (context) {
98
+ guard.setRequest(context)
99
+ }
100
+ return guard
101
+ }
102
+
103
+ /**
104
+ * Registra um guard driver customizado.
105
+ *
106
+ * ```ts
107
+ * auth.extend('jwt', (name, config, provider) => {
108
+ * return new JWTGuard(name, provider, config.secret)
109
+ * })
110
+ * ```
111
+ */
112
+ extend(driver: string, factory: GuardFactory): void {
113
+ this.customGuardFactories.set(driver, factory)
114
+ }
115
+
116
+ /**
117
+ * Registra um provider customizado.
118
+ *
119
+ * ```ts
120
+ * auth.extendProvider('drizzle', (config) => {
121
+ * return new DrizzleUserProvider(db, config.table)
122
+ * })
123
+ * ```
124
+ */
125
+ extendProvider(driver: string, factory: (config: ProviderConfig) => UserProvider): void {
126
+ this.customProviderFactories.set(driver, factory)
127
+ }
128
+
129
+ /**
130
+ * Registra uma instância de provider diretamente.
131
+ */
132
+ registerProvider(name: string, provider: UserProvider): void {
133
+ this.providerInstances.set(name, provider)
134
+ }
135
+
136
+ /** Retorna o nome do guard padrão */
137
+ getDefaultGuardName(): string {
138
+ return this.config.defaults.guard
139
+ }
140
+
141
+ /** Retorna a config */
142
+ getConfig(): AuthManagerConfig {
143
+ return this.config
144
+ }
145
+
146
+ /** Resolve um guard por nome */
147
+ private resolve(name: string): Guard {
148
+ const guardConfig = this.config.guards[name]
149
+ if (!guardConfig) {
150
+ throw new Error(
151
+ `Auth guard '${name}' not configured. ` +
152
+ `Available: ${Object.keys(this.config.guards).join(', ')}`
153
+ )
154
+ }
155
+
156
+ // Resolver provider
157
+ const provider = this.resolveProvider(guardConfig.provider)
158
+
159
+ // Verificar custom factory primeiro
160
+ if (this.customGuardFactories.has(guardConfig.driver)) {
161
+ return this.customGuardFactories.get(guardConfig.driver)!(name, guardConfig, provider)
162
+ }
163
+
164
+ // Built-in drivers
165
+ switch (guardConfig.driver) {
166
+ case 'session':
167
+ return new SessionGuard(name, provider, this.sessionManager)
168
+
169
+ case 'token':
170
+ return new TokenGuard(
171
+ name,
172
+ provider,
173
+ cacheManager.driver(),
174
+ guardConfig.tokenTtl as number | undefined
175
+ )
176
+
177
+ default:
178
+ throw new Error(
179
+ `Auth guard driver '${guardConfig.driver}' not supported. ` +
180
+ `Use auth.extend('${guardConfig.driver}', factory) to register it.`
181
+ )
182
+ }
183
+ }
184
+
185
+ /** Resolve um provider por nome */
186
+ private resolveProvider(name: string): UserProvider {
187
+ // Cache de instâncias
188
+ if (this.providerInstances.has(name)) {
189
+ return this.providerInstances.get(name)!
190
+ }
191
+
192
+ const providerConfig = this.config.providers[name]
193
+ if (!providerConfig) {
194
+ throw new Error(
195
+ `Auth provider '${name}' not configured. ` +
196
+ `Available: ${Object.keys(this.config.providers).join(', ')}`
197
+ )
198
+ }
199
+
200
+ // Custom factory
201
+ if (this.customProviderFactories.has(providerConfig.driver)) {
202
+ const provider = this.customProviderFactories.get(providerConfig.driver)!(providerConfig)
203
+ this.providerInstances.set(name, provider)
204
+ return provider
205
+ }
206
+
207
+ throw new Error(
208
+ `Auth provider driver '${providerConfig.driver}' not supported. ` +
209
+ `Use auth.extendProvider('${providerConfig.driver}', factory) to register it, ` +
210
+ `or use auth.registerProvider('${name}', providerInstance) to register directly.`
211
+ )
212
+ }
213
+ }
@@ -0,0 +1,66 @@
1
+ // 🧪 DevAuthProvider - Provider de desenvolvimento para testes de auth
2
+ //
3
+ // Aceita tokens simples para facilitar testes da demo de autenticação.
4
+ // NÃO USAR EM PRODUÇÃO!
5
+ //
6
+ // Tokens válidos:
7
+ // - "admin-token" → role: admin, permissions: all
8
+ // - "user-token" → role: user, permissions: básicas
9
+ // - "mod-token" → role: moderator, permissions: moderação
10
+
11
+ import type {
12
+ LiveAuthProvider,
13
+ LiveAuthCredentials,
14
+ LiveAuthContext,
15
+ } from '@core/server/live/auth/types'
16
+ import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
17
+
18
+ interface DevUser {
19
+ id: string
20
+ name: string
21
+ roles: string[]
22
+ permissions: string[]
23
+ }
24
+
25
+ const DEV_USERS: Record<string, DevUser> = {
26
+ 'admin-token': {
27
+ id: 'admin-1',
28
+ name: 'Admin User',
29
+ roles: ['admin', 'user'],
30
+ permissions: ['users.read', 'users.write', 'users.delete', 'chat.read', 'chat.write', 'chat.admin'],
31
+ },
32
+ 'user-token': {
33
+ id: 'user-1',
34
+ name: 'Regular User',
35
+ roles: ['user'],
36
+ permissions: ['chat.read', 'chat.write'],
37
+ },
38
+ 'mod-token': {
39
+ id: 'mod-1',
40
+ name: 'Moderator',
41
+ roles: ['moderator', 'user'],
42
+ permissions: ['chat.read', 'chat.write', 'chat.moderate'],
43
+ },
44
+ }
45
+
46
+ export class DevAuthProvider implements LiveAuthProvider {
47
+ readonly name = 'dev'
48
+
49
+ async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
50
+ const token = credentials.token as string
51
+ if (!token) return null
52
+
53
+ const user = DEV_USERS[token]
54
+ if (!user) return null
55
+
56
+ return new AuthenticatedContext(
57
+ {
58
+ id: user.id,
59
+ name: user.name,
60
+ roles: user.roles,
61
+ permissions: user.permissions,
62
+ },
63
+ token
64
+ )
65
+ }
66
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * FluxStack Auth - Hash Manager
3
+ *
4
+ * Abstração de password hashing usando APIs nativas do Bun.
5
+ * Suporta bcrypt e argon2id. Inclui needsRehash() para
6
+ * migração transparente de algoritmos.
7
+ *
8
+ * ```ts
9
+ * const hash = await Hash.make('my-password')
10
+ * const valid = await Hash.check('my-password', hash)
11
+ * ```
12
+ */
13
+
14
+ export type HashAlgorithm = 'bcrypt' | 'argon2id'
15
+
16
+ export interface HashOptions {
17
+ algorithm?: HashAlgorithm
18
+ /** Cost/rounds para bcrypt (default: 10) */
19
+ bcryptRounds?: number
20
+ /** Memory cost para argon2 em KiB (default: 65536) */
21
+ argon2MemoryCost?: number
22
+ /** Time cost para argon2 (default: 2) */
23
+ argon2TimeCost?: number
24
+ }
25
+
26
+ export class HashManager {
27
+ private algorithm: HashAlgorithm
28
+ private bcryptRounds: number
29
+ private argon2MemoryCost: number
30
+ private argon2TimeCost: number
31
+
32
+ constructor(options: HashOptions = {}) {
33
+ this.algorithm = options.algorithm ?? 'bcrypt'
34
+ this.bcryptRounds = options.bcryptRounds ?? 10
35
+ this.argon2MemoryCost = options.argon2MemoryCost ?? 65536
36
+ this.argon2TimeCost = options.argon2TimeCost ?? 2
37
+ }
38
+
39
+ /**
40
+ * Cria hash de uma password.
41
+ */
42
+ async make(plaintext: string): Promise<string> {
43
+ if (this.algorithm === 'argon2id') {
44
+ return Bun.password.hash(plaintext, {
45
+ algorithm: 'argon2id',
46
+ memoryCost: this.argon2MemoryCost,
47
+ timeCost: this.argon2TimeCost,
48
+ })
49
+ }
50
+
51
+ return Bun.password.hash(plaintext, {
52
+ algorithm: 'bcrypt',
53
+ cost: this.bcryptRounds,
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Verifica se plaintext corresponde ao hash.
59
+ */
60
+ async check(plaintext: string, hash: string): Promise<boolean> {
61
+ return Bun.password.verify(plaintext, hash)
62
+ }
63
+
64
+ /**
65
+ * Verifica se o hash precisa ser re-gerado.
66
+ * Útil para migrar de bcrypt → argon2 ou mudar rounds.
67
+ *
68
+ * Uso típico: após login bem-sucedido, verificar e re-hash se necessário.
69
+ */
70
+ needsRehash(hash: string): boolean {
71
+ if (this.algorithm === 'bcrypt') {
72
+ // bcrypt hashes começam com $2b$ ou $2a$
73
+ if (!hash.startsWith('$2b$') && !hash.startsWith('$2a$')) {
74
+ return true // Hash não é bcrypt, precisa migrar
75
+ }
76
+ // Verificar rounds: $2b$10$ → rounds = 10
77
+ const roundsMatch = hash.match(/^\$2[ab]\$(\d+)\$/)
78
+ if (roundsMatch) {
79
+ const currentRounds = parseInt(roundsMatch[1], 10)
80
+ return currentRounds !== this.bcryptRounds
81
+ }
82
+ return false
83
+ }
84
+
85
+ if (this.algorithm === 'argon2id') {
86
+ return !hash.startsWith('$argon2id$')
87
+ }
88
+
89
+ return false
90
+ }
91
+
92
+ /** Retorna o algoritmo atual */
93
+ getAlgorithm(): HashAlgorithm {
94
+ return this.algorithm
95
+ }
96
+ }
97
+
98
+ /** Instância global do hash manager (configurada no boot) */
99
+ let hashInstance: HashManager | null = null
100
+
101
+ export function getHashManager(): HashManager {
102
+ if (!hashInstance) {
103
+ hashInstance = new HashManager()
104
+ }
105
+ return hashInstance
106
+ }
107
+
108
+ export function setHashManager(manager: HashManager): void {
109
+ hashInstance = manager
110
+ }
111
+
112
+ /** Atalho para uso direto */
113
+ export const Hash = {
114
+ async make(plaintext: string): Promise<string> {
115
+ return getHashManager().make(plaintext)
116
+ },
117
+ async check(plaintext: string, hash: string): Promise<boolean> {
118
+ return getHashManager().check(plaintext, hash)
119
+ },
120
+ needsRehash(hash: string): boolean {
121
+ return getHashManager().needsRehash(hash)
122
+ },
123
+ }
@@ -0,0 +1,101 @@
1
+ // 🔒 Exemplo: Como criar um LiveAuthProvider customizado (JWT)
2
+ //
3
+ // Este arquivo mostra como criar um provider de autenticação para Live Components.
4
+ // Copie e adapte para o seu caso de uso.
5
+ //
6
+ // Registro:
7
+ // import { liveAuthManager } from '@core/server/live/auth'
8
+ // import { JWTAuthProvider } from './auth/JWTAuthProvider'
9
+ //
10
+ // liveAuthManager.register(new JWTAuthProvider('your-secret-key'))
11
+
12
+ import type {
13
+ LiveAuthProvider,
14
+ LiveAuthCredentials,
15
+ LiveAuthContext,
16
+ } from '@core/server/live/auth/types'
17
+ import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
18
+
19
+ /**
20
+ * Exemplo de provider JWT para Live Components.
21
+ *
22
+ * Em produção, use uma lib real como 'jose' ou 'jsonwebtoken'.
23
+ * Este exemplo usa decode simples para fins didáticos.
24
+ */
25
+ export class JWTAuthProvider implements LiveAuthProvider {
26
+ readonly name = 'jwt'
27
+ private secret: string
28
+
29
+ constructor(secret: string) {
30
+ this.secret = secret
31
+ }
32
+
33
+ async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
34
+ const token = credentials.token as string
35
+ if (!token) return null
36
+
37
+ try {
38
+ // Em produção: const payload = jwt.verify(token, this.secret)
39
+ const payload = this.decodeToken(token)
40
+ if (!payload) return null
41
+
42
+ return new AuthenticatedContext(
43
+ {
44
+ id: payload.sub,
45
+ roles: payload.roles || [],
46
+ permissions: payload.permissions || [],
47
+ name: payload.name,
48
+ email: payload.email,
49
+ },
50
+ token
51
+ )
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+
57
+ /**
58
+ * (Opcional) Autorização customizada por action.
59
+ * Se implementado, é chamado ALÉM da verificação de roles/permissions.
60
+ * Útil para lógica de negócio complexa (ex: limites por plano, rate limiting).
61
+ */
62
+ async authorizeAction(
63
+ context: LiveAuthContext,
64
+ componentName: string,
65
+ action: string
66
+ ): Promise<boolean> {
67
+ // Exemplo: bloquear ações destrutivas fora do horário comercial
68
+ // const hour = new Date().getHours()
69
+ // if (action === 'deleteAll' && (hour < 9 || hour > 18)) return false
70
+
71
+ return true // Allow by default
72
+ }
73
+
74
+ /**
75
+ * (Opcional) Autorização customizada por sala.
76
+ * Útil para salas privadas, premium, etc.
77
+ */
78
+ async authorizeRoom(
79
+ context: LiveAuthContext,
80
+ roomId: string
81
+ ): Promise<boolean> {
82
+ // Exemplo: salas "vip-*" requerem role premium
83
+ // if (roomId.startsWith('vip-') && !context.hasRole('premium')) return false
84
+
85
+ return true // Allow by default
86
+ }
87
+
88
+ // Decode simplificado (NÃO USAR EM PRODUÇÃO - não valida assinatura)
89
+ private decodeToken(token: string): any {
90
+ try {
91
+ const parts = token.split('.')
92
+ if (parts.length !== 3) return null
93
+ const payload = JSON.parse(atob(parts[1]))
94
+ // Em produção: verificar expiração, assinatura, etc.
95
+ if (payload.exp && payload.exp * 1000 < Date.now()) return null
96
+ return payload
97
+ } catch {
98
+ return null
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * FluxStack Auth - Rate Limiter
3
+ *
4
+ * Proteção contra brute-force. Usa o cache system modular.
5
+ * Inspirado no RateLimiter do Laravel (por chave: email+ip).
6
+ *
7
+ * ```ts
8
+ * const key = `login:${email}|${ip}`
9
+ * if (await rateLimiter.tooManyAttempts(key, 5)) {
10
+ * const seconds = await rateLimiter.availableIn(key)
11
+ * return { error: `Too many attempts. Try again in ${seconds}s` }
12
+ * }
13
+ * ```
14
+ */
15
+
16
+ import type { CacheDriver } from '@server/cache/contracts'
17
+
18
+ interface RateLimitEntry {
19
+ count: number
20
+ expiresAt: number
21
+ }
22
+
23
+ export class RateLimiter {
24
+ private cache: CacheDriver
25
+
26
+ constructor(cache: CacheDriver) {
27
+ this.cache = cache
28
+ }
29
+
30
+ /**
31
+ * Registra uma tentativa para a chave.
32
+ * @param key Chave identificadora (ex: 'login:user@email.com|127.0.0.1')
33
+ * @param decaySeconds Tempo em segundos até o contador resetar
34
+ */
35
+ async hit(key: string, decaySeconds: number = 60): Promise<number> {
36
+ const cacheKey = `rate_limit:${key}`
37
+ const entry = await this.cache.get<RateLimitEntry>(cacheKey)
38
+ const now = Date.now()
39
+
40
+ if (entry && entry.expiresAt > now) {
41
+ // Incrementar contador existente
42
+ const updated: RateLimitEntry = {
43
+ count: entry.count + 1,
44
+ expiresAt: entry.expiresAt,
45
+ }
46
+ const remainingTtl = Math.ceil((entry.expiresAt - now) / 1000)
47
+ await this.cache.set(cacheKey, updated, remainingTtl)
48
+ return updated.count
49
+ }
50
+
51
+ // Nova entrada
52
+ const newEntry: RateLimitEntry = {
53
+ count: 1,
54
+ expiresAt: now + (decaySeconds * 1000),
55
+ }
56
+ await this.cache.set(cacheKey, newEntry, decaySeconds)
57
+ return 1
58
+ }
59
+
60
+ /**
61
+ * Verifica se a chave excedeu o limite de tentativas.
62
+ */
63
+ async tooManyAttempts(key: string, maxAttempts: number): Promise<boolean> {
64
+ const attempts = await this.attempts(key)
65
+ return attempts >= maxAttempts
66
+ }
67
+
68
+ /**
69
+ * Retorna o número atual de tentativas para a chave.
70
+ */
71
+ async attempts(key: string): Promise<number> {
72
+ const cacheKey = `rate_limit:${key}`
73
+ const entry = await this.cache.get<RateLimitEntry>(cacheKey)
74
+
75
+ if (!entry) return 0
76
+ if (entry.expiresAt < Date.now()) return 0
77
+
78
+ return entry.count
79
+ }
80
+
81
+ /**
82
+ * Retorna quantos segundos até o rate limit expirar.
83
+ */
84
+ async availableIn(key: string): Promise<number> {
85
+ const cacheKey = `rate_limit:${key}`
86
+ const entry = await this.cache.get<RateLimitEntry>(cacheKey)
87
+
88
+ if (!entry) return 0
89
+ return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1000))
90
+ }
91
+
92
+ /**
93
+ * Retorna quantas tentativas restam.
94
+ */
95
+ async remainingAttempts(key: string, maxAttempts: number): Promise<number> {
96
+ const attempts = await this.attempts(key)
97
+ return Math.max(0, maxAttempts - attempts)
98
+ }
99
+
100
+ /**
101
+ * Limpa o rate limit para a chave (após login bem-sucedido).
102
+ */
103
+ async clear(key: string): Promise<void> {
104
+ await this.cache.delete(`rate_limit:${key}`)
105
+ }
106
+ }