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,192 @@
1
+ /**
2
+ * FluxStack Auth System - Contracts
3
+ *
4
+ * Inspirado no Laravel: Guard + Provider + Authenticatable.
5
+ * - Guard: COMO autenticar (session, token, JWT...)
6
+ * - UserProvider: ONDE buscar os usuários (memória, banco, API...)
7
+ * - Authenticatable: O QUE é um usuário autenticável
8
+ *
9
+ * Adaptado para API-only (sem HTML, sem redirects, sem CSRF).
10
+ */
11
+
12
+ // ===== Authenticatable =====
13
+
14
+ /**
15
+ * Interface que define o que é um "usuário autenticável".
16
+ * Qualquer model/entidade que implemente isso pode ser usada com o auth system.
17
+ *
18
+ * Para usar com seu ORM:
19
+ * ```ts
20
+ * class User implements Authenticatable {
21
+ * getAuthId() { return this.id }
22
+ * getAuthPassword() { return this.passwordHash }
23
+ * // ...
24
+ * }
25
+ * ```
26
+ */
27
+ export interface Authenticatable {
28
+ /** Retorna o identificador único (ex: id, uuid) */
29
+ getAuthId(): string | number
30
+
31
+ /** Retorna o nome do campo identificador (ex: 'id') */
32
+ getAuthIdField(): string
33
+
34
+ /** Retorna o hash da password armazenada */
35
+ getAuthPassword(): string
36
+
37
+ /** Retorna o token de "remember me" (ou null) */
38
+ getRememberToken(): string | null
39
+
40
+ /** Define o token de "remember me" */
41
+ setRememberToken(token: string | null): void
42
+
43
+ /** Retorna dados serializáveis do usuário (para response da API) */
44
+ toJSON(): Record<string, unknown>
45
+ }
46
+
47
+ // ===== UserProvider =====
48
+
49
+ /**
50
+ * Interface para buscar e validar usuários.
51
+ * O provider NÃO sabe como autenticar - apenas onde os dados estão.
52
+ *
53
+ * Para implementar com banco de dados:
54
+ * ```ts
55
+ * class DrizzleUserProvider implements UserProvider {
56
+ * async retrieveById(id) { return db.select().from(users).where(eq(users.id, id)) }
57
+ * async retrieveByCredentials({ email }) { return db.select().from(users).where(eq(users.email, email)) }
58
+ * async validateCredentials(user, { password }) { return Hash.check(password, user.getAuthPassword()) }
59
+ * }
60
+ * ```
61
+ */
62
+ export interface UserProvider {
63
+ /** Busca usuário pelo ID */
64
+ retrieveById(id: string | number): Promise<Authenticatable | null>
65
+
66
+ /**
67
+ * Busca usuário pelas credenciais (SEM a password).
68
+ * Ex: { email: "user@example.com" } → busca por email.
69
+ * A password é validada separadamente em validateCredentials().
70
+ */
71
+ retrieveByCredentials(credentials: Record<string, unknown>): Promise<Authenticatable | null>
72
+
73
+ /**
74
+ * Valida a password contra o hash do usuário.
75
+ * Recebe o user já encontrado + as credenciais originais (com password).
76
+ */
77
+ validateCredentials(user: Authenticatable, credentials: Record<string, unknown>): Promise<boolean>
78
+
79
+ /** Busca usuário pelo remember token */
80
+ retrieveByToken(id: string | number, token: string): Promise<Authenticatable | null>
81
+
82
+ /** Atualiza o remember token do usuário */
83
+ updateRememberToken(user: Authenticatable, token: string | null): Promise<void>
84
+
85
+ /** Cria um novo usuário (para registro) */
86
+ createUser(data: Record<string, unknown>): Promise<Authenticatable>
87
+ }
88
+
89
+ // ===== Guard =====
90
+
91
+ /**
92
+ * Interface do guard de autenticação.
93
+ * O guard sabe COMO autenticar (session cookie, bearer token, etc).
94
+ *
95
+ * Para criar um guard customizado:
96
+ * ```ts
97
+ * class JWTGuard implements Guard {
98
+ * async user() { /* decode JWT from header *\/ }
99
+ * async attempt(creds) { /* validate + generate JWT *\/ }
100
+ * }
101
+ * authManager.extend('jwt', (config) => new JWTGuard(config))
102
+ * ```
103
+ */
104
+ export interface Guard {
105
+ /** Nome do guard */
106
+ readonly name: string
107
+
108
+ /** Retorna o usuário autenticado ou null */
109
+ user(): Promise<Authenticatable | null>
110
+
111
+ /** Retorna o ID do usuário autenticado ou null */
112
+ id(): Promise<string | number | null>
113
+
114
+ /** Verifica se há um usuário autenticado */
115
+ check(): Promise<boolean>
116
+
117
+ /** Verifica se NÃO há usuário autenticado */
118
+ guest(): Promise<boolean>
119
+
120
+ /**
121
+ * Tenta autenticar com credenciais.
122
+ * Retorna o usuário se sucesso, null se falha.
123
+ */
124
+ attempt(credentials: Record<string, unknown>, remember?: boolean): Promise<Authenticatable | null>
125
+
126
+ /** Autentica um usuário diretamente (sem validar credenciais) */
127
+ login(user: Authenticatable, remember?: boolean): Promise<void>
128
+
129
+ /** Desloga o usuário atual */
130
+ logout(): Promise<void>
131
+
132
+ /**
133
+ * Valida credenciais SEM efetuar login.
134
+ * Útil para confirmar password antes de ações sensíveis.
135
+ */
136
+ validate(credentials: Record<string, unknown>): Promise<boolean>
137
+
138
+ /**
139
+ * Inicializa o guard com o contexto da request atual.
140
+ * Chamado pelo middleware antes de qualquer operação.
141
+ */
142
+ setRequest(context: RequestContext): void
143
+ }
144
+
145
+ // ===== Request Context =====
146
+
147
+ /**
148
+ * Contexto da request disponível para os guards.
149
+ * Abstrai o acesso a headers, cookies, etc.
150
+ */
151
+ export interface RequestContext {
152
+ /** Headers da request */
153
+ headers: Record<string, string | undefined>
154
+
155
+ /** Cookies da request */
156
+ cookie: Record<string, { value: string; set: (opts: CookieOptions) => void } | undefined>
157
+
158
+ /** Setter para cookies na response */
159
+ setCookie: (name: string, value: string, options?: CookieOptions) => void
160
+
161
+ /** Remove um cookie */
162
+ removeCookie: (name: string) => void
163
+
164
+ /** IP da request (para rate limiting) */
165
+ ip: string
166
+ }
167
+
168
+ /** Opções para cookies */
169
+ export interface CookieOptions {
170
+ maxAge?: number
171
+ httpOnly?: boolean
172
+ secure?: boolean
173
+ sameSite?: 'strict' | 'lax' | 'none'
174
+ path?: string
175
+ domain?: string
176
+ }
177
+
178
+ // ===== Guard Factory =====
179
+
180
+ /** Config de um guard individual */
181
+ export interface GuardConfig {
182
+ driver: string
183
+ provider: string
184
+ [key: string]: unknown
185
+ }
186
+
187
+ /** Factory function para criar guards customizados */
188
+ export type GuardFactory = (
189
+ name: string,
190
+ config: GuardConfig,
191
+ provider: UserProvider
192
+ ) => Guard
@@ -0,0 +1,167 @@
1
+ /**
2
+ * FluxStack Auth - Session Guard
3
+ *
4
+ * Guard baseado em session cookie. Padrão para SPAs que se comunicam
5
+ * com a API via fetch/axios (mesma origem ou CORS com credentials).
6
+ *
7
+ * Fluxo:
8
+ * 1. POST /login → attempt() → valida credenciais → cria sessão → seta cookie
9
+ * 2. Requests seguintes → cookie enviado automaticamente → user() resolve da sessão
10
+ * 3. POST /logout → logout() → destroi sessão → remove cookie
11
+ */
12
+
13
+ import type {
14
+ Guard,
15
+ Authenticatable,
16
+ UserProvider,
17
+ RequestContext,
18
+ } from '../contracts'
19
+ import type { SessionManager } from '../sessions/SessionManager'
20
+
21
+ export class SessionGuard implements Guard {
22
+ readonly name: string
23
+ private provider: UserProvider
24
+ private sessions: SessionManager
25
+ private request: RequestContext | null = null
26
+
27
+ /** Cache do usuário para a request atual (evita múltiplas queries) */
28
+ private resolvedUser: Authenticatable | null | undefined = undefined
29
+
30
+ constructor(name: string, provider: UserProvider, sessions: SessionManager) {
31
+ this.name = name
32
+ this.provider = provider
33
+ this.sessions = sessions
34
+ }
35
+
36
+ setRequest(context: RequestContext): void {
37
+ this.request = context
38
+ // Reset cache para nova request
39
+ this.resolvedUser = undefined
40
+ }
41
+
42
+ async user(): Promise<Authenticatable | null> {
43
+ // Cache per-request
44
+ if (this.resolvedUser !== undefined) {
45
+ return this.resolvedUser
46
+ }
47
+
48
+ this.resolvedUser = null
49
+
50
+ if (!this.request) return null
51
+
52
+ // 1. Ler session ID do cookie
53
+ const sessionId = this.getSessionId()
54
+ if (!sessionId) return null
55
+
56
+ // 2. Buscar dados da sessão
57
+ const sessionData = await this.sessions.read(sessionId)
58
+ if (!sessionData?.userId) return null
59
+
60
+ // 3. Buscar usuário pelo ID
61
+ const user = await this.provider.retrieveById(sessionData.userId as string | number)
62
+ if (user) {
63
+ this.resolvedUser = user
64
+ }
65
+
66
+ return this.resolvedUser
67
+ }
68
+
69
+ async id(): Promise<string | number | null> {
70
+ const user = await this.user()
71
+ return user?.getAuthId() ?? null
72
+ }
73
+
74
+ async check(): Promise<boolean> {
75
+ return (await this.user()) !== null
76
+ }
77
+
78
+ async guest(): Promise<boolean> {
79
+ return (await this.user()) === null
80
+ }
81
+
82
+ async attempt(
83
+ credentials: Record<string, unknown>,
84
+ _remember?: boolean
85
+ ): Promise<Authenticatable | null> {
86
+ // 1. Buscar usuário pelas credenciais (SEM password)
87
+ const user = await this.provider.retrieveByCredentials(credentials)
88
+ if (!user) return null
89
+
90
+ // 2. Validar password
91
+ const valid = await this.provider.validateCredentials(user, credentials)
92
+ if (!valid) return null
93
+
94
+ // 3. Login
95
+ await this.login(user)
96
+
97
+ return user
98
+ }
99
+
100
+ async login(user: Authenticatable, _remember?: boolean): Promise<void> {
101
+ if (!this.request) {
102
+ throw new Error('SessionGuard: request context not set. Call setRequest() first.')
103
+ }
104
+
105
+ const config = this.sessions.getConfig()
106
+
107
+ // 1. Regenerar sessão (ou criar nova) para prevenir session fixation
108
+ const existingSessionId = this.getSessionId()
109
+ let sessionId: string
110
+
111
+ if (existingSessionId) {
112
+ sessionId = await this.sessions.regenerate(existingSessionId)
113
+ } else {
114
+ sessionId = await this.sessions.create({})
115
+ }
116
+
117
+ // 2. Salvar userId na sessão
118
+ await this.sessions.put(sessionId, 'userId', user.getAuthId())
119
+ await this.sessions.put(sessionId, '_loginAt', Date.now())
120
+
121
+ // 3. Setar cookie
122
+ this.request.setCookie(config.cookieName, sessionId, {
123
+ maxAge: config.lifetime,
124
+ httpOnly: config.httpOnly,
125
+ secure: config.secure,
126
+ sameSite: config.sameSite,
127
+ path: config.path,
128
+ domain: config.domain,
129
+ })
130
+
131
+ // 4. Cache do usuário para esta request
132
+ this.resolvedUser = user
133
+ }
134
+
135
+ async logout(): Promise<void> {
136
+ if (!this.request) return
137
+
138
+ const config = this.sessions.getConfig()
139
+ const sessionId = this.getSessionId()
140
+
141
+ // 1. Destroir sessão
142
+ if (sessionId) {
143
+ await this.sessions.destroy(sessionId)
144
+ }
145
+
146
+ // 2. Remover cookie
147
+ this.request.removeCookie(config.cookieName)
148
+
149
+ // 3. Limpar cache
150
+ this.resolvedUser = null
151
+ }
152
+
153
+ async validate(credentials: Record<string, unknown>): Promise<boolean> {
154
+ const user = await this.provider.retrieveByCredentials(credentials)
155
+ if (!user) return false
156
+ return this.provider.validateCredentials(user, credentials)
157
+ }
158
+
159
+ /** Lê o session ID do cookie */
160
+ private getSessionId(): string | null {
161
+ if (!this.request) return null
162
+
163
+ const config = this.sessions.getConfig()
164
+ const cookie = this.request.cookie[config.cookieName]
165
+ return cookie?.value || null
166
+ }
167
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * FluxStack Auth - Token Guard
3
+ *
4
+ * Guard baseado em Bearer token (header Authorization).
5
+ * Para APIs consumidas por mobile apps, CLIs, integrações.
6
+ *
7
+ * Fluxo:
8
+ * 1. Client envia: Authorization: Bearer <token>
9
+ * 2. Guard busca user pelo token no provider
10
+ * 3. Se válido, user está autenticado
11
+ *
12
+ * Tokens são armazenados no cache com referência ao userId.
13
+ */
14
+
15
+ import type {
16
+ Guard,
17
+ Authenticatable,
18
+ UserProvider,
19
+ RequestContext,
20
+ } from '../contracts'
21
+ import type { CacheDriver } from '@server/cache/contracts'
22
+
23
+ interface StoredToken {
24
+ userId: string | number
25
+ createdAt: number
26
+ expiresAt: number | null
27
+ }
28
+
29
+ export class TokenGuard implements Guard {
30
+ readonly name: string
31
+ private provider: UserProvider
32
+ private cache: CacheDriver
33
+ private request: RequestContext | null = null
34
+ private tokenTtl: number
35
+
36
+ /** Cache do usuário para a request atual */
37
+ private resolvedUser: Authenticatable | null | undefined = undefined
38
+
39
+ constructor(
40
+ name: string,
41
+ provider: UserProvider,
42
+ cache: CacheDriver,
43
+ tokenTtlSeconds: number = 86400 // 24h default
44
+ ) {
45
+ this.name = name
46
+ this.provider = provider
47
+ this.cache = cache
48
+ this.tokenTtl = tokenTtlSeconds
49
+ }
50
+
51
+ setRequest(context: RequestContext): void {
52
+ this.request = context
53
+ this.resolvedUser = undefined
54
+ }
55
+
56
+ async user(): Promise<Authenticatable | null> {
57
+ if (this.resolvedUser !== undefined) {
58
+ return this.resolvedUser
59
+ }
60
+
61
+ this.resolvedUser = null
62
+
63
+ if (!this.request) return null
64
+
65
+ // 1. Extrair token do header Authorization
66
+ const token = this.getBearerToken()
67
+ if (!token) return null
68
+
69
+ // 2. Buscar token no cache
70
+ const tokenData = await this.cache.get<StoredToken>(`auth_token:${this.hashToken(token)}`)
71
+ if (!tokenData) return null
72
+
73
+ // 3. Verificar expiração
74
+ if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
75
+ await this.cache.delete(`auth_token:${this.hashToken(token)}`)
76
+ return null
77
+ }
78
+
79
+ // 4. Buscar usuário
80
+ const user = await this.provider.retrieveById(tokenData.userId)
81
+ if (user) {
82
+ this.resolvedUser = user
83
+ }
84
+
85
+ return this.resolvedUser
86
+ }
87
+
88
+ async id(): Promise<string | number | null> {
89
+ const user = await this.user()
90
+ return user?.getAuthId() ?? null
91
+ }
92
+
93
+ async check(): Promise<boolean> {
94
+ return (await this.user()) !== null
95
+ }
96
+
97
+ async guest(): Promise<boolean> {
98
+ return (await this.user()) === null
99
+ }
100
+
101
+ async attempt(credentials: Record<string, unknown>): Promise<Authenticatable | null> {
102
+ // 1. Buscar user
103
+ const user = await this.provider.retrieveByCredentials(credentials)
104
+ if (!user) return null
105
+
106
+ // 2. Validar password
107
+ const valid = await this.provider.validateCredentials(user, credentials)
108
+ if (!valid) return null
109
+
110
+ // 3. Gerar e armazenar token
111
+ await this.login(user)
112
+
113
+ return user
114
+ }
115
+
116
+ async login(user: Authenticatable): Promise<void> {
117
+ // Gerar token
118
+ const token = this.generateToken()
119
+ const hashedToken = this.hashToken(token)
120
+
121
+ // Armazenar no cache
122
+ const tokenData: StoredToken = {
123
+ userId: user.getAuthId(),
124
+ createdAt: Date.now(),
125
+ expiresAt: this.tokenTtl > 0 ? Date.now() + (this.tokenTtl * 1000) : null,
126
+ }
127
+ await this.cache.set(`auth_token:${hashedToken}`, tokenData, this.tokenTtl || undefined)
128
+
129
+ // Armazenar lista de tokens do user (para revogação em massa)
130
+ const userTokensKey = `user_tokens:${user.getAuthId()}`
131
+ const existingTokens = await this.cache.get<string[]>(userTokensKey) ?? []
132
+ existingTokens.push(hashedToken)
133
+ await this.cache.set(userTokensKey, existingTokens)
134
+
135
+ // Cache do user
136
+ this.resolvedUser = user
137
+
138
+ // Salvar token plain-text temporariamente para a response poder retorná-lo
139
+ ;(this as any)._lastGeneratedToken = token
140
+ }
141
+
142
+ async logout(): Promise<void> {
143
+ if (!this.request) return
144
+
145
+ const token = this.getBearerToken()
146
+ if (token) {
147
+ await this.cache.delete(`auth_token:${this.hashToken(token)}`)
148
+ }
149
+
150
+ this.resolvedUser = null
151
+ }
152
+
153
+ async validate(credentials: Record<string, unknown>): Promise<boolean> {
154
+ const user = await this.provider.retrieveByCredentials(credentials)
155
+ if (!user) return false
156
+ return this.provider.validateCredentials(user, credentials)
157
+ }
158
+
159
+ /**
160
+ * Revoga todos os tokens de um usuário.
161
+ */
162
+ async revokeAllTokens(userId: string | number): Promise<void> {
163
+ const userTokensKey = `user_tokens:${userId}`
164
+ const tokens = await this.cache.get<string[]>(userTokensKey) ?? []
165
+
166
+ for (const hashedToken of tokens) {
167
+ await this.cache.delete(`auth_token:${hashedToken}`)
168
+ }
169
+ await this.cache.delete(userTokensKey)
170
+ }
171
+
172
+ /** Retorna o último token gerado (para a response após login) */
173
+ getLastGeneratedToken(): string | null {
174
+ return (this as any)._lastGeneratedToken ?? null
175
+ }
176
+
177
+ /** Extrai Bearer token do header Authorization */
178
+ private getBearerToken(): string | null {
179
+ if (!this.request) return null
180
+
181
+ const authHeader = this.request.headers['authorization'] ?? this.request.headers['Authorization']
182
+ if (!authHeader?.startsWith('Bearer ')) return null
183
+
184
+ return authHeader.slice(7)
185
+ }
186
+
187
+ /** Gera um token criptograficamente seguro */
188
+ private generateToken(): string {
189
+ const bytes = new Uint8Array(32)
190
+ crypto.getRandomValues(bytes)
191
+ return Array.from(bytes)
192
+ .map(b => b.toString(16).padStart(2, '0'))
193
+ .join('')
194
+ }
195
+
196
+ /** Hash do token para armazenamento (nunca guardar plain-text) */
197
+ private hashToken(token: string): string {
198
+ const hasher = new Bun.CryptoHasher('sha256')
199
+ hasher.update(token)
200
+ return hasher.digest('hex')
201
+ }
202
+ }