create-fluxstack 1.5.0 → 1.5.2
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/.env.example +1 -8
- package/app/client/src/App.tsx +1 -4
- package/app/server/index.ts +0 -4
- package/app/server/routes/index.ts +1 -5
- package/config/index.ts +1 -9
- package/core/cli/generators/plugin.ts +34 -324
- package/core/cli/generators/template-engine.ts +0 -5
- package/core/cli/plugin-discovery.ts +12 -33
- package/core/framework/server.ts +0 -10
- package/core/plugins/dependency-manager.ts +22 -89
- package/core/plugins/index.ts +0 -4
- package/core/plugins/manager.ts +2 -3
- package/core/plugins/registry.ts +1 -28
- package/core/utils/logger/index.ts +0 -4
- package/core/utils/version.ts +1 -1
- package/fluxstack.config.ts +114 -253
- package/package.json +117 -117
- package/CRYPTO-AUTH-MIDDLEWARE-GUIDE.md +0 -475
- package/CRYPTO-AUTH-MIDDLEWARES.md +0 -473
- package/CRYPTO-AUTH-USAGE.md +0 -491
- package/EXEMPLO-ROTA-PROTEGIDA.md +0 -347
- package/QUICK-START-CRYPTO-AUTH.md +0 -221
- package/app/client/src/pages/CryptoAuthPage.tsx +0 -394
- package/app/server/routes/crypto-auth-demo.routes.ts +0 -167
- package/app/server/routes/example-with-crypto-auth.routes.ts +0 -235
- package/app/server/routes/exemplo-posts.routes.ts +0 -161
- package/core/plugins/module-resolver.ts +0 -216
- package/plugins/crypto-auth/README.md +0 -788
- package/plugins/crypto-auth/ai-context.md +0 -1282
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
- package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
- package/plugins/crypto-auth/client/components/index.ts +0 -12
- package/plugins/crypto-auth/client/index.ts +0 -12
- package/plugins/crypto-auth/config/index.ts +0 -34
- package/plugins/crypto-auth/index.ts +0 -162
- package/plugins/crypto-auth/package.json +0 -66
- package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
- package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
- package/plugins/crypto-auth/server/index.ts +0 -22
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -65
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -76
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
- package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -140
- package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
- package/plugins/crypto-auth/server/middlewares.ts +0 -19
- package/test-crypto-auth.ts +0 -101
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Command: make:protected-route
|
|
3
|
-
* Gera rotas protegidas automaticamente
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { CliCommand, CliContext } from '@/core/plugins/types'
|
|
7
|
-
import { writeFileSync, existsSync, mkdirSync } from 'fs'
|
|
8
|
-
import { join } from 'path'
|
|
9
|
-
|
|
10
|
-
const ROUTE_TEMPLATES = {
|
|
11
|
-
required: (name: string, pascalName: string) => `/**
|
|
12
|
-
* ${pascalName} Routes
|
|
13
|
-
* 🔒 Autenticação obrigatória
|
|
14
|
-
* Auto-gerado pelo comando: flux crypto-auth:make:route ${name} --auth required
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { Elysia, t } from 'elysia'
|
|
18
|
-
import { cryptoAuthRequired, getCryptoAuthUser } from '@/plugins/crypto-auth/server'
|
|
19
|
-
|
|
20
|
-
export const ${name}Routes = new Elysia({ prefix: '/${name}' })
|
|
21
|
-
|
|
22
|
-
// ========================================
|
|
23
|
-
// 🔒 ROTAS PROTEGIDAS (autenticação obrigatória)
|
|
24
|
-
// ========================================
|
|
25
|
-
.guard({}, (app) =>
|
|
26
|
-
app.use(cryptoAuthRequired())
|
|
27
|
-
|
|
28
|
-
// GET /api/${name}
|
|
29
|
-
.get('/', ({ request }) => {
|
|
30
|
-
const user = getCryptoAuthUser(request)!
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
success: true,
|
|
34
|
-
message: 'Lista de ${name}',
|
|
35
|
-
user: {
|
|
36
|
-
publicKey: user.publicKey.substring(0, 16) + '...',
|
|
37
|
-
isAdmin: user.isAdmin
|
|
38
|
-
},
|
|
39
|
-
data: []
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// GET /api/${name}/:id
|
|
44
|
-
.get('/:id', ({ request, params }) => {
|
|
45
|
-
const user = getCryptoAuthUser(request)!
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
success: true,
|
|
49
|
-
message: 'Detalhes de ${name}',
|
|
50
|
-
id: params.id,
|
|
51
|
-
user: user.publicKey.substring(0, 8) + '...'
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// POST /api/${name}
|
|
56
|
-
.post('/', ({ request, body }) => {
|
|
57
|
-
const user = getCryptoAuthUser(request)!
|
|
58
|
-
const data = body as any
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
success: true,
|
|
62
|
-
message: '${pascalName} criado com sucesso',
|
|
63
|
-
createdBy: user.publicKey.substring(0, 8) + '...',
|
|
64
|
-
data
|
|
65
|
-
}
|
|
66
|
-
}, {
|
|
67
|
-
body: t.Object({
|
|
68
|
-
// Adicione seus campos aqui
|
|
69
|
-
name: t.String({ minLength: 3 })
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// PUT /api/${name}/:id
|
|
74
|
-
.put('/:id', ({ request, params, body }) => {
|
|
75
|
-
const user = getCryptoAuthUser(request)!
|
|
76
|
-
const data = body as any
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
success: true,
|
|
80
|
-
message: '${pascalName} atualizado',
|
|
81
|
-
id: params.id,
|
|
82
|
-
updatedBy: user.publicKey.substring(0, 8) + '...',
|
|
83
|
-
data
|
|
84
|
-
}
|
|
85
|
-
}, {
|
|
86
|
-
body: t.Object({
|
|
87
|
-
name: t.String({ minLength: 3 })
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
// DELETE /api/${name}/:id
|
|
92
|
-
.delete('/:id', ({ request, params }) => {
|
|
93
|
-
const user = getCryptoAuthUser(request)!
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
success: true,
|
|
97
|
-
message: '${pascalName} deletado',
|
|
98
|
-
id: params.id,
|
|
99
|
-
deletedBy: user.publicKey.substring(0, 8) + '...'
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
)
|
|
103
|
-
`,
|
|
104
|
-
|
|
105
|
-
admin: (name: string, pascalName: string) => `/**
|
|
106
|
-
* ${pascalName} Routes
|
|
107
|
-
* 👑 Apenas administradores
|
|
108
|
-
* Auto-gerado pelo comando: flux crypto-auth:make:route ${name} --auth admin
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
import { Elysia, t } from 'elysia'
|
|
112
|
-
import { cryptoAuthAdmin, getCryptoAuthUser } from '@/plugins/crypto-auth/server'
|
|
113
|
-
|
|
114
|
-
export const ${name}Routes = new Elysia({ prefix: '/${name}' })
|
|
115
|
-
|
|
116
|
-
// ========================================
|
|
117
|
-
// 👑 ROTAS ADMIN (apenas administradores)
|
|
118
|
-
// ========================================
|
|
119
|
-
.guard({}, (app) =>
|
|
120
|
-
app.use(cryptoAuthAdmin())
|
|
121
|
-
|
|
122
|
-
// GET /api/${name}
|
|
123
|
-
.get('/', ({ request }) => {
|
|
124
|
-
const user = getCryptoAuthUser(request)!
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
success: true,
|
|
128
|
-
message: 'Painel administrativo de ${name}',
|
|
129
|
-
admin: user.publicKey.substring(0, 8) + '...',
|
|
130
|
-
data: []
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// POST /api/${name}
|
|
135
|
-
.post('/', ({ request, body }) => {
|
|
136
|
-
const user = getCryptoAuthUser(request)!
|
|
137
|
-
const data = body as any
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
success: true,
|
|
141
|
-
message: '${pascalName} criado pelo admin',
|
|
142
|
-
admin: user.publicKey.substring(0, 8) + '...',
|
|
143
|
-
data
|
|
144
|
-
}
|
|
145
|
-
}, {
|
|
146
|
-
body: t.Object({
|
|
147
|
-
name: t.String({ minLength: 3 })
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// DELETE /api/${name}/:id
|
|
152
|
-
.delete('/:id', ({ request, params }) => {
|
|
153
|
-
const user = getCryptoAuthUser(request)!
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
success: true,
|
|
157
|
-
message: '${pascalName} deletado pelo admin',
|
|
158
|
-
id: params.id,
|
|
159
|
-
admin: user.publicKey.substring(0, 8) + '...'
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
)
|
|
163
|
-
`,
|
|
164
|
-
|
|
165
|
-
optional: (name: string, pascalName: string) => `/**
|
|
166
|
-
* ${pascalName} Routes
|
|
167
|
-
* 🌓 Autenticação opcional
|
|
168
|
-
* Auto-gerado pelo comando: flux crypto-auth:make:route ${name} --auth optional
|
|
169
|
-
*/
|
|
170
|
-
|
|
171
|
-
import { Elysia } from 'elysia'
|
|
172
|
-
import { cryptoAuthOptional, getCryptoAuthUser } from '@/plugins/crypto-auth/server'
|
|
173
|
-
|
|
174
|
-
export const ${name}Routes = new Elysia({ prefix: '/${name}' })
|
|
175
|
-
|
|
176
|
-
// ========================================
|
|
177
|
-
// 🌐 ROTA PÚBLICA
|
|
178
|
-
// ========================================
|
|
179
|
-
.get('/', () => ({
|
|
180
|
-
success: true,
|
|
181
|
-
message: 'Lista pública de ${name}',
|
|
182
|
-
data: []
|
|
183
|
-
}))
|
|
184
|
-
|
|
185
|
-
// ========================================
|
|
186
|
-
// 🌓 ROTAS COM AUTH OPCIONAL
|
|
187
|
-
// ========================================
|
|
188
|
-
.guard({}, (app) =>
|
|
189
|
-
app.use(cryptoAuthOptional())
|
|
190
|
-
|
|
191
|
-
// GET /api/${name}/:id
|
|
192
|
-
.get('/:id', ({ request, params }) => {
|
|
193
|
-
const user = getCryptoAuthUser(request)
|
|
194
|
-
const isAuthenticated = !!user
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
success: true,
|
|
198
|
-
id: params.id,
|
|
199
|
-
message: isAuthenticated
|
|
200
|
-
? \`${pascalName} personalizado para \${user.publicKey.substring(0, 8)}...\`
|
|
201
|
-
: 'Visualização pública de ${name}',
|
|
202
|
-
// Conteúdo extra apenas para autenticados
|
|
203
|
-
premiumContent: isAuthenticated ? 'Conteúdo exclusivo' : null,
|
|
204
|
-
viewer: isAuthenticated
|
|
205
|
-
? user.publicKey.substring(0, 8) + '...'
|
|
206
|
-
: 'Visitante anônimo'
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
)
|
|
210
|
-
`,
|
|
211
|
-
|
|
212
|
-
public: (name: string, pascalName: string) => `/**
|
|
213
|
-
* ${pascalName} Routes
|
|
214
|
-
* 🌐 Totalmente público
|
|
215
|
-
* Auto-gerado pelo comando: flux crypto-auth:make:route ${name} --auth public
|
|
216
|
-
*/
|
|
217
|
-
|
|
218
|
-
import { Elysia } from 'elysia'
|
|
219
|
-
|
|
220
|
-
export const ${name}Routes = new Elysia({ prefix: '/${name}' })
|
|
221
|
-
|
|
222
|
-
// ========================================
|
|
223
|
-
// 🌐 ROTAS PÚBLICAS
|
|
224
|
-
// ========================================
|
|
225
|
-
|
|
226
|
-
// GET /api/${name}
|
|
227
|
-
.get('/', () => ({
|
|
228
|
-
success: true,
|
|
229
|
-
message: 'Lista de ${name}',
|
|
230
|
-
data: []
|
|
231
|
-
}))
|
|
232
|
-
|
|
233
|
-
// GET /api/${name}/:id
|
|
234
|
-
.get('/:id', ({ params }) => ({
|
|
235
|
-
success: true,
|
|
236
|
-
id: params.id,
|
|
237
|
-
message: 'Detalhes de ${name}'
|
|
238
|
-
}))
|
|
239
|
-
`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function toPascalCase(str: string): string {
|
|
243
|
-
return str
|
|
244
|
-
.split(/[-_]/)
|
|
245
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
246
|
-
.join('')
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export const makeProtectedRouteCommand: CliCommand = {
|
|
250
|
-
name: 'crypto-auth:make:route',
|
|
251
|
-
description: 'Gera um arquivo de rotas com proteção crypto-auth',
|
|
252
|
-
category: 'Crypto Auth',
|
|
253
|
-
aliases: ['crypto-auth:generate:route'],
|
|
254
|
-
|
|
255
|
-
arguments: [
|
|
256
|
-
{
|
|
257
|
-
name: 'name',
|
|
258
|
-
description: 'Nome da rota (ex: posts, users, admin)',
|
|
259
|
-
required: true,
|
|
260
|
-
type: 'string'
|
|
261
|
-
}
|
|
262
|
-
],
|
|
263
|
-
|
|
264
|
-
options: [
|
|
265
|
-
{
|
|
266
|
-
name: 'auth',
|
|
267
|
-
short: 'a',
|
|
268
|
-
description: 'Tipo de autenticação (required, admin, optional, public)',
|
|
269
|
-
type: 'string',
|
|
270
|
-
default: 'required',
|
|
271
|
-
choices: ['required', 'admin', 'optional', 'public']
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: 'output',
|
|
275
|
-
short: 'o',
|
|
276
|
-
description: 'Diretório de saída (padrão: app/server/routes)',
|
|
277
|
-
type: 'string',
|
|
278
|
-
default: 'app/server/routes'
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
name: 'force',
|
|
282
|
-
short: 'f',
|
|
283
|
-
description: 'Sobrescrever arquivo existente',
|
|
284
|
-
type: 'boolean',
|
|
285
|
-
default: false
|
|
286
|
-
}
|
|
287
|
-
],
|
|
288
|
-
|
|
289
|
-
examples: [
|
|
290
|
-
'flux crypto-auth:make:route posts',
|
|
291
|
-
'flux crypto-auth:make:route admin --auth admin',
|
|
292
|
-
'flux crypto-auth:make:route feed --auth optional',
|
|
293
|
-
'flux crypto-auth:make:route articles --auth required --force'
|
|
294
|
-
],
|
|
295
|
-
|
|
296
|
-
handler: async (args, options, context) => {
|
|
297
|
-
const [name] = args as [string]
|
|
298
|
-
const { auth, output, force } = options as {
|
|
299
|
-
auth: 'required' | 'admin' | 'optional' | 'public'
|
|
300
|
-
output: string
|
|
301
|
-
force: boolean
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Validar nome
|
|
305
|
-
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
306
|
-
console.error('❌ Nome inválido. Use apenas letras minúsculas, números e hífens.')
|
|
307
|
-
console.error(' Exemplos válidos: posts, my-posts, user-settings')
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const pascalName = toPascalCase(name)
|
|
312
|
-
const fileName = `${name}.routes.ts`
|
|
313
|
-
const outputDir = join(context.workingDir, output)
|
|
314
|
-
const filePath = join(outputDir, fileName)
|
|
315
|
-
|
|
316
|
-
// Verificar se arquivo existe
|
|
317
|
-
if (existsSync(filePath) && !force) {
|
|
318
|
-
console.error(`❌ Arquivo já existe: ${filePath}`)
|
|
319
|
-
console.error(' Use --force para sobrescrever')
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Criar diretório se não existir
|
|
324
|
-
if (!existsSync(outputDir)) {
|
|
325
|
-
mkdirSync(outputDir, { recursive: true })
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Gerar código
|
|
329
|
-
const template = ROUTE_TEMPLATES[auth]
|
|
330
|
-
const code = template(name, pascalName)
|
|
331
|
-
|
|
332
|
-
// Escrever arquivo
|
|
333
|
-
writeFileSync(filePath, code, 'utf-8')
|
|
334
|
-
|
|
335
|
-
console.log(`\n✅ Rota criada com sucesso!`)
|
|
336
|
-
console.log(`📁 Arquivo: ${filePath}`)
|
|
337
|
-
console.log(`🔐 Tipo de auth: ${auth}`)
|
|
338
|
-
|
|
339
|
-
// Instruções de uso
|
|
340
|
-
console.log(`\n📋 Próximos passos:`)
|
|
341
|
-
console.log(`\n1. Importar a rota em app/server/routes/index.ts:`)
|
|
342
|
-
console.log(` import { ${name}Routes } from './${name}.routes'`)
|
|
343
|
-
console.log(`\n2. Registrar no apiRoutes:`)
|
|
344
|
-
console.log(` export const apiRoutes = new Elysia({ prefix: '/api' })`)
|
|
345
|
-
console.log(` .use(${name}Routes)`)
|
|
346
|
-
console.log(`\n3. Rotas disponíveis:`)
|
|
347
|
-
|
|
348
|
-
const routes = {
|
|
349
|
-
required: [
|
|
350
|
-
`GET /api/${name}`,
|
|
351
|
-
`GET /api/${name}/:id`,
|
|
352
|
-
`POST /api/${name}`,
|
|
353
|
-
`PUT /api/${name}/:id`,
|
|
354
|
-
`DELETE /api/${name}/:id`
|
|
355
|
-
],
|
|
356
|
-
admin: [
|
|
357
|
-
`GET /api/${name}`,
|
|
358
|
-
`POST /api/${name}`,
|
|
359
|
-
`DELETE /api/${name}/:id`
|
|
360
|
-
],
|
|
361
|
-
optional: [
|
|
362
|
-
`GET /api/${name}`,
|
|
363
|
-
`GET /api/${name}/:id`
|
|
364
|
-
],
|
|
365
|
-
public: [
|
|
366
|
-
`GET /api/${name}`,
|
|
367
|
-
`GET /api/${name}/:id`
|
|
368
|
-
]
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
routes[auth].forEach(route => console.log(` ${route}`))
|
|
372
|
-
|
|
373
|
-
console.log(`\n4. Testar (sem auth):`)
|
|
374
|
-
console.log(` curl http://localhost:3000/api/${name}`)
|
|
375
|
-
|
|
376
|
-
if (auth !== 'public') {
|
|
377
|
-
const expectedStatus = auth === 'optional' ? '200 (sem conteúdo premium)' : '401'
|
|
378
|
-
console.log(` Esperado: ${expectedStatus}`)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
console.log(`\n🚀 Pronto! Inicie o servidor com: bun run dev`)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cliente de Autenticação Criptográfica
|
|
3
|
-
* Sistema baseado em assinatura Ed25519 SEM sessões no servidor
|
|
4
|
-
*
|
|
5
|
-
* Funcionamento:
|
|
6
|
-
* 1. Cliente gera par de chaves Ed25519 localmente
|
|
7
|
-
* 2. Chave privada NUNCA sai do navegador
|
|
8
|
-
* 3. Cada requisição é assinada automaticamente
|
|
9
|
-
* 4. Servidor valida assinatura usando chave pública recebida
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { ed25519 } from '@noble/curves/ed25519'
|
|
13
|
-
import { sha256 } from '@noble/hashes/sha256'
|
|
14
|
-
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
15
|
-
|
|
16
|
-
export interface KeyPair {
|
|
17
|
-
publicKey: string
|
|
18
|
-
privateKey: string
|
|
19
|
-
createdAt: Date
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AuthConfig {
|
|
23
|
-
storage?: 'localStorage' | 'sessionStorage' | 'memory'
|
|
24
|
-
autoInit?: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SignedRequestOptions extends RequestInit {
|
|
28
|
-
skipAuth?: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class CryptoAuthClient {
|
|
32
|
-
private keys: KeyPair | null = null
|
|
33
|
-
private config: AuthConfig
|
|
34
|
-
private storage: Storage | Map<string, string>
|
|
35
|
-
private readonly STORAGE_KEY = 'fluxstack_crypto_keys'
|
|
36
|
-
|
|
37
|
-
constructor(config: AuthConfig = {}) {
|
|
38
|
-
this.config = {
|
|
39
|
-
storage: 'localStorage',
|
|
40
|
-
autoInit: true,
|
|
41
|
-
...config
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Configurar storage
|
|
45
|
-
if (this.config.storage === 'localStorage' && typeof localStorage !== 'undefined') {
|
|
46
|
-
this.storage = localStorage
|
|
47
|
-
} else if (this.config.storage === 'sessionStorage' && typeof sessionStorage !== 'undefined') {
|
|
48
|
-
this.storage = sessionStorage
|
|
49
|
-
} else {
|
|
50
|
-
this.storage = new Map<string, string>()
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Auto-inicializar se configurado
|
|
54
|
-
if (this.config.autoInit) {
|
|
55
|
-
this.initialize()
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Inicializar (gerar ou carregar chaves)
|
|
61
|
-
*/
|
|
62
|
-
initialize(): KeyPair {
|
|
63
|
-
// Tentar carregar chaves existentes
|
|
64
|
-
const existingKeys = this.loadKeys()
|
|
65
|
-
if (existingKeys) {
|
|
66
|
-
this.keys = existingKeys
|
|
67
|
-
return existingKeys
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Criar novo par de chaves
|
|
71
|
-
return this.createNewKeys()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Criar novo par de chaves
|
|
76
|
-
* NUNCA envia chave privada ao servidor!
|
|
77
|
-
*/
|
|
78
|
-
createNewKeys(): KeyPair {
|
|
79
|
-
// Gerar par de chaves Ed25519
|
|
80
|
-
const privateKey = ed25519.utils.randomPrivateKey()
|
|
81
|
-
const publicKey = ed25519.getPublicKey(privateKey)
|
|
82
|
-
|
|
83
|
-
const keys: KeyPair = {
|
|
84
|
-
publicKey: bytesToHex(publicKey),
|
|
85
|
-
privateKey: bytesToHex(privateKey),
|
|
86
|
-
createdAt: new Date()
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.keys = keys
|
|
90
|
-
this.saveKeys(keys)
|
|
91
|
-
|
|
92
|
-
return keys
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Fazer requisição autenticada com assinatura
|
|
97
|
-
*/
|
|
98
|
-
async fetch(url: string, options: SignedRequestOptions = {}): Promise<Response> {
|
|
99
|
-
const { skipAuth = false, ...fetchOptions } = options
|
|
100
|
-
|
|
101
|
-
if (skipAuth) {
|
|
102
|
-
return fetch(url, fetchOptions)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!this.keys) {
|
|
106
|
-
this.initialize()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!this.keys) {
|
|
110
|
-
throw new Error('Chaves não inicializadas')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Preparar dados de autenticação
|
|
114
|
-
const timestamp = Date.now()
|
|
115
|
-
const nonce = this.generateNonce()
|
|
116
|
-
const message = this.buildMessage(fetchOptions.method || 'GET', url, fetchOptions.body)
|
|
117
|
-
const signature = this.signMessage(message, timestamp, nonce)
|
|
118
|
-
|
|
119
|
-
// Adicionar headers de autenticação
|
|
120
|
-
const headers = {
|
|
121
|
-
'Content-Type': 'application/json',
|
|
122
|
-
...fetchOptions.headers,
|
|
123
|
-
'x-public-key': this.keys.publicKey,
|
|
124
|
-
'x-timestamp': timestamp.toString(),
|
|
125
|
-
'x-nonce': nonce,
|
|
126
|
-
'x-signature': signature
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return fetch(url, {
|
|
130
|
-
...fetchOptions,
|
|
131
|
-
headers
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Obter chaves atuais
|
|
137
|
-
*/
|
|
138
|
-
getKeys(): KeyPair | null {
|
|
139
|
-
return this.keys
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Verificar se tem chaves
|
|
144
|
-
*/
|
|
145
|
-
isInitialized(): boolean {
|
|
146
|
-
return this.keys !== null
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Limpar chaves (logout)
|
|
151
|
-
*/
|
|
152
|
-
clearKeys(): void {
|
|
153
|
-
this.keys = null
|
|
154
|
-
if (this.storage instanceof Map) {
|
|
155
|
-
this.storage.delete(this.STORAGE_KEY)
|
|
156
|
-
} else {
|
|
157
|
-
this.storage.removeItem(this.STORAGE_KEY)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Importar chave privada existente
|
|
163
|
-
* @param privateKeyHex - Chave privada em formato hexadecimal (64 caracteres)
|
|
164
|
-
* @returns KeyPair com as chaves importadas
|
|
165
|
-
* @throws Error se a chave privada for inválida
|
|
166
|
-
*/
|
|
167
|
-
importPrivateKey(privateKeyHex: string): KeyPair {
|
|
168
|
-
// Validar formato
|
|
169
|
-
if (!/^[a-fA-F0-9]{64}$/.test(privateKeyHex)) {
|
|
170
|
-
throw new Error('Chave privada inválida. Deve ter 64 caracteres hexadecimais.')
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
// Converter hex para bytes
|
|
175
|
-
const privateKeyBytes = hexToBytes(privateKeyHex)
|
|
176
|
-
|
|
177
|
-
// Derivar chave pública da privada
|
|
178
|
-
const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes)
|
|
179
|
-
|
|
180
|
-
const keys: KeyPair = {
|
|
181
|
-
publicKey: bytesToHex(publicKeyBytes),
|
|
182
|
-
privateKey: privateKeyHex.toLowerCase(),
|
|
183
|
-
createdAt: new Date()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
this.keys = keys
|
|
187
|
-
this.saveKeys(keys)
|
|
188
|
-
|
|
189
|
-
return keys
|
|
190
|
-
} catch (error) {
|
|
191
|
-
throw new Error('Erro ao importar chave privada: ' + (error as Error).message)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Exportar chave privada (para backup)
|
|
197
|
-
* @returns Chave privada em formato hexadecimal
|
|
198
|
-
* @throws Error se não houver chaves inicializadas
|
|
199
|
-
*/
|
|
200
|
-
exportPrivateKey(): string {
|
|
201
|
-
if (!this.keys) {
|
|
202
|
-
throw new Error('Nenhuma chave inicializada para exportar')
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return this.keys.privateKey
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Assinar mensagem
|
|
210
|
-
*/
|
|
211
|
-
private signMessage(message: string, timestamp: number, nonce: string): string {
|
|
212
|
-
if (!this.keys) {
|
|
213
|
-
throw new Error('Chaves não inicializadas')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Construir mensagem completa: publicKey:timestamp:nonce:message
|
|
217
|
-
const fullMessage = `${this.keys.publicKey}:${timestamp}:${nonce}:${message}`
|
|
218
|
-
const messageHash = sha256(new TextEncoder().encode(fullMessage))
|
|
219
|
-
|
|
220
|
-
const privateKeyBytes = hexToBytes(this.keys.privateKey)
|
|
221
|
-
const signature = ed25519.sign(messageHash, privateKeyBytes)
|
|
222
|
-
|
|
223
|
-
return bytesToHex(signature)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Construir mensagem para assinatura
|
|
228
|
-
*/
|
|
229
|
-
private buildMessage(method: string, url: string, body?: BodyInit | null): string {
|
|
230
|
-
let message = `${method}:${url}`
|
|
231
|
-
|
|
232
|
-
if (body) {
|
|
233
|
-
if (typeof body === 'string') {
|
|
234
|
-
message += `:${body}`
|
|
235
|
-
} else {
|
|
236
|
-
message += `:${JSON.stringify(body)}`
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return message
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Gerar nonce aleatório
|
|
245
|
-
*/
|
|
246
|
-
private generateNonce(): string {
|
|
247
|
-
const bytes = new Uint8Array(16)
|
|
248
|
-
crypto.getRandomValues(bytes)
|
|
249
|
-
return bytesToHex(bytes)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Carregar chaves do storage
|
|
254
|
-
*/
|
|
255
|
-
private loadKeys(): KeyPair | null {
|
|
256
|
-
try {
|
|
257
|
-
let data: string | null
|
|
258
|
-
|
|
259
|
-
if (this.storage instanceof Map) {
|
|
260
|
-
data = this.storage.get(this.STORAGE_KEY) || null
|
|
261
|
-
} else {
|
|
262
|
-
data = this.storage.getItem(this.STORAGE_KEY)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!data) {
|
|
266
|
-
return null
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const parsed = JSON.parse(data)
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
publicKey: parsed.publicKey,
|
|
273
|
-
privateKey: parsed.privateKey,
|
|
274
|
-
createdAt: new Date(parsed.createdAt)
|
|
275
|
-
}
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error('Erro ao carregar chaves:', error)
|
|
278
|
-
return null
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Salvar chaves no storage
|
|
284
|
-
*/
|
|
285
|
-
private saveKeys(keys: KeyPair): void {
|
|
286
|
-
try {
|
|
287
|
-
const data = JSON.stringify({
|
|
288
|
-
publicKey: keys.publicKey,
|
|
289
|
-
privateKey: keys.privateKey,
|
|
290
|
-
createdAt: keys.createdAt.toISOString()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
if (this.storage instanceof Map) {
|
|
294
|
-
this.storage.set(this.STORAGE_KEY, data)
|
|
295
|
-
} else {
|
|
296
|
-
this.storage.setItem(this.STORAGE_KEY, data)
|
|
297
|
-
}
|
|
298
|
-
} catch (error) {
|
|
299
|
-
console.error('Erro ao salvar chaves:', error)
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|