smoonb 0.0.6 → 0.0.8

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 (35) hide show
  1. package/.smoonbrc +7 -6
  2. package/.smoonbrc.example +2 -1
  3. package/README.md +164 -164
  4. package/backups/backup-2025-10-17T19-52-20-211Z/auth-config.json +7 -0
  5. package/backups/backup-2025-10-17T19-52-20-211Z/backup-manifest.json +19 -0
  6. package/backups/backup-2025-10-17T19-52-20-211Z/database-2025-10-17T19-52-20-215Z.dump +0 -0
  7. package/backups/backup-2025-10-17T19-52-20-211Z/functions/README.md +4 -0
  8. package/backups/backup-2025-10-17T19-52-20-211Z/realtime-config.json +7 -0
  9. package/backups/backup-2025-10-17T19-52-20-211Z/storage/storage-config.json +6 -0
  10. package/backups/backup-2025-10-17T20-38-13-188Z/auth-config.json +7 -0
  11. package/backups/backup-2025-10-17T20-38-13-188Z/backup-manifest.json +19 -0
  12. package/backups/backup-2025-10-17T20-38-13-188Z/database-2025-10-17T20-38-13-194Z.dump +0 -0
  13. package/backups/backup-2025-10-17T20-38-13-188Z/functions/README.md +4 -0
  14. package/backups/backup-2025-10-17T20-38-13-188Z/realtime-config.json +7 -0
  15. package/backups/backup-2025-10-17T20-38-13-188Z/storage/storage-config.json +6 -0
  16. package/bin/smoonb.js +16 -32
  17. package/package.json +1 -1
  18. package/src/commands/backup.js +140 -239
  19. package/src/commands/check.js +209 -349
  20. package/src/commands/config.js +78 -77
  21. package/src/commands/functions.js +123 -349
  22. package/src/commands/restore.js +122 -294
  23. package/src/index.js +12 -21
  24. package/src/services/introspect.js +299 -0
  25. package/src/utils/cli.js +87 -0
  26. package/src/utils/config.js +140 -0
  27. package/src/utils/fsx.js +110 -0
  28. package/src/utils/hash.js +40 -0
  29. package/src/utils/supabase.js +447 -387
  30. package/src/commands/secrets.js +0 -361
  31. /package/{backup → backups}/backup-2025-10-17T19-18-58-539Z/auth-config.json +0 -0
  32. /package/{backup → backups}/backup-2025-10-17T19-18-58-539Z/backup-manifest.json +0 -0
  33. /package/{backup → backups}/backup-2025-10-17T19-18-58-539Z/functions/README.md +0 -0
  34. /package/{backup → backups}/backup-2025-10-17T19-18-58-539Z/realtime-config.json +0 -0
  35. /package/{backup → backups}/backup-2025-10-17T19-18-58-539Z/storage/storage-config.json +0 -0
@@ -0,0 +1,299 @@
1
+ const { createClient } = require('@supabase/supabase-js');
2
+ const { runCommand } = require('./cli');
3
+
4
+ /**
5
+ * Serviço de introspecção do banco de dados Supabase
6
+ */
7
+ class IntrospectionService {
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.supabase = createClient(config.supabase.url, config.supabase.serviceKey);
11
+ }
12
+
13
+ /**
14
+ * Executa query SQL no banco
15
+ * @param {string} query - Query SQL
16
+ * @returns {Promise<any[]>} - Resultado da query
17
+ */
18
+ async executeQuery(query) {
19
+ const { data, error } = await this.supabase.rpc('exec_sql', { sql: query });
20
+ if (error) {
21
+ throw new Error(`Erro na query: ${error.message}`);
22
+ }
23
+ return data || [];
24
+ }
25
+
26
+ /**
27
+ * Obtém inventário de extensões
28
+ * @returns {Promise<object>} - Lista de extensões
29
+ */
30
+ async getExtensions() {
31
+ try {
32
+ const query = `SELECT extname, extversion FROM pg_extension ORDER BY extname;`;
33
+ const result = await this.executeQuery(query);
34
+ return {
35
+ extensions: result.map(row => ({
36
+ name: row.extname,
37
+ version: row.extversion
38
+ }))
39
+ };
40
+ } catch (error) {
41
+ console.warn('⚠️ Não foi possível obter extensões:', error.message);
42
+ return { extensions: [] };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Obtém inventário de tabelas
48
+ * @returns {Promise<object>} - Lista de tabelas
49
+ */
50
+ async getTables() {
51
+ try {
52
+ const query = `
53
+ SELECT schemaname, tablename, tableowner
54
+ FROM pg_tables
55
+ WHERE schemaname IN ('public', 'auth', 'storage')
56
+ ORDER BY schemaname, tablename;
57
+ `;
58
+ const result = await this.executeQuery(query);
59
+ return {
60
+ tables: result.map(row => ({
61
+ schema: row.schemaname,
62
+ name: row.tablename,
63
+ owner: row.tableowner
64
+ }))
65
+ };
66
+ } catch (error) {
67
+ console.warn('⚠️ Não foi possível obter tabelas:', error.message);
68
+ return { tables: [] };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Obtém inventário de políticas RLS
74
+ * @returns {Promise<object>} - Lista de políticas
75
+ */
76
+ async getPolicies() {
77
+ try {
78
+ const query = `
79
+ SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
80
+ FROM pg_policies
81
+ ORDER BY schemaname, tablename, policyname;
82
+ `;
83
+ const result = await this.executeQuery(query);
84
+ return {
85
+ policies: result.map(row => ({
86
+ schema: row.schemaname,
87
+ table: row.tablename,
88
+ name: row.policyname,
89
+ permissive: row.permissive,
90
+ roles: row.roles,
91
+ command: row.cmd,
92
+ qual: row.qual,
93
+ withCheck: row.with_check
94
+ }))
95
+ };
96
+ } catch (error) {
97
+ console.warn('⚠️ Não foi possível obter políticas:', error.message);
98
+ return { policies: [] };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Obtém inventário de configurações RLS
104
+ * @returns {Promise<object>} - Status RLS por tabela
105
+ */
106
+ async getRLSStatus() {
107
+ try {
108
+ const query = `
109
+ SELECT n.nspname as schema_name, c.relname as table_name, c.relrowsecurity, c.relforcerowsecurity
110
+ FROM pg_class c
111
+ JOIN pg_namespace n ON n.oid = c.relnamespace
112
+ WHERE c.relkind = 'r' AND n.nspname IN ('public', 'auth', 'storage')
113
+ ORDER BY n.nspname, c.relname;
114
+ `;
115
+ const result = await this.executeQuery(query);
116
+ return {
117
+ rlsStatus: result.map(row => ({
118
+ schema: row.schema_name,
119
+ table: row.table_name,
120
+ rowSecurityEnabled: row.relrowsecurity,
121
+ forceRowSecurity: row.relforcerowsecurity
122
+ }))
123
+ };
124
+ } catch (error) {
125
+ console.warn('⚠️ Não foi possível obter status RLS:', error.message);
126
+ return { rlsStatus: [] };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Obtém inventário de publicações Realtime
132
+ * @returns {Promise<object>} - Lista de publicações
133
+ */
134
+ async getRealtimePublications() {
135
+ try {
136
+ const publicationsQuery = `SELECT pubname FROM pg_publication ORDER BY pubname;`;
137
+ const publications = await this.executeQuery(publicationsQuery);
138
+
139
+ const publicationTablesQuery = `
140
+ SELECT p.pubname, c.relname as table_name, n.nspname as schema_name
141
+ FROM pg_publication_tables pt
142
+ JOIN pg_publication p ON p.oid = pt.ptpubid
143
+ JOIN pg_class c ON c.oid = pt.ptrelid
144
+ JOIN pg_namespace n ON n.oid = c.relnamespace
145
+ ORDER BY p.pubname, n.nspname, c.relname;
146
+ `;
147
+ const publicationTables = await this.executeQuery(publicationTablesQuery);
148
+
149
+ return {
150
+ publications: publications.map(row => ({
151
+ name: row.pubname,
152
+ tables: publicationTables
153
+ .filter(pt => pt.pubname === row.pubname)
154
+ .map(pt => ({
155
+ schema: pt.schema_name,
156
+ table: pt.table_name
157
+ }))
158
+ }))
159
+ };
160
+ } catch (error) {
161
+ console.warn('⚠️ Não foi possível obter publicações Realtime:', error.message);
162
+ return { publications: [] };
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Obtém inventário de buckets de Storage
168
+ * @returns {Promise<object>} - Lista de buckets e objetos
169
+ */
170
+ async getStorageInventory() {
171
+ try {
172
+ const { data: buckets, error: bucketsError } = await this.supabase.storage.listBuckets();
173
+
174
+ if (bucketsError) {
175
+ throw new Error(bucketsError.message);
176
+ }
177
+
178
+ const inventory = {
179
+ buckets: []
180
+ };
181
+
182
+ for (const bucket of buckets) {
183
+ const bucketInfo = {
184
+ id: bucket.id,
185
+ name: bucket.name,
186
+ public: bucket.public,
187
+ file_size_limit: bucket.file_size_limit,
188
+ allowed_mime_types: bucket.allowed_mime_types,
189
+ objects: []
190
+ };
191
+
192
+ try {
193
+ // Listar objetos do bucket (sem baixar conteúdo)
194
+ const { data: objects, error: objectsError } = await this.supabase.storage
195
+ .from(bucket.name)
196
+ .list('', { limit: 1000 });
197
+
198
+ if (!objectsError && objects) {
199
+ bucketInfo.objects = objects.map(obj => ({
200
+ name: obj.name,
201
+ size: obj.metadata?.size,
202
+ last_modified: obj.updated_at,
203
+ content_type: obj.metadata?.mimetype
204
+ }));
205
+ }
206
+ } catch (error) {
207
+ console.warn(`⚠️ Não foi possível listar objetos do bucket ${bucket.name}:`, error.message);
208
+ }
209
+
210
+ inventory.buckets.push(bucketInfo);
211
+ }
212
+
213
+ return inventory;
214
+ } catch (error) {
215
+ console.warn('⚠️ Não foi possível obter inventário de Storage:', error.message);
216
+ return { buckets: [] };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Obtém inventário de Edge Functions
222
+ * @returns {Promise<object>} - Lista de functions
223
+ */
224
+ async getEdgeFunctions() {
225
+ try {
226
+ const { stdout } = await runCommand('supabase functions list', {
227
+ env: { ...process.env, SUPABASE_ACCESS_TOKEN: this.config.supabase.serviceKey }
228
+ });
229
+
230
+ // Parse da saída do comando
231
+ const functions = [];
232
+ const lines = stdout.split('\n');
233
+
234
+ for (const line of lines) {
235
+ const trimmed = line.trim();
236
+ if (trimmed && !trimmed.startsWith('NAME') && !trimmed.startsWith('-')) {
237
+ const parts = trimmed.split(/\s+/);
238
+ if (parts.length >= 2) {
239
+ functions.push({
240
+ name: parts[0],
241
+ version: parts[1] || 'unknown',
242
+ status: parts[2] || 'unknown'
243
+ });
244
+ }
245
+ }
246
+ }
247
+
248
+ return { functions };
249
+ } catch (error) {
250
+ console.warn('⚠️ Não foi possível obter Edge Functions:', error.message);
251
+ return { functions: [] };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Gera inventário completo
257
+ * @returns {Promise<object>} - Inventário completo
258
+ */
259
+ async generateFullInventory() {
260
+ console.log('🔍 Gerando inventário completo...');
261
+
262
+ const inventory = {
263
+ generated_at: new Date().toISOString(),
264
+ project_id: this.config.supabase.projectId,
265
+ components: {}
266
+ };
267
+
268
+ // Extensões
269
+ if (this.config.backup.includeRealtime) {
270
+ inventory.components.extensions = await this.getExtensions();
271
+ }
272
+
273
+ // Tabelas
274
+ inventory.components.tables = await this.getTables();
275
+
276
+ // Políticas RLS
277
+ inventory.components.policies = await this.getPolicies();
278
+ inventory.components.rlsStatus = await this.getRLSStatus();
279
+
280
+ // Realtime
281
+ if (this.config.backup.includeRealtime) {
282
+ inventory.components.realtime = await this.getRealtimePublications();
283
+ }
284
+
285
+ // Storage
286
+ if (this.config.backup.includeStorage) {
287
+ inventory.components.storage = await this.getStorageInventory();
288
+ }
289
+
290
+ // Edge Functions
291
+ if (this.config.backup.includeFunctions) {
292
+ inventory.components.functions = await this.getEdgeFunctions();
293
+ }
294
+
295
+ return inventory;
296
+ }
297
+ }
298
+
299
+ module.exports = { IntrospectionService };
@@ -0,0 +1,87 @@
1
+ const { spawn } = require('child_process');
2
+ const { promisify } = require('util');
3
+ const exec = promisify(require('child_process').exec);
4
+
5
+ /**
6
+ * Detecta se um binário está disponível no PATH
7
+ * @param {string} name - Nome do binário (ex: 'supabase', 'psql')
8
+ * @returns {Promise<string|null>} - Caminho do binário ou null se não encontrado
9
+ */
10
+ async function ensureBin(name) {
11
+ try {
12
+ const command = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
13
+ const { stdout } = await exec(command);
14
+ const path = stdout.trim().split('\n')[0];
15
+ return path || null;
16
+ } catch (error) {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Executa um comando e retorna resultado estruturado
23
+ * @param {string} cmd - Comando principal
24
+ * @param {string[]} args - Argumentos do comando
25
+ * @param {object} opts - Opções adicionais (cwd, env, etc.)
26
+ * @returns {Promise<{code: number, stdout: string, stderr: string}>}
27
+ */
28
+ async function run(cmd, args = [], opts = {}) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(cmd, args, {
31
+ stdio: ['pipe', 'pipe', 'pipe'],
32
+ ...opts
33
+ });
34
+
35
+ let stdout = '';
36
+ let stderr = '';
37
+
38
+ child.stdout.on('data', (data) => {
39
+ stdout += data.toString();
40
+ });
41
+
42
+ child.stderr.on('data', (data) => {
43
+ stderr += data.toString();
44
+ });
45
+
46
+ child.on('close', (code) => {
47
+ if (code === 0) {
48
+ resolve({ code, stdout, stderr });
49
+ } else {
50
+ const error = new Error(`Comando falhou com código ${code}: ${cmd} ${args.join(' ')}`);
51
+ error.code = code;
52
+ error.stdout = stdout;
53
+ error.stderr = stderr;
54
+ reject(error);
55
+ }
56
+ });
57
+
58
+ child.on('error', (error) => {
59
+ reject(new Error(`Erro ao executar comando: ${error.message}`));
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Executa um comando simples (string completa)
66
+ * @param {string} command - Comando completo
67
+ * @param {object} opts - Opções adicionais
68
+ * @returns {Promise<{code: number, stdout: string, stderr: string}>}
69
+ */
70
+ async function runCommand(command, opts = {}) {
71
+ try {
72
+ const { stdout, stderr } = await exec(command, opts);
73
+ return { code: 0, stdout, stderr };
74
+ } catch (error) {
75
+ return {
76
+ code: error.code || 1,
77
+ stdout: error.stdout || '',
78
+ stderr: error.stderr || error.message
79
+ };
80
+ }
81
+ }
82
+
83
+ module.exports = {
84
+ ensureBin,
85
+ run,
86
+ runCommand
87
+ };
@@ -0,0 +1,140 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * Lê configuração do .smoonbrc
7
+ * @returns {Promise<object>} - Configuração carregada com defaults
8
+ */
9
+ async function readConfig() {
10
+ const configPaths = [
11
+ path.join(process.cwd(), '.smoonbrc'),
12
+ path.join(os.homedir(), '.smoonbrc')
13
+ ];
14
+
15
+ let configContent = null;
16
+ let configPath = null;
17
+
18
+ for (const configPathCandidate of configPaths) {
19
+ try {
20
+ if (fs.existsSync(configPathCandidate)) {
21
+ configContent = fs.readFileSync(configPathCandidate, 'utf8');
22
+ configPath = configPathCandidate;
23
+ break;
24
+ }
25
+ } catch (error) {
26
+ // Continue para próximo caminho
27
+ }
28
+ }
29
+
30
+ if (!configContent) {
31
+ throw new Error('Arquivo .smoonbrc não encontrado. Execute: npx smoonb config --init');
32
+ }
33
+
34
+ let config;
35
+ try {
36
+ config = JSON.parse(configContent);
37
+ } catch (error) {
38
+ throw new Error(`Erro ao parsear .smoonbrc: ${error.message}`);
39
+ }
40
+
41
+ // Aplicar defaults
42
+ const defaultConfig = {
43
+ backup: {
44
+ includeFunctions: true,
45
+ includeStorage: true,
46
+ includeAuth: true,
47
+ includeRealtime: true,
48
+ outputDir: './backups'
49
+ },
50
+ restore: {
51
+ cleanRestore: true,
52
+ verifyAfterRestore: true
53
+ }
54
+ };
55
+
56
+ const mergedConfig = mergeDeep(defaultConfig, config);
57
+
58
+ // Warning para pgDumpPath deprecated
59
+ if (mergedConfig.backup?.pgDumpPath) {
60
+ console.warn('⚠️ backup.pgDumpPath será ignorado na v0.0.8 (usando Supabase CLI).');
61
+ delete mergedConfig.backup.pgDumpPath;
62
+ }
63
+
64
+ return mergedConfig;
65
+ }
66
+
67
+ /**
68
+ * Valida configuração para uma ação específica
69
+ * @param {object} config - Configuração carregada
70
+ * @param {string} action - Ação ('backup', 'restore', 'inventory')
71
+ * @throws {Error} - Se configuração inválida
72
+ */
73
+ function validateFor(config, action) {
74
+ const errors = [];
75
+
76
+ switch (action) {
77
+ case 'backup':
78
+ case 'restore':
79
+ if (!config.supabase?.databaseUrl) {
80
+ errors.push('supabase.databaseUrl é obrigatório');
81
+ }
82
+ break;
83
+
84
+ case 'inventory':
85
+ if (!config.supabase?.url) {
86
+ errors.push('supabase.url é obrigatório');
87
+ }
88
+ if (!config.supabase?.serviceKey) {
89
+ errors.push('supabase.serviceKey é obrigatório');
90
+ }
91
+ break;
92
+ }
93
+
94
+ if (errors.length > 0) {
95
+ throw new Error(`Configuração inválida para ${action}: ${errors.join(', ')}`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Merge profundo de objetos
101
+ * @param {object} target - Objeto destino
102
+ * @param {object} source - Objeto origem
103
+ * @returns {object} - Objeto mesclado
104
+ */
105
+ function mergeDeep(target, source) {
106
+ const result = { ...target };
107
+
108
+ for (const key in source) {
109
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
110
+ result[key] = mergeDeep(target[key] || {}, source[key]);
111
+ } else {
112
+ result[key] = source[key];
113
+ }
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * Salva configuração no .smoonbrc
121
+ * @param {object} config - Configuração para salvar
122
+ * @param {string} targetPath - Caminho de destino (opcional)
123
+ */
124
+ async function saveConfig(config, targetPath = null) {
125
+ const configPath = targetPath || path.join(process.cwd(), '.smoonbrc');
126
+
127
+ // Remover pgDumpPath se existir
128
+ if (config.backup?.pgDumpPath) {
129
+ delete config.backup.pgDumpPath;
130
+ }
131
+
132
+ const jsonContent = JSON.stringify(config, null, 2);
133
+ await fs.promises.writeFile(configPath, jsonContent, 'utf8');
134
+ }
135
+
136
+ module.exports = {
137
+ readConfig,
138
+ validateFor,
139
+ saveConfig
140
+ };
@@ -0,0 +1,110 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Copia um diretório recursivamente
6
+ * @param {string} src - Diretório origem
7
+ * @param {string} dest - Diretório destino
8
+ * @returns {Promise<void>}
9
+ */
10
+ async function copyDir(src, dest) {
11
+ try {
12
+ // Node.js 18+ tem fs.promises.cp
13
+ if (fs.promises.cp) {
14
+ await fs.promises.cp(src, dest, { recursive: true });
15
+ return;
16
+ }
17
+ } catch (error) {
18
+ // Fallback para fs-extra se disponível
19
+ try {
20
+ const fse = require('fs-extra');
21
+ await fse.copy(src, dest);
22
+ return;
23
+ } catch (fseError) {
24
+ // Fallback manual usando fs nativo
25
+ }
26
+ }
27
+
28
+ // Fallback manual usando fs nativo
29
+ await copyDirManual(src, dest);
30
+ }
31
+
32
+ /**
33
+ * Implementação manual de cópia de diretório
34
+ * @param {string} src - Diretório origem
35
+ * @param {string} dest - Diretório destino
36
+ */
37
+ async function copyDirManual(src, dest) {
38
+ const stat = await fs.promises.stat(src);
39
+
40
+ if (stat.isDirectory()) {
41
+ await fs.promises.mkdir(dest, { recursive: true });
42
+ const entries = await fs.promises.readdir(src);
43
+
44
+ for (const entry of entries) {
45
+ const srcPath = path.join(src, entry);
46
+ const destPath = path.join(dest, entry);
47
+ await copyDirManual(srcPath, destPath);
48
+ }
49
+ } else {
50
+ await fs.promises.copyFile(src, dest);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Cria diretório se não existir
56
+ * @param {string} dirPath - Caminho do diretório
57
+ * @returns {Promise<void>}
58
+ */
59
+ async function ensureDir(dirPath) {
60
+ try {
61
+ await fs.promises.mkdir(dirPath, { recursive: true });
62
+ } catch (error) {
63
+ if (error.code !== 'EEXIST') {
64
+ throw error;
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Verifica se um arquivo existe
71
+ * @param {string} filePath - Caminho do arquivo
72
+ * @returns {Promise<boolean>}
73
+ */
74
+ async function exists(filePath) {
75
+ try {
76
+ await fs.promises.access(filePath);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Escreve JSON de forma segura
85
+ * @param {string} filePath - Caminho do arquivo
86
+ * @param {any} data - Dados para escrever
87
+ * @param {number} spaces - Espaços para indentação (padrão: 2)
88
+ */
89
+ async function writeJson(filePath, data, spaces = 2) {
90
+ const json = JSON.stringify(data, null, spaces);
91
+ await fs.promises.writeFile(filePath, json, 'utf8');
92
+ }
93
+
94
+ /**
95
+ * Lê JSON de forma segura
96
+ * @param {string} filePath - Caminho do arquivo
97
+ * @returns {Promise<any>}
98
+ */
99
+ async function readJson(filePath) {
100
+ const content = await fs.promises.readFile(filePath, 'utf8');
101
+ return JSON.parse(content);
102
+ }
103
+
104
+ module.exports = {
105
+ copyDir,
106
+ ensureDir,
107
+ exists,
108
+ writeJson,
109
+ readJson
110
+ };
@@ -0,0 +1,40 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * Calcula SHA256 de um arquivo usando stream
6
+ * @param {string} filePath - Caminho do arquivo
7
+ * @returns {Promise<string>} - Hash SHA256 em hexadecimal
8
+ */
9
+ async function sha256(filePath) {
10
+ return new Promise((resolve, reject) => {
11
+ const hash = crypto.createHash('sha256');
12
+ const stream = fs.createReadStream(filePath);
13
+
14
+ stream.on('data', (data) => {
15
+ hash.update(data);
16
+ });
17
+
18
+ stream.on('end', () => {
19
+ resolve(hash.digest('hex'));
20
+ });
21
+
22
+ stream.on('error', (error) => {
23
+ reject(error);
24
+ });
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Calcula SHA256 de uma string
30
+ * @param {string} data - String para calcular hash
31
+ * @returns {string} - Hash SHA256 em hexadecimal
32
+ */
33
+ function sha256String(data) {
34
+ return crypto.createHash('sha256').update(data).digest('hex');
35
+ }
36
+
37
+ module.exports = {
38
+ sha256,
39
+ sha256String
40
+ };