smoonb 0.0.1
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/.smoonbrc.example +18 -0
- package/LICENSE.md +64 -0
- package/README.md +227 -0
- package/bin/smoonb.js +113 -0
- package/package.json +43 -0
- package/src/commands/backup.js +245 -0
- package/src/commands/check.js +405 -0
- package/src/commands/config.js +75 -0
- package/src/commands/functions.js +375 -0
- package/src/commands/restore.js +326 -0
- package/src/commands/secrets.js +361 -0
- package/src/index.js +269 -0
- package/src/utils/supabase.js +364 -0
- package/src/utils/validation.js +351 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilitários de validação para inputs e configurações
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validar Project ID do Supabase
|
|
9
|
+
*/
|
|
10
|
+
function validateProjectId(projectId) {
|
|
11
|
+
if (!projectId) {
|
|
12
|
+
return { valid: false, error: 'Project ID é obrigatório' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Project ID deve ter formato específico (ex: abc123def456)
|
|
16
|
+
const projectIdRegex = /^[a-z0-9]{20}$/;
|
|
17
|
+
if (!projectIdRegex.test(projectId)) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: 'Project ID deve ter 20 caracteres alfanuméricos (ex: abc123def456)'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { valid: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validar URL do Supabase
|
|
29
|
+
*/
|
|
30
|
+
function validateSupabaseUrl(url) {
|
|
31
|
+
if (!url) {
|
|
32
|
+
return { valid: false, error: 'URL do Supabase é obrigatória' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const parsedUrl = new URL(url);
|
|
37
|
+
|
|
38
|
+
// Deve ser HTTPS
|
|
39
|
+
if (parsedUrl.protocol !== 'https:') {
|
|
40
|
+
return { valid: false, error: 'URL deve usar HTTPS' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Deve ter formato correto
|
|
44
|
+
if (!parsedUrl.hostname.includes('.supabase.co')) {
|
|
45
|
+
return { valid: false, error: 'URL deve ser um domínio Supabase válido' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { valid: true };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { valid: false, error: 'URL inválida' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validar Service Key do Supabase
|
|
56
|
+
*/
|
|
57
|
+
function validateServiceKey(serviceKey) {
|
|
58
|
+
if (!serviceKey) {
|
|
59
|
+
return { valid: false, error: 'Service Key é obrigatória' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Service Key deve ter formato específico
|
|
63
|
+
const serviceKeyRegex = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
64
|
+
if (!serviceKeyRegex.test(serviceKey)) {
|
|
65
|
+
return { valid: false, error: 'Service Key deve ser um JWT válido' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { valid: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validar Anon Key do Supabase
|
|
73
|
+
*/
|
|
74
|
+
function validateAnonKey(anonKey) {
|
|
75
|
+
if (!anonKey) {
|
|
76
|
+
return { valid: false, error: 'Anon Key é obrigatória' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Anon Key deve ter formato específico
|
|
80
|
+
const anonKeyRegex = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
81
|
+
if (!anonKeyRegex.test(anonKey)) {
|
|
82
|
+
return { valid: false, error: 'Anon Key deve ser um JWT válido' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validar URL de conexão da database
|
|
90
|
+
*/
|
|
91
|
+
function validateDatabaseUrl(dbUrl) {
|
|
92
|
+
if (!dbUrl) {
|
|
93
|
+
return { valid: false, error: 'URL da database é obrigatória' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const parsedUrl = new URL(dbUrl);
|
|
98
|
+
|
|
99
|
+
// Deve ser PostgreSQL
|
|
100
|
+
if (parsedUrl.protocol !== 'postgresql:') {
|
|
101
|
+
return { valid: false, error: 'URL deve usar protocolo postgresql:' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Deve ter host, port e database
|
|
105
|
+
if (!parsedUrl.hostname || !parsedUrl.port || !parsedUrl.pathname.slice(1)) {
|
|
106
|
+
return { valid: false, error: 'URL deve incluir host, porta e nome da database' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: true };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { valid: false, error: 'URL da database inválida' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validar diretório de backup
|
|
117
|
+
*/
|
|
118
|
+
function validateBackupDir(backupDir) {
|
|
119
|
+
if (!backupDir) {
|
|
120
|
+
return { valid: false, error: 'Diretório de backup é obrigatório' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Deve ser um caminho válido
|
|
124
|
+
if (typeof backupDir !== 'string' || backupDir.trim().length === 0) {
|
|
125
|
+
return { valid: false, error: 'Diretório de backup deve ser uma string válida' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Não deve conter caracteres perigosos
|
|
129
|
+
const dangerousChars = /[<>:"|?*\x00-\x1f]/;
|
|
130
|
+
if (dangerousChars.test(backupDir)) {
|
|
131
|
+
return { valid: false, error: 'Diretório contém caracteres inválidos' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { valid: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validar configuração completa
|
|
139
|
+
*/
|
|
140
|
+
function validateConfig(config) {
|
|
141
|
+
const errors = [];
|
|
142
|
+
|
|
143
|
+
// Validar Supabase URL
|
|
144
|
+
if (config.supabase?.url) {
|
|
145
|
+
const urlValidation = validateSupabaseUrl(config.supabase.url);
|
|
146
|
+
if (!urlValidation.valid) {
|
|
147
|
+
errors.push(`Supabase URL: ${urlValidation.error}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validar Service Key
|
|
152
|
+
if (config.supabase?.serviceKey) {
|
|
153
|
+
const serviceKeyValidation = validateServiceKey(config.supabase.serviceKey);
|
|
154
|
+
if (!serviceKeyValidation.valid) {
|
|
155
|
+
errors.push(`Service Key: ${serviceKeyValidation.error}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validar Anon Key
|
|
160
|
+
if (config.supabase?.anonKey) {
|
|
161
|
+
const anonKeyValidation = validateAnonKey(config.supabase.anonKey);
|
|
162
|
+
if (!anonKeyValidation.valid) {
|
|
163
|
+
errors.push(`Anon Key: ${anonKeyValidation.error}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validar Database URL
|
|
168
|
+
if (config.supabase?.databaseUrl) {
|
|
169
|
+
const dbUrlValidation = validateDatabaseUrl(config.supabase.databaseUrl);
|
|
170
|
+
if (!dbUrlValidation.valid) {
|
|
171
|
+
errors.push(`Database URL: ${dbUrlValidation.error}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
valid: errors.length === 0,
|
|
177
|
+
errors
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validar opções de backup
|
|
183
|
+
*/
|
|
184
|
+
function validateBackupOptions(options) {
|
|
185
|
+
const errors = [];
|
|
186
|
+
|
|
187
|
+
// Project ID é obrigatório
|
|
188
|
+
const projectIdValidation = validateProjectId(options.projectId);
|
|
189
|
+
if (!projectIdValidation.valid) {
|
|
190
|
+
errors.push(projectIdValidation.error);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Output dir é obrigatório
|
|
194
|
+
const outputDirValidation = validateBackupDir(options.output);
|
|
195
|
+
if (!outputDirValidation.valid) {
|
|
196
|
+
errors.push(outputDirValidation.error);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
valid: errors.length === 0,
|
|
201
|
+
errors
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Validar opções de restore
|
|
207
|
+
*/
|
|
208
|
+
function validateRestoreOptions(options) {
|
|
209
|
+
const errors = [];
|
|
210
|
+
|
|
211
|
+
// Project ID é obrigatório
|
|
212
|
+
const projectIdValidation = validateProjectId(options.projectId);
|
|
213
|
+
if (!projectIdValidation.valid) {
|
|
214
|
+
errors.push(projectIdValidation.error);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Backup dir é obrigatório
|
|
218
|
+
const backupDirValidation = validateBackupDir(options.backupDir);
|
|
219
|
+
if (!backupDirValidation.valid) {
|
|
220
|
+
errors.push(backupDirValidation.error);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
valid: errors.length === 0,
|
|
225
|
+
errors
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validar arquivo de secrets
|
|
231
|
+
*/
|
|
232
|
+
function validateSecretsFile(filePath) {
|
|
233
|
+
if (!filePath) {
|
|
234
|
+
return { valid: false, error: 'Caminho do arquivo de secrets é obrigatório' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Deve ser um arquivo .env
|
|
238
|
+
if (!filePath.endsWith('.env')) {
|
|
239
|
+
return { valid: false, error: 'Arquivo deve ter extensão .env' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { valid: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validar nome de Edge Function
|
|
247
|
+
*/
|
|
248
|
+
function validateFunctionName(functionName) {
|
|
249
|
+
if (!functionName) {
|
|
250
|
+
return { valid: false, error: 'Nome da function é obrigatório' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Deve seguir convenções de nomenclatura
|
|
254
|
+
const functionNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
255
|
+
if (!functionNameRegex.test(functionName)) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
error: 'Nome da function deve conter apenas letras minúsculas, números e hífens'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Não deve ser muito longo
|
|
263
|
+
if (functionName.length > 50) {
|
|
264
|
+
return { valid: false, error: 'Nome da function não pode ter mais de 50 caracteres' };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { valid: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validar arquivo de manifesto de backup
|
|
272
|
+
*/
|
|
273
|
+
function validateBackupManifest(manifest) {
|
|
274
|
+
const errors = [];
|
|
275
|
+
|
|
276
|
+
if (!manifest) {
|
|
277
|
+
return { valid: false, errors: ['Manifesto é obrigatório'] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Deve ter timestamp
|
|
281
|
+
if (!manifest.timestamp) {
|
|
282
|
+
errors.push('Timestamp é obrigatório');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Deve ter projectId
|
|
286
|
+
if (!manifest.projectId) {
|
|
287
|
+
errors.push('Project ID é obrigatório');
|
|
288
|
+
} else {
|
|
289
|
+
const projectIdValidation = validateProjectId(manifest.projectId);
|
|
290
|
+
if (!projectIdValidation.valid) {
|
|
291
|
+
errors.push(`Project ID: ${projectIdValidation.error}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Deve ter version
|
|
296
|
+
if (!manifest.version) {
|
|
297
|
+
errors.push('Versão é obrigatória');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Deve ter components
|
|
301
|
+
if (!manifest.components) {
|
|
302
|
+
errors.push('Componentes são obrigatórios');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
valid: errors.length === 0,
|
|
307
|
+
errors
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Mostrar erros de validação formatados
|
|
313
|
+
*/
|
|
314
|
+
function showValidationErrors(errors, context = 'Validação') {
|
|
315
|
+
if (errors.length === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.error(chalk.red.bold(`❌ ${context} falhou:`));
|
|
320
|
+
errors.forEach(error => {
|
|
321
|
+
console.error(chalk.red(` - ${error}`));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Validar e mostrar resultado
|
|
327
|
+
*/
|
|
328
|
+
function validateAndShow(validationResult, context = 'Validação') {
|
|
329
|
+
if (!validationResult.valid) {
|
|
330
|
+
showValidationErrors(validationResult.errors, context);
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = {
|
|
337
|
+
validateProjectId,
|
|
338
|
+
validateSupabaseUrl,
|
|
339
|
+
validateServiceKey,
|
|
340
|
+
validateAnonKey,
|
|
341
|
+
validateDatabaseUrl,
|
|
342
|
+
validateBackupDir,
|
|
343
|
+
validateConfig,
|
|
344
|
+
validateBackupOptions,
|
|
345
|
+
validateRestoreOptions,
|
|
346
|
+
validateSecretsFile,
|
|
347
|
+
validateFunctionName,
|
|
348
|
+
validateBackupManifest,
|
|
349
|
+
showValidationErrors,
|
|
350
|
+
validateAndShow
|
|
351
|
+
};
|