@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 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; a credencial completa aparece apenas na criação ou em uma rotação concluída
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",