create-fluxstack 1.0.22 → 1.1.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-fluxstack",
3
- "version": "1.0.22",
4
- "description": "⚡ Modern full-stack TypeScript framework with Elysia + React + Bun",
3
+ "version": "1.1.0",
4
+ "description": "⚡ Revolutionary full-stack TypeScript framework with Temporal Bridge Auto-Discovery, Elysia + React + Bun",
5
5
  "keywords": [
6
6
  "framework",
7
7
  "full-stack",
@@ -29,7 +29,7 @@
29
29
  "dev:backend": "bun run core/cli/index.ts backend",
30
30
  "dev:coordinated": "concurrently --prefix {name} --names BACKEND,VITE --prefix-colors blue,green --kill-others-on-fail \"bun --watch app/server/index.ts\" \"vite --config vite.config.ts\"",
31
31
  "dev:clean": "bun run run-clean.ts",
32
- "build": "bun run core/cli/index.ts build",
32
+ "build": "cross-env NODE_ENV=production bun run core/cli/index.ts build",
33
33
  "build:frontend": "vite build --config vite.config.ts --emptyOutDir",
34
34
  "build:backend": "bun run core/cli/index.ts build:backend",
35
35
  "start": "bun run core/cli/index.ts start",
@@ -70,6 +70,7 @@
70
70
  "@vitest/coverage-v8": "^3.2.4",
71
71
  "@vitest/ui": "^3.2.4",
72
72
  "concurrently": "^9.2.0",
73
+ "cross-env": "^10.1.0",
73
74
  "eslint": "^9.30.1",
74
75
  "eslint-plugin-react-hooks": "^5.2.0",
75
76
  "eslint-plugin-react-refresh": "^0.4.20",
@@ -0,0 +1,238 @@
1
+ # FluxStack Crypto Auth Plugin
2
+
3
+ Plugin de autenticação criptográfica baseado em Ed25519 para FluxStack.
4
+
5
+ ## Características
6
+
7
+ - 🔐 **Zero-friction auth** - Sem cadastros, senhas ou emails
8
+ - ✅ **Criptografia Ed25519** - Assinatura criptográfica de todas as requisições
9
+ - 🌐 **Cross-platform** - Funciona em qualquer ambiente JavaScript
10
+ - 💾 **Stateless backend** - Não precisa de banco para autenticação
11
+ - 🎨 **Componentes React** - Componentes prontos para uso
12
+ - ⚡ **Integração FluxStack** - Plugin nativo do FluxStack
13
+
14
+ ## Instalação
15
+
16
+ ```bash
17
+ # O plugin já está incluído na pasta plugins/
18
+ # Apenas habilite no fluxstack.config.ts
19
+ ```
20
+
21
+ ## Configuração
22
+
23
+ No seu `fluxstack.config.ts`:
24
+
25
+ ```typescript
26
+ export const config: FluxStackConfig = {
27
+ // ... outras configurações
28
+
29
+ plugins: {
30
+ enabled: ['crypto-auth'],
31
+ config: {
32
+ 'crypto-auth': {
33
+ enabled: true,
34
+ sessionTimeout: 1800000, // 30 minutos
35
+ maxTimeDrift: 300000, // 5 minutos
36
+ adminKeys: [
37
+ 'sua_chave_publica_admin_aqui'
38
+ ],
39
+ protectedRoutes: [
40
+ '/api/admin/*',
41
+ '/api/protected/*'
42
+ ],
43
+ publicRoutes: [
44
+ '/api/auth/*',
45
+ '/api/health',
46
+ '/api/docs'
47
+ ],
48
+ enableMetrics: true
49
+ }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Uso no Frontend
56
+
57
+ ### 1. Configurar o Provider
58
+
59
+ ```tsx
60
+ import React from 'react'
61
+ import { AuthProvider } from '@/plugins/crypto-auth/client'
62
+
63
+ function App() {
64
+ return (
65
+ <AuthProvider
66
+ config={{
67
+ apiBaseUrl: 'http://localhost:3000',
68
+ storage: 'localStorage'
69
+ }}
70
+ onAuthChange={(isAuth, session) => {
71
+ console.log('Auth changed:', isAuth, session)
72
+ }}
73
+ >
74
+ <YourApp />
75
+ </AuthProvider>
76
+ )
77
+ }
78
+ ```
79
+
80
+ ### 2. Usar o Hook de Autenticação
81
+
82
+ ```tsx
83
+ import React from 'react'
84
+ import { useAuth } from '@/plugins/crypto-auth/client'
85
+
86
+ function Dashboard() {
87
+ const {
88
+ isAuthenticated,
89
+ isAdmin,
90
+ permissions,
91
+ login,
92
+ logout,
93
+ client
94
+ } = useAuth()
95
+
96
+ const handleApiCall = async () => {
97
+ try {
98
+ const response = await client.fetch('/api/protected/data')
99
+ const data = await response.json()
100
+ console.log(data)
101
+ } catch (error) {
102
+ console.error('Erro na API:', error)
103
+ }
104
+ }
105
+
106
+ if (!isAuthenticated) {
107
+ return <button onClick={login}>Entrar</button>
108
+ }
109
+
110
+ return (
111
+ <div>
112
+ <h1>Dashboard {isAdmin && '(Admin)'}</h1>
113
+ <p>Permissões: {permissions.join(', ')}</p>
114
+ <button onClick={handleApiCall}>Chamar API</button>
115
+ <button onClick={logout}>Sair</button>
116
+ </div>
117
+ )
118
+ }
119
+ ```
120
+
121
+ ### 3. Componentes Prontos
122
+
123
+ ```tsx
124
+ import React from 'react'
125
+ import {
126
+ LoginButton,
127
+ ProtectedRoute,
128
+ SessionInfo
129
+ } from '@/plugins/crypto-auth/client'
130
+
131
+ function MyApp() {
132
+ return (
133
+ <div>
134
+ {/* Botão de login/logout */}
135
+ <LoginButton
136
+ onLogin={(session) => console.log('Logado:', session)}
137
+ onLogout={() => console.log('Deslogado')}
138
+ showPermissions={true}
139
+ />
140
+
141
+ {/* Rota protegida */}
142
+ <ProtectedRoute requireAdmin={true}>
143
+ <AdminPanel />
144
+ </ProtectedRoute>
145
+
146
+ {/* Informações da sessão */}
147
+ <SessionInfo
148
+ showPrivateKey={false}
149
+ compact={true}
150
+ />
151
+ </div>
152
+ )
153
+ }
154
+ ```
155
+
156
+ ### 4. HOC para Proteção
157
+
158
+ ```tsx
159
+ import { withAuth } from '@/plugins/crypto-auth/client'
160
+
161
+ const AdminComponent = () => <div>Área Admin</div>
162
+
163
+ // Proteger componente
164
+ const ProtectedAdmin = withAuth(AdminComponent, {
165
+ requireAdmin: true,
166
+ requiredPermissions: ['admin']
167
+ })
168
+ ```
169
+
170
+ ## Uso no Backend
171
+
172
+ O plugin automaticamente:
173
+
174
+ - Registra rotas de autenticação em `/api/auth/*`
175
+ - Aplica middleware de autenticação nas rotas protegidas
176
+ - Valida assinaturas Ed25519 em cada requisição
177
+ - Gerencia sessões em memória
178
+
179
+ ### Rotas Automáticas
180
+
181
+ - `POST /api/auth/session/init` - Inicializar sessão
182
+ - `POST /api/auth/session/validate` - Validar sessão
183
+ - `GET /api/auth/session/info` - Informações da sessão
184
+ - `POST /api/auth/session/logout` - Encerrar sessão
185
+
186
+ ### Acessar Usuário nas Rotas
187
+
188
+ ```typescript
189
+ // Em suas rotas FluxStack
190
+ app.get('/api/protected/data', ({ user }) => {
191
+ // user estará disponível se autenticado
192
+ if (!user) {
193
+ return { error: 'Não autenticado' }
194
+ }
195
+
196
+ return {
197
+ message: 'Dados protegidos',
198
+ user: {
199
+ sessionId: user.sessionId,
200
+ isAdmin: user.isAdmin,
201
+ permissions: user.permissions
202
+ }
203
+ }
204
+ })
205
+ ```
206
+
207
+ ## Como Funciona
208
+
209
+ 1. **Cliente gera** par de chaves Ed25519 automaticamente
210
+ 2. **Session ID** = chave pública (64 chars hex)
211
+ 3. **Todas as requisições** são assinadas com a chave privada
212
+ 4. **Backend valida** a assinatura Ed25519 em cada request
213
+ 5. **Admin access** via chaves públicas autorizadas
214
+
215
+ ## Segurança
216
+
217
+ - ✅ Assinatura Ed25519 em todas as requisições
218
+ - ✅ Nonce para prevenir replay attacks
219
+ - ✅ Validação de timestamp (5 min máximo)
220
+ - ✅ Timeout de sessões (30 min padrão)
221
+ - ✅ Chaves privadas nunca saem do cliente
222
+ - ✅ Stateless - sem dependência de banco de dados
223
+
224
+ ## Desenvolvimento
225
+
226
+ Para desenvolver o plugin:
227
+
228
+ ```bash
229
+ # Instalar dependências
230
+ npm install @noble/curves @noble/hashes
231
+
232
+ # Para desenvolvimento com React
233
+ npm install --save-dev @types/react react
234
+ ```
235
+
236
+ ## Licença
237
+
238
+ MIT
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Cliente de Autenticação Criptográfica
3
+ * Gerencia autenticação no lado do cliente
4
+ */
5
+
6
+ import { ed25519 } from '@noble/curves/ed25519'
7
+ import { sha256 } from '@noble/hashes/sha256'
8
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
9
+
10
+ export interface SessionInfo {
11
+ sessionId: string
12
+ publicKey: string
13
+ privateKey: string
14
+ isAdmin: boolean
15
+ permissions: string[]
16
+ createdAt: Date
17
+ lastUsed: Date
18
+ }
19
+
20
+ export interface AuthConfig {
21
+ apiBaseUrl?: string
22
+ storage?: 'localStorage' | 'sessionStorage' | 'memory'
23
+ autoInit?: boolean
24
+ sessionTimeout?: number
25
+ }
26
+
27
+ export interface SignedRequestOptions extends RequestInit {
28
+ skipAuth?: boolean
29
+ }
30
+
31
+ export class CryptoAuthClient {
32
+ private session: SessionInfo | null = null
33
+ private config: AuthConfig
34
+ private storage: Storage | Map<string, string>
35
+
36
+ constructor(config: AuthConfig = {}) {
37
+ this.config = {
38
+ apiBaseUrl: '',
39
+ storage: 'localStorage',
40
+ autoInit: true,
41
+ sessionTimeout: 1800000, // 30 minutos
42
+ ...config
43
+ }
44
+
45
+ // Configurar storage
46
+ if (this.config.storage === 'localStorage' && typeof localStorage !== 'undefined') {
47
+ this.storage = localStorage
48
+ } else if (this.config.storage === 'sessionStorage' && typeof sessionStorage !== 'undefined') {
49
+ this.storage = sessionStorage
50
+ } else {
51
+ this.storage = new Map<string, string>()
52
+ }
53
+
54
+ // Auto-inicializar se configurado
55
+ if (this.config.autoInit) {
56
+ this.initialize()
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Inicializar sessão
62
+ */
63
+ async initialize(): Promise<SessionInfo> {
64
+ // Tentar carregar sessão existente
65
+ const existingSession = this.loadSession()
66
+ if (existingSession && this.isSessionValid(existingSession)) {
67
+ this.session = existingSession
68
+ return existingSession
69
+ }
70
+
71
+ // Criar nova sessão
72
+ return this.createNewSession()
73
+ }
74
+
75
+ /**
76
+ * Criar nova sessão
77
+ */
78
+ async createNewSession(): Promise<SessionInfo> {
79
+ try {
80
+ // Gerar par de chaves
81
+ const privateKey = ed25519.utils.randomPrivateKey()
82
+ const publicKey = ed25519.getPublicKey(privateKey)
83
+
84
+ const sessionId = bytesToHex(publicKey)
85
+ const privateKeyHex = bytesToHex(privateKey)
86
+
87
+ // Registrar sessão no servidor
88
+ const response = await fetch(`${this.config.apiBaseUrl}/api/auth/session/init`, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json'
92
+ },
93
+ body: JSON.stringify({
94
+ publicKey: sessionId
95
+ })
96
+ })
97
+
98
+ if (!response.ok) {
99
+ throw new Error(`Erro ao inicializar sessão: ${response.statusText}`)
100
+ }
101
+
102
+ const result = await response.json()
103
+ if (!result.success) {
104
+ throw new Error(result.error || 'Erro desconhecido ao inicializar sessão')
105
+ }
106
+
107
+ // Criar objeto de sessão
108
+ const session: SessionInfo = {
109
+ sessionId,
110
+ publicKey: sessionId,
111
+ privateKey: privateKeyHex,
112
+ isAdmin: result.user?.isAdmin || false,
113
+ permissions: result.user?.permissions || ['read'],
114
+ createdAt: new Date(),
115
+ lastUsed: new Date()
116
+ }
117
+
118
+ this.session = session
119
+ this.saveSession(session)
120
+
121
+ return session
122
+ } catch (error) {
123
+ console.error('Erro ao criar nova sessão:', error)
124
+ throw error
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Fazer requisição autenticada
130
+ */
131
+ async fetch(url: string, options: SignedRequestOptions = {}): Promise<Response> {
132
+ const { skipAuth = false, ...fetchOptions } = options
133
+
134
+ if (skipAuth) {
135
+ return fetch(url, fetchOptions)
136
+ }
137
+
138
+ if (!this.session) {
139
+ await this.initialize()
140
+ }
141
+
142
+ if (!this.session) {
143
+ throw new Error('Sessão não inicializada')
144
+ }
145
+
146
+ // Preparar headers de autenticação
147
+ const timestamp = Date.now()
148
+ const nonce = this.generateNonce()
149
+ const message = this.buildMessage(fetchOptions.method || 'GET', url, fetchOptions.body)
150
+ const signature = this.signMessage(message, timestamp, nonce)
151
+
152
+ const headers = {
153
+ 'Content-Type': 'application/json',
154
+ ...fetchOptions.headers,
155
+ 'x-session-id': this.session.sessionId,
156
+ 'x-timestamp': timestamp.toString(),
157
+ 'x-nonce': nonce,
158
+ 'x-signature': signature
159
+ }
160
+
161
+ // Atualizar último uso
162
+ this.session.lastUsed = new Date()
163
+ this.saveSession(this.session)
164
+
165
+ return fetch(url, {
166
+ ...fetchOptions,
167
+ headers
168
+ })
169
+ }
170
+
171
+ /**
172
+ * Obter informações da sessão atual
173
+ */
174
+ getSession(): SessionInfo | null {
175
+ return this.session
176
+ }
177
+
178
+ /**
179
+ * Verificar se está autenticado
180
+ */
181
+ isAuthenticated(): boolean {
182
+ return this.session !== null && this.isSessionValid(this.session)
183
+ }
184
+
185
+ /**
186
+ * Verificar se é admin
187
+ */
188
+ isAdmin(): boolean {
189
+ return this.session?.isAdmin || false
190
+ }
191
+
192
+ /**
193
+ * Obter permissões
194
+ */
195
+ getPermissions(): string[] {
196
+ return this.session?.permissions || []
197
+ }
198
+
199
+ /**
200
+ * Fazer logout
201
+ */
202
+ async logout(): Promise<void> {
203
+ if (this.session) {
204
+ try {
205
+ await this.fetch(`${this.config.apiBaseUrl}/api/auth/session/logout`, {
206
+ method: 'POST'
207
+ })
208
+ } catch (error) {
209
+ console.warn('Erro ao fazer logout no servidor:', error)
210
+ }
211
+
212
+ this.session = null
213
+ this.clearSession()
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Assinar mensagem
219
+ */
220
+ private signMessage(message: string, timestamp: number, nonce: string): string {
221
+ if (!this.session) {
222
+ throw new Error('Sessão não inicializada')
223
+ }
224
+
225
+ const fullMessage = `${this.session.sessionId}:${timestamp}:${nonce}:${message}`
226
+ const messageHash = sha256(new TextEncoder().encode(fullMessage))
227
+ const privateKeyBytes = hexToBytes(this.session.privateKey)
228
+ const signature = ed25519.sign(messageHash, privateKeyBytes)
229
+
230
+ return bytesToHex(signature)
231
+ }
232
+
233
+ /**
234
+ * Construir mensagem para assinatura
235
+ */
236
+ private buildMessage(method: string, url: string, body?: any): string {
237
+ const urlObj = new URL(url, window.location.origin)
238
+ let message = `${method}:${urlObj.pathname}`
239
+
240
+ if (body) {
241
+ if (typeof body === 'string') {
242
+ message += `:${body}`
243
+ } else {
244
+ message += `:${JSON.stringify(body)}`
245
+ }
246
+ }
247
+
248
+ return message
249
+ }
250
+
251
+ /**
252
+ * Gerar nonce aleatório
253
+ */
254
+ private generateNonce(): string {
255
+ const array = new Uint8Array(16)
256
+ crypto.getRandomValues(array)
257
+ return bytesToHex(array)
258
+ }
259
+
260
+ /**
261
+ * Verificar se sessão é válida
262
+ */
263
+ private isSessionValid(session: SessionInfo): boolean {
264
+ const now = Date.now()
265
+ const sessionAge = now - session.lastUsed.getTime()
266
+ return sessionAge < (this.config.sessionTimeout || 1800000)
267
+ }
268
+
269
+ /**
270
+ * Salvar sessão no storage
271
+ */
272
+ private saveSession(session: SessionInfo): void {
273
+ const sessionData = JSON.stringify({
274
+ ...session,
275
+ createdAt: session.createdAt.toISOString(),
276
+ lastUsed: session.lastUsed.toISOString()
277
+ })
278
+
279
+ if (this.storage instanceof Map) {
280
+ this.storage.set('crypto-auth-session', sessionData)
281
+ } else {
282
+ this.storage.setItem('crypto-auth-session', sessionData)
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Carregar sessão do storage
288
+ */
289
+ private loadSession(): SessionInfo | null {
290
+ try {
291
+ let sessionData: string | null
292
+
293
+ if (this.storage instanceof Map) {
294
+ sessionData = this.storage.get('crypto-auth-session') || null
295
+ } else {
296
+ sessionData = this.storage.getItem('crypto-auth-session')
297
+ }
298
+
299
+ if (!sessionData) {
300
+ return null
301
+ }
302
+
303
+ const parsed = JSON.parse(sessionData)
304
+ return {
305
+ ...parsed,
306
+ createdAt: new Date(parsed.createdAt),
307
+ lastUsed: new Date(parsed.lastUsed)
308
+ }
309
+ } catch (error) {
310
+ console.warn('Erro ao carregar sessão:', error)
311
+ return null
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Limpar sessão do storage
317
+ */
318
+ private clearSession(): void {
319
+ if (this.storage instanceof Map) {
320
+ this.storage.delete('crypto-auth-session')
321
+ } else {
322
+ this.storage.removeItem('crypto-auth-session')
323
+ }
324
+ }
325
+ }