@zenifra/cli 0.3.0 → 0.3.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/README.md +13 -1
- package/bin/lib/public-ux.mjs +40 -0
- package/bin/zenifra.mjs +31 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,10 @@ Fluxos:
|
|
|
148
148
|
- `zenifra deploy`: dispara o build/deploy GitHub e retorna o `build_id`
|
|
149
149
|
- `zenifra deploy watch`: usa esse `build_id` para acompanhar o build em tempo real e imprimir os logs incrementais ate o fim
|
|
150
150
|
|
|
151
|
+
Antes de uma mutacao, confirme o perfil, a API e a organizacao ativa. Para criacoes, confirme tambem o catalogo, plano, pagamento, exposicao, dominio, origem do deploy, porta, instancias e ambientes. Depois, leia o projeto de volta e acompanhe o build/deployment ate um estado terminal.
|
|
152
|
+
|
|
153
|
+
O dominio principal e um dominio personalizado sao campos diferentes. Nao repita o dominio principal em `config.custom_domains`; depois de adicionar um dominio personalizado, valide DNS/TLS e leia a URL final antes de considerar a operacao concluida.
|
|
154
|
+
|
|
151
155
|
Se voce rodar apenas `zenifra deploy`, a CLI mostra a ajuda especifica do comando com uso, flags, exemplos e exemplo de retorno.
|
|
152
156
|
|
|
153
157
|
## Login pelo navegador (OAuth)
|
|
@@ -317,9 +321,17 @@ Observacoes do wizard:
|
|
|
317
321
|
- em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
|
|
318
322
|
- em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
|
|
319
323
|
- em projetos Valkey, a capacidade é definida pelo plano e a CLI não pergunta instâncias, imagem, variáveis de ambiente ou exposição HTTP
|
|
320
|
-
- a conexão mascarada pode ser consultada a qualquer momento;
|
|
324
|
+
- a conexão mascarada pode ser consultada a qualquer momento; uma rotação concluída pode salvar a conexão utilizável em arquivo privado com `--connection-file <path>`
|
|
321
325
|
- `valkey credentials rotate` retorna uma operação assíncrona; use `--wait` ou `valkey credentials status` para acompanhar
|
|
322
326
|
|
|
327
|
+
Exemplo de entrega segura para automação local:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
zenifra valkey credentials rotate --project <project-id> --wait --connection-file /caminho/privado/redis-url.txt --json
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
O arquivo é criado com permissão privada; a conexão não aparece na saída do comando quando essa opção é usada.
|
|
334
|
+
|
|
323
335
|
## Regressao manual de auto-scaling em staging
|
|
324
336
|
|
|
325
337
|
O teste de staging cria projetos, gera trafego, consulta consumo e remove somente os projetos criados pela propria execucao. Ele exige uma API de teste explicita, rejeita a API de producao e exige habilitacao explicita das mutacoes. O exemplo abaixo usa uma API local de teste.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { atomicPrivateWrite } from './profile-store.mjs';
|
|
2
|
+
|
|
3
|
+
const MASKED_SECRET_PATTERN = /\*{4,}|\[redacted\]|<redacted>/i;
|
|
4
|
+
|
|
5
|
+
function normalizeDomain(value) {
|
|
6
|
+
return String(value || '').trim().replace(/\.+$/, '').toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeCustomDomains(domains, { primaryDomain } = {}) {
|
|
10
|
+
if (!Array.isArray(domains)) return [];
|
|
11
|
+
const normalized = domains.map(normalizeDomain).filter(Boolean);
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const domain of normalized) {
|
|
14
|
+
if (seen.has(domain)) throw new Error(`Duplicate custom domain: ${domain}`);
|
|
15
|
+
seen.add(domain);
|
|
16
|
+
}
|
|
17
|
+
const primary = normalizeDomain(primaryDomain);
|
|
18
|
+
if (primary && seen.has(primary)) {
|
|
19
|
+
throw new Error(`Custom domain conflicts with the primary domain: ${primary}`);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateValkeyConnection(value) {
|
|
25
|
+
const connection = String(value ?? '');
|
|
26
|
+
if (!connection || connection.trim() === '' || MASKED_SECRET_PATTERN.test(connection)) {
|
|
27
|
+
throw new Error('The Valkey connection is masked. Rotate credentials and save the one-time connection securely.');
|
|
28
|
+
}
|
|
29
|
+
return connection;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeValkeyConnectionFile(path, value) {
|
|
33
|
+
const connection = validateValkeyConnection(value);
|
|
34
|
+
await atomicPrivateWrite(path, connection);
|
|
35
|
+
return connection;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isMaskedSecret(value) {
|
|
39
|
+
return MASKED_SECRET_PATTERN.test(String(value || ''));
|
|
40
|
+
}
|
package/bin/zenifra.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import process from 'node:process';
|
|
|
9
9
|
import readline from 'node:readline/promises';
|
|
10
10
|
import { oauthLogin, openBrowser, validateOAuthProfile, refreshOAuth, revokeOAuth } from './lib/oauth.mjs';
|
|
11
11
|
import { withProfileLock, atomicPrivateWrite } from './lib/profile-store.mjs';
|
|
12
|
+
import { normalizeCustomDomains, writeValkeyConnectionFile } from './lib/public-ux.mjs';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_API_BASE_URL = 'https://api.zenifra.com/v1';
|
|
14
15
|
const DOCS_BASE_URL = 'https://docs.zenifra.com/pt';
|
|
@@ -35,6 +36,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
35
36
|
'build',
|
|
36
37
|
'challengeToken',
|
|
37
38
|
'code',
|
|
39
|
+
'connectionFile',
|
|
38
40
|
'commitSha',
|
|
39
41
|
'config',
|
|
40
42
|
'count',
|
|
@@ -394,18 +396,18 @@ const HELP_SPECS = [
|
|
|
394
396
|
},
|
|
395
397
|
{
|
|
396
398
|
command: 'valkey credentials rotate',
|
|
397
|
-
usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
399
|
+
usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--connection-file <path>] [--json]',
|
|
398
400
|
description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
|
|
399
|
-
flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime a resposta em JSON.'],
|
|
401
|
+
flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--connection-file <path> Salva a conexao concluida em arquivo privado.', '--json Imprime a resposta em JSON.'],
|
|
400
402
|
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
|
|
401
403
|
output: 'Operacao aceita: rotation_123\nEstado: accepted',
|
|
402
404
|
notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
|
|
403
405
|
},
|
|
404
406
|
{
|
|
405
407
|
command: 'valkey credentials status',
|
|
406
|
-
usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
408
|
+
usage: 'zenifra valkey credentials status --project <id> --operation <id> [--connection-file <path>] [--json]',
|
|
407
409
|
description: 'Consulta o estado de uma renovacao de credencial Valkey.',
|
|
408
|
-
flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
|
|
410
|
+
flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--connection-file <path> Salva a conexao concluida em arquivo privado.', '--json Imprime a resposta em JSON.'],
|
|
409
411
|
examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
|
|
410
412
|
output: 'Operacao: rotation_123\nEstado: completed',
|
|
411
413
|
},
|
|
@@ -3491,6 +3493,9 @@ async function handleProjectCreate(session, flags) {
|
|
|
3491
3493
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
3492
3494
|
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
3493
3495
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
3496
|
+
if (Array.isArray(config?.custom_domains)) {
|
|
3497
|
+
config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
|
|
3498
|
+
}
|
|
3494
3499
|
const typeProject = normalizeTypeProject(config?.type_project);
|
|
3495
3500
|
const valkeyCatalog = typeProject === 'valkey'
|
|
3496
3501
|
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
@@ -4227,6 +4232,17 @@ async function handleValkeyCredentialStatus(session, flags) {
|
|
|
4227
4232
|
`/managed-services/${projectId}/credential-rotations/${flags.operation}`,
|
|
4228
4233
|
{ orgId },
|
|
4229
4234
|
));
|
|
4235
|
+
if (flags.connectionFile) {
|
|
4236
|
+
await writeValkeyConnectionFile(String(flags.connectionFile), data.connection_string);
|
|
4237
|
+
if (flags.json) return printJson({
|
|
4238
|
+
operation_id: data.operation_id,
|
|
4239
|
+
state: data.state,
|
|
4240
|
+
connection_saved: true,
|
|
4241
|
+
connection_file: String(flags.connectionFile),
|
|
4242
|
+
});
|
|
4243
|
+
process.stdout.write(`Credencial concluida e salva em ${flags.connectionFile}.\n`);
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4230
4246
|
if (flags.json) return printJson(data);
|
|
4231
4247
|
printValkeyRotation(data);
|
|
4232
4248
|
}
|
|
@@ -4249,6 +4265,17 @@ async function handleValkeyCredentialRotate(session, flags) {
|
|
|
4249
4265
|
|
|
4250
4266
|
if (flags.wait) {
|
|
4251
4267
|
const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
|
|
4268
|
+
if (flags.connectionFile) {
|
|
4269
|
+
await writeValkeyConnectionFile(String(flags.connectionFile), completed.connection_string);
|
|
4270
|
+
if (flags.json) return printJson({
|
|
4271
|
+
operation_id: completed.operation_id,
|
|
4272
|
+
state: completed.state,
|
|
4273
|
+
connection_saved: true,
|
|
4274
|
+
connection_file: String(flags.connectionFile),
|
|
4275
|
+
});
|
|
4276
|
+
process.stdout.write(`Credencial concluida e salva em ${flags.connectionFile}.\n`);
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4252
4279
|
if (flags.json) return printJson(completed);
|
|
4253
4280
|
printValkeyRotation(completed);
|
|
4254
4281
|
return;
|