@zenifra/cli 0.3.0 → 0.3.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/README.md +13 -1
- package/bin/lib/public-ux.mjs +40 -0
- package/bin/zenifra.mjs +44 -5
- 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
|
},
|
|
@@ -1811,6 +1813,18 @@ function envsForOutput(envs, { showValues = false } = {}) {
|
|
|
1811
1813
|
}));
|
|
1812
1814
|
}
|
|
1813
1815
|
|
|
1816
|
+
const PROJECT_INFO_SECRET_FIELD = /(?:^|_)(?:api_?key|access_?token|refresh_?token|secret|password|credential|connection_string|private_?jwk)(?:$|_)/i;
|
|
1817
|
+
|
|
1818
|
+
function projectInfoForOutput(value, key = '') {
|
|
1819
|
+
if (key === 'envs' && Array.isArray(value)) return envsForOutput(value);
|
|
1820
|
+
if (Array.isArray(value)) return value.map((item) => projectInfoForOutput(item, key));
|
|
1821
|
+
if (!value || typeof value !== 'object') return PROJECT_INFO_SECRET_FIELD.test(key) ? maskEnvValue(value) : value;
|
|
1822
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1823
|
+
entryKey,
|
|
1824
|
+
PROJECT_INFO_SECRET_FIELD.test(entryKey) ? maskEnvValue(entryValue) : projectInfoForOutput(entryValue, entryKey),
|
|
1825
|
+
]));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1814
1828
|
function formatPublicUrl(value) {
|
|
1815
1829
|
const raw = String(value || '').trim();
|
|
1816
1830
|
if (!raw) return '';
|
|
@@ -3491,6 +3505,9 @@ async function handleProjectCreate(session, flags) {
|
|
|
3491
3505
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
3492
3506
|
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
3493
3507
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
3508
|
+
if (Array.isArray(config?.custom_domains)) {
|
|
3509
|
+
config.custom_domains = normalizeCustomDomains(config.custom_domains, { primaryDomain: config.domain });
|
|
3510
|
+
}
|
|
3494
3511
|
const typeProject = normalizeTypeProject(config?.type_project);
|
|
3495
3512
|
const valkeyCatalog = typeProject === 'valkey'
|
|
3496
3513
|
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
@@ -3554,7 +3571,7 @@ async function handleProjectInfo(session, flags) {
|
|
|
3554
3571
|
const orgId = await resolveOrgId(session, flags);
|
|
3555
3572
|
const project = await getProject(session, flags, projectId, orgId);
|
|
3556
3573
|
|
|
3557
|
-
if (flags.json) return printJson(project);
|
|
3574
|
+
if (flags.json) return printJson(projectInfoForOutput(project));
|
|
3558
3575
|
printProject({ ...project, id: projectId });
|
|
3559
3576
|
}
|
|
3560
3577
|
|
|
@@ -4227,6 +4244,17 @@ async function handleValkeyCredentialStatus(session, flags) {
|
|
|
4227
4244
|
`/managed-services/${projectId}/credential-rotations/${flags.operation}`,
|
|
4228
4245
|
{ orgId },
|
|
4229
4246
|
));
|
|
4247
|
+
if (flags.connectionFile) {
|
|
4248
|
+
await writeValkeyConnectionFile(String(flags.connectionFile), data.connection_string);
|
|
4249
|
+
if (flags.json) return printJson({
|
|
4250
|
+
operation_id: data.operation_id,
|
|
4251
|
+
state: data.state,
|
|
4252
|
+
connection_saved: true,
|
|
4253
|
+
connection_file: String(flags.connectionFile),
|
|
4254
|
+
});
|
|
4255
|
+
process.stdout.write(`Credencial concluida e salva em ${flags.connectionFile}.\n`);
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4230
4258
|
if (flags.json) return printJson(data);
|
|
4231
4259
|
printValkeyRotation(data);
|
|
4232
4260
|
}
|
|
@@ -4249,6 +4277,17 @@ async function handleValkeyCredentialRotate(session, flags) {
|
|
|
4249
4277
|
|
|
4250
4278
|
if (flags.wait) {
|
|
4251
4279
|
const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
|
|
4280
|
+
if (flags.connectionFile) {
|
|
4281
|
+
await writeValkeyConnectionFile(String(flags.connectionFile), completed.connection_string);
|
|
4282
|
+
if (flags.json) return printJson({
|
|
4283
|
+
operation_id: completed.operation_id,
|
|
4284
|
+
state: completed.state,
|
|
4285
|
+
connection_saved: true,
|
|
4286
|
+
connection_file: String(flags.connectionFile),
|
|
4287
|
+
});
|
|
4288
|
+
process.stdout.write(`Credencial concluida e salva em ${flags.connectionFile}.\n`);
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4252
4291
|
if (flags.json) return printJson(completed);
|
|
4253
4292
|
printValkeyRotation(completed);
|
|
4254
4293
|
return;
|