create-fluxstack 1.5.2 → 1.5.3

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 (51) hide show
  1. package/.env.example +8 -1
  2. package/CRYPTO-AUTH-MIDDLEWARE-GUIDE.md +475 -0
  3. package/CRYPTO-AUTH-MIDDLEWARES.md +473 -0
  4. package/CRYPTO-AUTH-USAGE.md +491 -0
  5. package/EXEMPLO-ROTA-PROTEGIDA.md +347 -0
  6. package/QUICK-START-CRYPTO-AUTH.md +221 -0
  7. package/app/client/src/App.tsx +4 -1
  8. package/app/client/src/pages/CryptoAuthPage.tsx +394 -0
  9. package/app/server/index.ts +4 -0
  10. package/app/server/live/FluxStackConfig.ts +1 -1
  11. package/app/server/routes/crypto-auth-demo.routes.ts +167 -0
  12. package/app/server/routes/example-with-crypto-auth.routes.ts +235 -0
  13. package/app/server/routes/exemplo-posts.routes.ts +161 -0
  14. package/app/server/routes/index.ts +5 -1
  15. package/config/index.ts +9 -1
  16. package/core/cli/generators/plugin.ts +324 -34
  17. package/core/cli/generators/template-engine.ts +5 -0
  18. package/core/cli/plugin-discovery.ts +33 -12
  19. package/core/framework/server.ts +10 -0
  20. package/core/plugins/dependency-manager.ts +89 -22
  21. package/core/plugins/index.ts +4 -0
  22. package/core/plugins/manager.ts +3 -2
  23. package/core/plugins/module-resolver.ts +216 -0
  24. package/core/plugins/registry.ts +28 -1
  25. package/core/utils/logger/index.ts +4 -0
  26. package/core/utils/version.ts +1 -1
  27. package/fluxstack.config.ts +253 -114
  28. package/package.json +3 -3
  29. package/plugins/crypto-auth/README.md +788 -0
  30. package/plugins/crypto-auth/ai-context.md +1282 -0
  31. package/plugins/crypto-auth/cli/make-protected-route.command.ts +383 -0
  32. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -0
  33. package/plugins/crypto-auth/client/components/AuthProvider.tsx +131 -0
  34. package/plugins/crypto-auth/client/components/LoginButton.tsx +138 -0
  35. package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +89 -0
  36. package/plugins/crypto-auth/client/components/index.ts +12 -0
  37. package/plugins/crypto-auth/client/index.ts +12 -0
  38. package/plugins/crypto-auth/config/index.ts +34 -0
  39. package/plugins/crypto-auth/index.ts +162 -0
  40. package/plugins/crypto-auth/package.json +66 -0
  41. package/plugins/crypto-auth/server/AuthMiddleware.ts +181 -0
  42. package/plugins/crypto-auth/server/CryptoAuthService.ts +186 -0
  43. package/plugins/crypto-auth/server/index.ts +22 -0
  44. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +65 -0
  45. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +26 -0
  46. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +76 -0
  47. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +45 -0
  48. package/plugins/crypto-auth/server/middlewares/helpers.ts +140 -0
  49. package/plugins/crypto-auth/server/middlewares/index.ts +22 -0
  50. package/plugins/crypto-auth/server/middlewares.ts +19 -0
  51. package/test-crypto-auth.ts +101 -0
@@ -0,0 +1,383 @@
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
+ }
@@ -0,0 +1,302 @@
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
+ }