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/app/server/live/register-components.ts +6 -26
- package/core/build/bundler.ts +53 -5
- package/core/build/flux-plugins-generator.ts +315 -0
- package/core/build/index.ts +1 -3
- package/core/build/live-components-generator.ts +231 -0
- package/core/build/optimizer.ts +2 -54
- package/core/cli/index.ts +2 -1
- package/core/config/env.ts +3 -1
- package/core/config/schema.ts +0 -3
- package/core/framework/server.ts +33 -1
- package/core/plugins/built-in/static/index.ts +24 -10
- package/core/plugins/manager.ts +51 -8
- package/core/utils/helpers.ts +9 -3
- package/fluxstack.config.ts +4 -10
- package/package.json +4 -3
- package/plugins/crypto-auth/README.md +238 -0
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +325 -0
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +190 -0
- package/plugins/crypto-auth/client/components/LoginButton.tsx +155 -0
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +109 -0
- package/plugins/crypto-auth/client/components/SessionInfo.tsx +242 -0
- package/plugins/crypto-auth/client/components/index.ts +15 -0
- package/plugins/crypto-auth/client/index.ts +12 -0
- package/plugins/crypto-auth/index.ts +230 -0
- package/plugins/crypto-auth/package.json +65 -0
- package/plugins/crypto-auth/plugin.json +29 -0
- package/plugins/crypto-auth/server/AuthMiddleware.ts +237 -0
- package/plugins/crypto-auth/server/CryptoAuthService.ts +293 -0
- package/plugins/crypto-auth/server/index.ts +9 -0
- package/vite.config.ts +16 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-fluxstack",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "⚡
|
|
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
|
+
}
|