auroq-os 2.8.0 → 2.8.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.
@@ -0,0 +1,1271 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auroq OS — Conexao Infisical (cofre gratis, rota B)
5
+ *
6
+ * Para o aluno que nao quer pagar o 1Password. O aluno so cria a conta no Infisical (gratis) e entra pelo
7
+ * navegador (`infisical login`, que abre sozinho). O resto e daqui: garante o CLI do Infisical, cria o
8
+ * projeto "Auroq", cria uma identidade de maquina do PROJETO (so enxerga esse projeto), com Universal Auth e
9
+ * uma chave sem validade, grava a chave neste computador e testa num terminal novo (grava, le e apaga um
10
+ * segredo de teste). A sessao do aluno (que expira em dias) so existe durante a configuracao; depois quem
11
+ * trabalha e a identidade, que renova o acesso sozinha a cada uso.
12
+ *
13
+ * O client secret nasce e e gravado dentro deste processo: nunca e impresso nem passa por argumento de linha
14
+ * de comando. Depois de gravado ele e uma variavel de ambiente do aluno (mesmo arquivo de ambiente da rota A):
15
+ * por isso o Ops nunca roda `env` nem abre esse arquivo.
16
+ *
17
+ * @module auroq-os/infisical
18
+ */
19
+
20
+ const os = require('os');
21
+ const path = require('path');
22
+ const fs = require('fs-extra');
23
+ const { execSync, execFileSync, spawnSync } = require('child_process');
24
+ const op = require('./onepassword');
25
+ const apiLib = require('./infisical-api');
26
+ const cliLib = require('./infisical-cli');
27
+
28
+ const PURPLE = '\x1b[38;2;120;80;200m';
29
+ const GOLD = '\x1b[38;2;245;158;11m';
30
+ const CYAN = '\x1b[38;2;34;211;238m';
31
+ const GREEN = '\x1b[38;2;52;211;153m';
32
+ const DIM = '\x1b[2m';
33
+ const BOLD = '\x1b[1m';
34
+ const RESET = '\x1b[0m';
35
+
36
+ function log(msg) { console.log(` ${msg}`); }
37
+ function step(msg) { console.log(`\n ${CYAN}▸${RESET} ${msg}`); }
38
+ function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
39
+ function warn(msg) { console.log(` ${GOLD}!${RESET} ${msg}`); }
40
+ function err(msg) { console.log(` ${PURPLE}✗${RESET} ${msg}`); }
41
+
42
+ const CLIENT_ID_VAR = 'AUROQ_INFISICAL_CLIENT_ID';
43
+ const CLIENT_SECRET_VAR = 'AUROQ_INFISICAL_CLIENT_SECRET';
44
+ const ENV_COMMENT = '# Infisical - identidade do agente (gravado pelo Auroq OS)';
45
+ const PROJECT_NAME = 'Auroq';
46
+ const PROJECT_SLUG = 'auroq';
47
+ const IDENTITY_NAME = 'Auroq';
48
+ const ENV_SLUG = 'prod';
49
+ const TEST_SECRET = 'AUROQ_TESTE';
50
+ const KEY_PREFIX = 'Auroq OS - ';
51
+ const MAX_WAIT_SECONDS = 570; // o CLI espera o navegador por 10 min; a ferramenta do agente corta em 10 min
52
+ const DEFAULT_WAIT_SECONDS = 540;
53
+ const MIN_WAIT_SECONDS = 5; // piso: tempo 0 no runProcess significa "sem limite", e --esperar 0 so quer a URL na hora
54
+ const LOGIN_RETRY_WAITS_MS = [20000, 35000]; // nunca 3 falhas em 30 s: a identidade trava por 5 min
55
+ const LOGIN_LOCK_WINDOW_MS = 30000; // o contador de falhas de login da identidade zera 30 s depois da ULTIMA falha
56
+ const EXIT_CODE = { ok: 0, aguardando: 0, erro: 1 };
57
+
58
+ // ─── Etapas ──────────────────────────────────────────────
59
+
60
+ const ETAPAS = ['conta', 'cli', 'login', 'projeto', 'identidade', 'teste', 'pronto'];
61
+ const ETAPA_NOME = {
62
+ conta: 'conta no Infisical (entrou pelo navegador)',
63
+ cli: 'ferramenta infisical instalada',
64
+ login: 'login no navegador',
65
+ projeto: 'projeto Auroq',
66
+ identidade: 'chave do agente (identidade) criada e guardada',
67
+ teste: 'teste em terminal novo (grava, le e apaga)',
68
+ pronto: 'pronto',
69
+ };
70
+
71
+ function emptyEtapas() {
72
+ return Object.fromEntries(ETAPAS.map((e) => [e, 'pendente']));
73
+ }
74
+
75
+ function stateFile() {
76
+ return path.join(op.auroqDir(), 'infisical-etapas.json');
77
+ }
78
+
79
+ function configFile() {
80
+ return path.join(op.auroqDir(), 'infisical.json');
81
+ }
82
+
83
+ /** Estado salvo (ou null). Nunca lanca: arquivo ausente ou estragado = comeca do zero. */
84
+ function readState() {
85
+ try {
86
+ const data = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
87
+ if (!data || typeof data !== 'object' || data.versao !== 1 || !data.etapas) return null;
88
+ const etapas = emptyEtapas();
89
+ for (const e of ETAPAS) if (data.etapas[e] === 'feito') etapas[e] = 'feito';
90
+ return { versao: 1, etapas, ultimo: data.ultimo || null, atualizado_em: data.atualizado_em || null };
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function writeState(state) {
97
+ try {
98
+ op.writeFileAtomic(stateFile(), `${JSON.stringify(state, null, 2)}\n`);
99
+ } catch {
100
+ /* o estado e conforto, nunca motivo para falhar a conexao */
101
+ }
102
+ }
103
+
104
+ /** Config sem segredo (dominio, projeto, ambiente, identidade, caminho do CLI). */
105
+ function readConfig() {
106
+ try {
107
+ const data = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
108
+ if (!data || typeof data !== 'object' || data.versao !== 1) return null;
109
+ return data;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function writeConfig(cfg) {
116
+ const merged = { ...(readConfig() || {}), ...cfg, versao: 1 };
117
+ op.writeFileAtomic(configFile(), `${JSON.stringify(merged, null, 2)}\n`);
118
+ return merged;
119
+ }
120
+
121
+ function nextStage(etapas) {
122
+ return ETAPAS.find((e) => etapas[e] !== 'feito') || null;
123
+ }
124
+
125
+ function makeProgress(d) {
126
+ const prev = d.state.read();
127
+ const etapas = prev ? { ...prev.etapas } : emptyEtapas();
128
+ return {
129
+ etapas,
130
+ mark(...stages) { for (const e of stages) etapas[e] = 'feito'; },
131
+ reset(...stages) { for (const e of stages) etapas[e] = 'pendente'; },
132
+ all() { this.mark(...ETAPAS); },
133
+ save(result) {
134
+ d.state.write({
135
+ versao: 1,
136
+ atualizado_em: new Date(d.now()).toISOString(),
137
+ etapas: { ...etapas },
138
+ proxima: nextStage(etapas),
139
+ ultimo: result ? { status: result.status, codigo: result.codigo } : null,
140
+ });
141
+ },
142
+ };
143
+ }
144
+
145
+ // ─── Textos e helpers puros ──────────────────────────────
146
+
147
+ function fim(status, codigo, mensagem, extra = {}) {
148
+ return { status, codigo, mensagem, ...extra };
149
+ }
150
+
151
+ /** Uma linha so, sem JWT nem client secret. */
152
+ function clean(text, max = 300) {
153
+ // corta ANTES do redact: as regex dele custam N^2 em texto longo, e o que passa de 4000 caracteres nunca apareceria
154
+ const bounded = String(text === undefined || text === null ? '' : text).slice(0, 4000);
155
+ return apiLib.redact(op.oneLine(apiLib.redact(bounded), max));
156
+ }
157
+
158
+ function parseArgs(argv) {
159
+ const args = Array.isArray(argv) ? argv : [];
160
+ const opts = { rotacionar: false, agente: false, estado: false, regiao: null, dominio: null, esperar: null };
161
+ for (let i = 0; i < args.length; i++) {
162
+ const a = String(args[i]);
163
+ if (a === '--rotacionar') opts.rotacionar = true;
164
+ else if (a === '--agente') opts.agente = true;
165
+ else if (a === '--estado') opts.estado = true;
166
+ else if (a === '--regiao' && args[i + 1] && !String(args[i + 1]).startsWith('--')) opts.regiao = String(args[++i]).toLowerCase();
167
+ else if (a.startsWith('--regiao=')) opts.regiao = a.slice('--regiao='.length).toLowerCase() || null;
168
+ else if (a === '--dominio' && args[i + 1] && !String(args[i + 1]).startsWith('--')) opts.dominio = String(args[++i]);
169
+ else if (a.startsWith('--dominio=')) opts.dominio = a.slice('--dominio='.length) || null;
170
+ else if (a === '--esperar' && args[i + 1] && !String(args[i + 1]).startsWith('--')) opts.esperar = Number(args[++i]);
171
+ else if (a.startsWith('--esperar=')) opts.esperar = Number(a.slice('--esperar='.length));
172
+ }
173
+ if (opts.esperar !== null && (!Number.isFinite(opts.esperar) || opts.esperar < 0)) opts.esperar = null;
174
+ return opts;
175
+ }
176
+
177
+ /** Dominio do Infisical: --dominio > --regiao > o gravado na config > US. Devolve { dominio, regiao } ou null (invalido). */
178
+ function resolveDomain(opts, saved) {
179
+ if (opts.dominio) {
180
+ const dominio = apiLib.normalizeDomain(opts.dominio);
181
+ return dominio ? { dominio, regiao: 'custom' } : null;
182
+ }
183
+ if (opts.regiao) {
184
+ // so as regioes da lista: 'constructor' e '__proto__' existem em qualquer objeto, mas nao sao regioes
185
+ const known = typeof opts.regiao === 'string' && Object.prototype.hasOwnProperty.call(apiLib.REGIONS, opts.regiao);
186
+ return known ? { dominio: apiLib.REGIONS[opts.regiao], regiao: opts.regiao } : null;
187
+ }
188
+ if (saved && saved.dominio) {
189
+ const dominio = apiLib.normalizeDomain(saved.dominio);
190
+ if (dominio) return { dominio, regiao: saved.regiao || 'custom' };
191
+ }
192
+ return { dominio: apiLib.REGIONS.us, regiao: 'us' };
193
+ }
194
+
195
+ /** Nome do computador na descricao da chave: so para o aluno LER. Quem identifica a chave e o marcador com o id. */
196
+ function hostLabel(hostname) {
197
+ const safe = String(hostname || 'computador').replace(/[^A-Za-z0-9 ._-]/g, '').trim().slice(0, 40);
198
+ return safe || 'computador';
199
+ }
200
+
201
+ const COMPUTER_ID_RE = /^[0-9a-f]{10}$/;
202
+
203
+ /**
204
+ * Marcador do computador na descricao da chave (` [id] - `). A rotacao so revoga chaves que tem o marcador DESTE
205
+ * computador: o nome do host nao serve (dois Macs com o mesmo nome padrao, ou um host que mudou de nome).
206
+ */
207
+ function keyMarker(computerId) {
208
+ if (!COMPUTER_ID_RE.test(String(computerId || ''))) throw new TypeError('id do computador invalido');
209
+ return ` [${computerId}] - `;
210
+ }
211
+
212
+ /** "Auroq OS - <host> [<id>] - <data>". O host e so para leitura humana. */
213
+ function keyDescription(hostname, nowMs, computerId) {
214
+ return `${KEY_PREFIX}${hostLabel(hostname)}${keyMarker(computerId)}${new Date(nowMs).toISOString().slice(0, 10)}`;
215
+ }
216
+
217
+ /** Chaves (nao revogadas) que sao DESTE computador: descricao do Auroq OS com o marcador do id. */
218
+ function ownKeys(secrets, computerId) {
219
+ const marker = keyMarker(computerId);
220
+ return (Array.isArray(secrets) ? secrets : []).filter((s) => s && !s.revoked && String(s.description || '').startsWith(KEY_PREFIX) && String(s.description).includes(marker));
221
+ }
222
+
223
+ /** Identificador do computador que o sistema ja tem (nao muda com o nome do host). null se nao der para ler. */
224
+ function machineSeed(env = {}) {
225
+ const platform = env.platform || process.platform;
226
+ const exec = env.execFileSync || execFileSync;
227
+ const read = env.readFileSync || fs.readFileSync;
228
+ try {
229
+ if (platform === 'darwin') {
230
+ const text = String(exec('/usr/sbin/ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }));
231
+ const m = /"IOPlatformUUID"\s*=\s*"([0-9A-Fa-f-]{16,64})"/.exec(text);
232
+ return m ? m[1].toLowerCase() : null;
233
+ }
234
+ if (platform === 'win32') {
235
+ const text = String(exec('reg', ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, windowsHide: true }));
236
+ const m = /MachineGuid\s+REG_SZ\s+([0-9A-Fa-f-]{16,64})/.exec(text);
237
+ return m ? m[1].toLowerCase() : null;
238
+ }
239
+ for (const file of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
240
+ try {
241
+ const id = String(read(file, 'utf8')).trim();
242
+ if (/^[0-9a-fA-F-]{16,64}$/.test(id)) return id.toLowerCase();
243
+ } catch { /* tenta o proximo */ }
244
+ }
245
+ } catch { /* sem leitura do sistema: cai no id guardado */ }
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Id estavel e curto (10 hex) deste computador PARA ESTE USUARIO: hash do id que o sistema ja tem (nunca o
251
+ * valor cru) + usuario. Nao depende do nome do host (que muda com DHCP ou quando o aluno renomeia o computador)
252
+ * nem do infisical.json. Se o sistema nao informar nada, usa um id aleatorio guardado no infisical.json.
253
+ */
254
+ function localComputerId(d, env = {}) {
255
+ const crypto = require('crypto');
256
+ const seed = (d && typeof d.machineSeed === 'function' ? d.machineSeed() : machineSeed(env));
257
+ if (seed) {
258
+ let user = '';
259
+ try { user = os.userInfo().username; } catch { user = ''; }
260
+ return crypto.createHash('sha256').update(`auroq-os|infisical|${seed}|${user}`).digest('hex').slice(0, 10);
261
+ }
262
+ const cfg = d && d.config ? d.config.read() : null;
263
+ if (cfg && COMPUTER_ID_RE.test(String(cfg.instalacaoId || ''))) return cfg.instalacaoId;
264
+ const id = crypto.randomBytes(5).toString('hex');
265
+ if (d && d.config) d.config.write({ instalacaoId: id });
266
+ return id;
267
+ }
268
+
269
+ // ─── Credenciais no computador (mesmo arquivo de ambiente da rota A) ────────
270
+
271
+ const CRED_LINE_RE = /^\s*(export\s+)?AUROQ_INFISICAL_CLIENT_(ID|SECRET)=/;
272
+
273
+ /** clientId do Universal Auth: UUID. Nada fora desse formato entra no arquivo de ambiente (o shell executa o que estiver la). */
274
+ function isClientId(value) {
275
+ return typeof value === 'string' && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
276
+ }
277
+
278
+ function isCredPair(creds) {
279
+ return Boolean(creds) && isClientId(creds.clientId) && apiLib.isClientSecretShape(creds.clientSecret);
280
+ }
281
+
282
+ function upsertEnvFileContent(existing, creds) {
283
+ // Defesa final: o arquivo e lido pelo shell de todo terminal novo. Valor fora do formato nunca entra.
284
+ if (!isCredPair(creds)) throw new Error('credencial em formato inesperado');
285
+ const kept = String(existing || '')
286
+ .split('\n')
287
+ .filter((line) => !CRED_LINE_RE.test(line) && line.trim() !== ENV_COMMENT);
288
+ while (kept.length > 0 && kept[kept.length - 1].trim() === '') kept.pop();
289
+ kept.push('');
290
+ kept.push(ENV_COMMENT);
291
+ kept.push(`export ${CLIENT_ID_VAR}='${creds.clientId}'`);
292
+ kept.push(`export ${CLIENT_SECRET_VAR}='${creds.clientSecret}'`);
293
+ kept.push('');
294
+ return kept.join('\n');
295
+ }
296
+
297
+ /** Le as credenciais da(s) LINHA(S) que o Auroq grava (ultima definicao vence). Aspas simples, duplas ou nenhuma. */
298
+ function parseEnvFileCreds(content) {
299
+ const found = { clientId: null, clientSecret: null };
300
+ for (const line of String(content || '').split('\n')) {
301
+ const m = line.match(/^\s*(?:export\s+)?AUROQ_INFISICAL_CLIENT_(ID|SECRET)=(.*)$/);
302
+ if (!m) continue;
303
+ const value = m[2].trim().replace(/^(['"])(.*)\1$/, '$2') || null;
304
+ if (m[1] === 'ID') found.clientId = value;
305
+ else found.clientSecret = value;
306
+ }
307
+ return found.clientId || found.clientSecret ? found : null;
308
+ }
309
+
310
+ /** Linha do aluno que define a variavel de outro jeito (ex.: busca no Keychain): o valor nao e uma credencial. */
311
+ function hasDynamicCredDefinition(content) {
312
+ return String(content || '').split('\n').some((line) => {
313
+ const m = line.match(/^\s*(?:export\s+)?AUROQ_INFISICAL_CLIENT_(ID|SECRET)=(.*)$/);
314
+ if (!m) return false;
315
+ const value = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
316
+ if (value === '') return false;
317
+ return m[1] === 'ID' ? !isClientId(value) : !apiLib.isClientSecretShape(value);
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Por que NAO da para gravar as credenciais no arquivo de ambiente (ou null se da). Roda ANTES de criar a
323
+ * chave, porque ela so aparece uma vez. Mesmas razoes da rota A: atalho, arquivo no git, linha propria do
324
+ * aluno que define a variavel de outro jeito, sem permissao. No Windows vai para variavel do usuario.
325
+ */
326
+ function envFileProblem() {
327
+ if (process.platform === 'win32') return null;
328
+ const file = op.envFilePath();
329
+ const name = path.basename(file);
330
+ let st = null;
331
+ try { st = fs.lstatSync(file); } catch { /* ainda nao existe: tudo bem */ }
332
+ if (st && st.isSymbolicLink()) {
333
+ return `${name} e um atalho (link) para outro lugar; nao gravo a chave nele para ela nao ir parar num repositorio`;
334
+ }
335
+ if (st && op.isTrackedByGit(file)) {
336
+ return `${name} esta versionado no git; nao gravo a chave nele para ela nao ir parar no GitHub`;
337
+ }
338
+ if (st) {
339
+ let content = '';
340
+ try { content = fs.readFileSync(file, 'utf8'); } catch { return `${name} nao pode ser lido`; }
341
+ if (hasDynamicCredDefinition(content)) {
342
+ return `${name} ja define ${CLIENT_ID_VAR} ou ${CLIENT_SECRET_VAR} de outro jeito (uma linha sua); nao vou apagar a sua linha`;
343
+ }
344
+ }
345
+ try {
346
+ fs.accessSync(st ? file : path.dirname(file), fs.constants.W_OK);
347
+ } catch {
348
+ return `${name} (ou a pasta dele) nao aceita escrita`;
349
+ }
350
+ return null;
351
+ }
352
+
353
+ function persistCreds(creds) {
354
+ if (!isCredPair(creds)) throw new Error('credencial em formato inesperado');
355
+ if (process.platform === 'win32') {
356
+ // Variavel de ambiente do usuario: os valores passam por env do processo, nunca por argv
357
+ const psEnv = { stdio: 'pipe', env: { ...process.env, AUROQ_TMP_ID: creds.clientId, AUROQ_TMP_SECRET: creds.clientSecret } };
358
+ try {
359
+ execSync(
360
+ `powershell -NoProfile -Command "[Environment]::SetEnvironmentVariable('${CLIENT_ID_VAR}', $env:AUROQ_TMP_ID, 'User'); [Environment]::SetEnvironmentVariable('${CLIENT_SECRET_VAR}', $env:AUROQ_TMP_SECRET, 'User')"`,
361
+ psEnv,
362
+ );
363
+ } catch {
364
+ // Modo restrito (Constrained Language): chamadas .NET bloqueadas; o registro via cmdlet e permitido
365
+ execSync(
366
+ `powershell -NoProfile -Command "Set-ItemProperty -Path 'HKCU:\\Environment' -Name '${CLIENT_ID_VAR}' -Value $env:AUROQ_TMP_ID; Set-ItemProperty -Path 'HKCU:\\Environment' -Name '${CLIENT_SECRET_VAR}' -Value $env:AUROQ_TMP_SECRET"`,
367
+ psEnv,
368
+ );
369
+ warn('Gravado via registro (modo restrito detectado). Se o terminal novo nao enxergar, reinicie o computador uma vez.');
370
+ }
371
+ return 'variaveis de ambiente do usuario (Windows)';
372
+ }
373
+ const envFile = op.envFilePath();
374
+ const problem = envFileProblem();
375
+ if (problem) throw new Error(problem);
376
+ const existing = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '';
377
+ op.writeFileAtomic(envFile, upsertEnvFileContent(existing, creds));
378
+ return envFile.replace(os.homedir(), '~');
379
+ }
380
+
381
+ /** O que um terminal NOVO vai enxergar: arquivo de ambiente (Mac/Linux) ou variaveis do usuario (Windows). */
382
+ function readPersistedCreds() {
383
+ if (process.platform === 'win32') {
384
+ const run = (script) => {
385
+ try {
386
+ return execFileSync('powershell', ['-NoProfile', '-Command', script], { stdio: 'pipe', encoding: 'utf8' }).trim();
387
+ } catch {
388
+ return '';
389
+ }
390
+ };
391
+ const get = (name) => run(`[Environment]::GetEnvironmentVariable('${name}', 'User')`)
392
+ || run(`(Get-ItemProperty -Path 'HKCU:\\Environment' -Name '${name}' -ErrorAction SilentlyContinue).${name}`);
393
+ const clientId = get(CLIENT_ID_VAR);
394
+ const clientSecret = get(CLIENT_SECRET_VAR);
395
+ return clientId && clientSecret ? { clientId, clientSecret } : null;
396
+ }
397
+ try {
398
+ const file = op.envFilePath();
399
+ const parsed = fs.existsSync(file) ? parseEnvFileCreds(fs.readFileSync(file, 'utf8')) : null;
400
+ return parsed && parsed.clientId && parsed.clientSecret ? parsed : null;
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Credenciais que ja existem, na ordem de conferir: as GRAVADAS no computador primeiro e, se forem outras, as do
408
+ * ambiente deste processo. A gravada manda (como no `segredo`): um terminal/Claude aberto antes de uma troca de chave
409
+ * ainda tem a chave ANTIGA no ambiente (no Windows o ambiente do processo nunca atualiza), e ela nao pode passar na
410
+ * frente da nova: seria regravada por cima dela e revogada em seguida.
411
+ */
412
+ function currentCreds(d) {
413
+ const candidates = [];
414
+ const persisted = d.readPersistedCreds();
415
+ if (isCredPair(persisted)) candidates.push({ creds: persisted, origem: 'gravada' });
416
+ const fromEnv = { clientId: d.env[CLIENT_ID_VAR], clientSecret: d.env[CLIENT_SECRET_VAR] };
417
+ if (isCredPair(fromEnv) && !candidates.some((c) => c.creds.clientId === fromEnv.clientId && c.creds.clientSecret === fromEnv.clientSecret)) {
418
+ candidates.push({ creds: fromEnv, origem: 'ambiente' });
419
+ }
420
+ return candidates;
421
+ }
422
+
423
+ // ─── Dependencias reais (trocaveis nos testes) ───────────
424
+
425
+ function shellQuote(value) {
426
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
427
+ }
428
+
429
+ /** Ambiente de um terminal NOVO: sem as credenciais deste processo (so o arquivo/variavel do usuario conta). */
430
+ function freshEnvBase() {
431
+ const env = { ...process.env };
432
+ for (const k of Object.keys(env)) {
433
+ if (k.toUpperCase() === CLIENT_ID_VAR || k.toUpperCase() === CLIENT_SECRET_VAR) delete env[k];
434
+ }
435
+ return env;
436
+ }
437
+
438
+ /**
439
+ * "Terminal novo": no Mac/Linux abre um shell de verdade, que le sozinho o arquivo de ambiente, e roda o
440
+ * proprio auroq-os (o mesmo comando que os agentes vao usar). No Windows um processo filho herda o ambiente
441
+ * deste, entao usa o valor que um terminal novo leria (variavel do usuario).
442
+ */
443
+ function runInFreshShell(args, { input = '' } = {}) {
444
+ const bin = path.join(__dirname, '..', 'bin', 'auroq-os.js');
445
+ const run = (file, fileArgs, env) => {
446
+ const r = spawnSync(file, fileArgs, {
447
+ encoding: 'utf8', env, input, timeout: 90000, maxBuffer: 16 * 1024 * 1024, windowsHide: true,
448
+ });
449
+ return {
450
+ ok: !r.error && r.status === 0,
451
+ status: r.status,
452
+ stdout: r.stdout || '',
453
+ stderr: r.stderr || '',
454
+ timedOut: !!(r.error && r.error.code === 'ETIMEDOUT'),
455
+ };
456
+ };
457
+ if (process.platform === 'win32') {
458
+ const persisted = readPersistedCreds();
459
+ const env = freshEnvBase();
460
+ if (persisted) {
461
+ env[CLIENT_ID_VAR] = persisted.clientId;
462
+ env[CLIENT_SECRET_VAR] = persisted.clientSecret;
463
+ }
464
+ return run(process.execPath, [bin, ...args], env);
465
+ }
466
+ const { bin: shell, flag } = op.freshShellFor(op.envFilePath());
467
+ const line = [process.execPath, bin, ...args].map(shellQuote).join(' ');
468
+ return run(shell, [flag, line], freshEnvBase());
469
+ }
470
+
471
+ /** Um terminal novo enxerga as credenciais certas? (compara so um resumo; nada e impresso) */
472
+ function freshShellSees(creds) {
473
+ const crypto = require('crypto');
474
+ const digest = (c) => crypto.createHash('sha256').update(`${c.clientId}\n${c.clientSecret}`).digest('hex');
475
+ if (process.platform === 'win32') {
476
+ const p = readPersistedCreds();
477
+ return Boolean(p) && digest(p) === digest(creds);
478
+ }
479
+ const script = `const c=require('crypto');process.stdout.write(c.createHash('sha256').update((process.env.${CLIENT_ID_VAR}||'')+'\\n'+(process.env.${CLIENT_SECRET_VAR}||'')).digest('hex'))`;
480
+ const { bin: shell, flag } = op.freshShellFor(op.envFilePath());
481
+ const line = [process.execPath, '-e', script].map(shellQuote).join(' ');
482
+ const r = spawnSync(shell, [flag, line], { encoding: 'utf8', env: freshEnvBase(), input: '', timeout: 60000 });
483
+ return !r.error && r.status === 0 && String(r.stdout || '').trim().endsWith(digest(creds));
484
+ }
485
+
486
+ function makeDefaultCliFacade() {
487
+ const cliDeps = cliLib.makeDefaultCliDeps ? cliLib.makeDefaultCliDeps() : {};
488
+ return {
489
+ find: (preferred) => cliLib.findCli(cliDeps, { preferred }),
490
+ install: () => cliLib.installCli(cliDeps),
491
+ loginStatus: (cli) => cliLib.loginStatus(cliDeps, cli),
492
+ browserLogin: (cli, o) => cliLib.browserLogin(cliDeps, cli, o),
493
+ getUserToken: (cli) => cliLib.getUserToken(cliDeps, cli),
494
+ logout: (cli) => cliLib.logoutProfile(cliDeps, cli),
495
+ };
496
+ }
497
+
498
+ function makeDefaultDeps() {
499
+ return {
500
+ platform: process.platform,
501
+ env: process.env,
502
+ now: () => Date.now(),
503
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
504
+ hostname: () => os.hostname(),
505
+ lock: op.makeLock('infisical.lock'),
506
+ state: { read: readState, write: writeState },
507
+ config: { read: readConfig, write: writeConfig },
508
+ api: (baseUrl, token) => apiLib.createClient({ baseUrl, token }),
509
+ ops: apiLib.ops,
510
+ cli: makeDefaultCliFacade(),
511
+ envFileProblem,
512
+ persistCreds,
513
+ readPersistedCreds,
514
+ runFresh: runInFreshShell,
515
+ freshSees: freshShellSees,
516
+ registerCofre: (tipo) => {
517
+ try { require('./cofre').registrar(tipo); } catch { /* o registro e conforto: nunca falha a conexao */ }
518
+ },
519
+ out: { log, step, ok, warn, err, tick: () => process.stdout.write('.') },
520
+ };
521
+ }
522
+
523
+ // ─── Fluxo ───────────────────────────────────────────────
524
+
525
+ /** Traduz um erro da API (codigo da infisical-api) no resultado da conexao. `padrao` e o codigo quando nao ha um mais especifico. */
526
+ function apiFail(res, padrao, o) {
527
+ const detalhe = clean(res.mensagem || '');
528
+ const sufixo = detalhe ? ` (${detalhe})` : '';
529
+ switch (res.codigo) {
530
+ case 'rede':
531
+ case 'tempo':
532
+ return fim('erro', 'rede', `Sem conexao com o Infisical${sufixo}.`);
533
+ case 'limite':
534
+ return fim('erro', 'limite', `O Infisical limitou os acessos por enquanto (limite de uso)${sufixo}. Espere alguns minutos e rode de novo.`);
535
+ case 'bloqueada':
536
+ return fim('erro', 'bloqueada', 'A chave do agente ficou bloqueada por alguns minutos (varias tentativas seguidas). Espere uns 6 minutos e rode de novo.');
537
+ case 'limite-plano':
538
+ return fim('erro', 'limite-plano', `O plano gratis do Infisical tem 5 vagas e elas acabaram: voce conta 1 e o agente Auroq (a identidade, uma so) conta 1. Apagar uma chave nao libera vaga. Para liberar, apague no painel do Infisical uma identidade de maquina (machine identity) que voce nao usa e rode de novo${sufixo}.`);
539
+ case 'plano-restringe':
540
+ return fim('erro', 'plano-restringe', `O plano gratis do Infisical nao permitiu ${o && o.acao ? o.acao : 'essa etapa'}${sufixo}. Isso NAO e falta de vaga: nao apague nada no painel. Nao consigo contornar sozinho; se o 1Password for uma opcao para voce, use ele como cofre, ou peca ajuda ao Arcano com o codigo plano-restringe.`);
541
+ case 'sem-permissao':
542
+ return fim('erro', 'sem-permissao', `Sua conta do Infisical nao tem permissao para ${o && o.acao ? o.acao : 'essa etapa'}${sufixo}. Encerrei a sessao: se a conta ou a organizacao for a errada, rode de novo e escolha outra no navegador.`);
543
+ case 'sessao-expirada':
544
+ return fim('aguardando', 'login-no-navegador', 'A sessao do navegador expirou. Rode de novo para entrar outra vez no Infisical.', { reentrar: true });
545
+ default:
546
+ break;
547
+ }
548
+ if (padrao === 'falha-projeto') return fim('erro', 'falha-projeto', `Nao consegui preparar o projeto Auroq no Infisical${sufixo}.`);
549
+ if (padrao === 'falha-identidade') return fim('erro', 'falha-identidade', `Nao consegui criar a identidade do agente no Infisical${sufixo}.`);
550
+ if (padrao === 'falha-chave') return fim('erro', 'falha-chave', `Nao consegui criar a chave do agente no Infisical${sufixo}.`);
551
+ return fim('erro', 'falha', `Resposta inesperada do Infisical${sufixo}.`);
552
+ }
553
+
554
+ /**
555
+ * Falhas de login (chave recusada) que ESTA execucao ja causou na identidade. O Infisical trava a identidade por
556
+ * 5 minutos na 3a falha seguida com menos de 30 s entre elas (ate com a chave certa): conferir uma chave morta
557
+ * conta como uma delas, entao o teste da chave nova tem de respeitar o que ja foi gasto.
558
+ */
559
+ function newLoginLedger() {
560
+ return { count: 0, lastAt: 0 };
561
+ }
562
+
563
+ function noteLoginFailure(d, ledger) {
564
+ const now = d.now();
565
+ if (ledger.count > 0 && now - ledger.lastAt > LOGIN_LOCK_WINDOW_MS) ledger.count = 0;
566
+ ledger.count += 1;
567
+ ledger.lastAt = now;
568
+ }
569
+
570
+ /** Antes de um login que pode falhar: se a falha seria a 3a seguida, espera o contador da identidade zerar. */
571
+ async function waitLoginRoom(d, ledger) {
572
+ if (ledger.count < 2) return;
573
+ const gap = d.now() - ledger.lastAt;
574
+ if (gap <= LOGIN_LOCK_WINDOW_MS) await d.sleep(LOGIN_LOCK_WINDOW_MS - gap + 1000);
575
+ ledger.count = 0;
576
+ }
577
+
578
+ /** O que as credenciais gravadas conseguem: login Universal Auth e listar os segredos do projeto. */
579
+ async function checkCreds(d, cfg, creds) {
580
+ const login = await d.ops.universalLogin(d.api(cfg.dominio), creds);
581
+ if (!login.ok) return { ok: false, motivo: login.codigo, mensagem: login.mensagem };
582
+ const listed = await d.ops.listSecretNames(d.api(cfg.dominio, login.accessToken), { projectId: cfg.projectId, environment: cfg.env || ENV_SLUG });
583
+ if (!listed.ok) {
584
+ // projeto apagado ou identidade tirada do projeto: a chave nao serve mais para o combinado
585
+ if (listed.codigo === 'nao-encontrado' || listed.codigo === 'sem-permissao') return { ok: false, motivo: 'sem-acesso', mensagem: listed.mensagem };
586
+ return { ok: false, motivo: listed.codigo, mensagem: listed.mensagem };
587
+ }
588
+ return { ok: true };
589
+ }
590
+
591
+ const TROCA_PENDENTE_NOTA = 'Atencao: a troca de chave deste computador ainda nao terminou — a chave antiga continua valendo. Rode de novo para terminar.';
592
+
593
+ /** Troca de chave que ficou pela metade (so ids de chaves, nunca segredo) e que vale para este endereco do Infisical. */
594
+ function readPendingRotation(cfg, dominio) {
595
+ const p = cfg && cfg.rotacaoPendente;
596
+ if (!p || typeof p !== 'object' || !Array.isArray(p.ids)) return null;
597
+ if (p.dominio && apiLib.normalizeDomain(p.dominio) !== dominio) return null;
598
+ const ids = [...new Set(p.ids.filter((x) => typeof x === 'string' && x.length > 0 && x.length <= 100))];
599
+ if (!ids.length) return null;
600
+ return { identityId: typeof p.identityId === 'string' && p.identityId ? p.identityId : null, ids };
601
+ }
602
+
603
+ /** "Nao deu para conferir a chave agora" (limite, rede, bloqueio, servidor...): NAO e prova de chave morta, entao nao cria nada. */
604
+ function indisponivel(motivo, mensagem, nada) {
605
+ if (motivo === 'limite') return fim('erro', 'limite', `O Infisical limitou os acessos por enquanto (limite de uso). ${nada} Espere alguns minutos e rode de novo.`);
606
+ if (motivo === 'bloqueada') return fim('erro', 'bloqueada', `A chave do agente ficou bloqueada por alguns minutos (varias tentativas seguidas). ${nada} Espere uns 6 minutos e rode de novo.`);
607
+ if (motivo === 'rede' || motivo === 'tempo') return fim('erro', 'rede', `Sem conexao com o Infisical para conferir a chave deste computador. ${nada} Confira a internet e rode de novo.`);
608
+ return fim('erro', 'credencial-indisponivel', `Nao consegui conferir a chave deste computador agora (${clean(mensagem) || 'resposta inesperada do Infisical'}). ${nada} Tente de novo em alguns minutos; se continuar assim por mais de uma hora, a chave pode ter sido revogada: rode com --rotacionar.`);
609
+ }
610
+
611
+ /**
612
+ * Um terminal NOVO enxerga esta chave? Vale para qualquer origem. Chave que so existe no ambiente deste processo
613
+ * (e funciona) e gravada aqui, sem criar outra; se nao der para gravar, nao ha terminal novo que a enxergue.
614
+ */
615
+ async function terminalSees(d, cand) {
616
+ if (await d.freshSees(cand.creds)) return true;
617
+ if (cand.origem === 'ambiente' && !d.envFileProblem()) {
618
+ try {
619
+ d.persistCreds(cand.creds);
620
+ } catch {
621
+ return false;
622
+ }
623
+ return Boolean(await d.freshSees(cand.creds));
624
+ }
625
+ return false;
626
+ }
627
+
628
+ function notSeenResult(origem) {
629
+ if (origem === 'ambiente') {
630
+ return fim('erro', 'nao-gravou', 'A chave funciona neste terminal, mas um terminal novo nao a enxerga e nao consegui grava-la neste computador. Peca ajuda ao Arcano com o codigo nao-gravou; nao criei nada novo.');
631
+ }
632
+ return fim('erro', 'nao-gravou', 'A chave esta gravada neste computador, mas um terminal novo nao a enxerga (o seu terminal parece ler outro arquivo de configuracao). Peca ajuda ao Arcano com o codigo nao-gravou; nao criei nada novo.');
633
+ }
634
+
635
+ /** Fecha a sessao do aluno (perfil auroq) se esta execucao a usou: o JWT de administrador da organizacao nao fica parado no computador. */
636
+ async function closeSession(d, ctx) {
637
+ if (!ctx.sessionUsed || !ctx.cliPath) return;
638
+ let r = null;
639
+ try {
640
+ r = await d.cli.logout(ctx.cliPath);
641
+ } catch {
642
+ r = { ok: false };
643
+ }
644
+ if (r && r.ok === false) {
645
+ // O programa que o Auroq instalou nao esta no PATH: a linha traz o caminho dele, escrita sem sintaxe de shell
646
+ // (aspas e & funcionam so em alguns terminais do Windows).
647
+ d.out.warn(`Nao consegui sair da sua sessao do Infisical neste computador; ela expira sozinha em alguns dias. Para sair agora, rode o programa "${clean(ctx.cliPath, 400)}" com: logout --profile auroq`);
648
+ return;
649
+ }
650
+ d.out.log(`${DIM}O Auroq saiu da sua sessao do Infisical. Se abrir o painel do Infisical no navegador, entre de novo.${RESET}`);
651
+ }
652
+
653
+ async function connectAutomatic(opts = {}, deps = {}) {
654
+ const d = { ...makeDefaultDeps(), ...deps };
655
+ if (!d.lock.acquire()) {
656
+ return fim('aguardando', 'outra-execucao', 'Ja tem outra execucao do conectar-cofre (Infisical) em andamento neste computador. Espere ela terminar (ate uns 15 minutos) e rode de novo; nao abri nada em duplicidade.');
657
+ }
658
+ const prog = makeProgress(d);
659
+ const ctx = { cliPath: null, sessionUsed: false, trocaPendente: false, logins: newLoginLedger() };
660
+ let result = null;
661
+ let thrown = null;
662
+ try {
663
+ try {
664
+ result = await runConnect(opts, d, prog, ctx);
665
+ } catch (e) {
666
+ thrown = e;
667
+ result = fim('erro', 'falha', `Falha inesperada: ${clean(e && e.message)}`);
668
+ }
669
+ // Quando a configuracao termina (certo ou com erro que o aluno resolve em outra rodada), a sessao dele sai.
670
+ // Nao roda no meio de um login que ainda nao terminou (ai a sessao nem foi usada).
671
+ await closeSession(d, ctx);
672
+ if (ctx.trocaPendente && result.status !== 'ok') result = { ...result, mensagem: `${result.mensagem} ${TROCA_PENDENTE_NOTA}` };
673
+ prog.save(result);
674
+ } finally {
675
+ d.lock.release();
676
+ }
677
+ if (thrown) throw thrown;
678
+ return result;
679
+ }
680
+
681
+ async function runConnect(opts, d, prog, ctx) {
682
+ const out = d.out;
683
+ const t0 = d.now();
684
+ const saved = d.config.read();
685
+ const where = resolveDomain(opts, saved);
686
+ if (!where) return fim('erro', 'falha', 'Endereco do Infisical invalido. Use --regiao us, --regiao eu ou --dominio https://...');
687
+ const { dominio, regiao } = where;
688
+ if (regiao === 'custom' && !Object.values(apiLib.REGIONS).includes(dominio)) {
689
+ out.warn(`Usando o endereco ${dominio} (nao e o Infisical Cloud US nem EU).`);
690
+ }
691
+ let computerId = null;
692
+ const getComputerId = () => {
693
+ if (!computerId) computerId = typeof d.computerId === 'function' ? d.computerId() : localComputerId(d);
694
+ return computerId;
695
+ };
696
+ // Troca de chave que ficou pela metade numa rodada anterior (a chave antiga ainda vale)
697
+ const pending = readPendingRotation(saved, dominio);
698
+ if (pending) ctx.trocaPendente = true;
699
+
700
+ // 1. Ja esta conectado? (sem CLI e sem navegador: so as credenciais deste computador)
701
+ let reuse = null; // credencial que funciona, para nao criar outra chave
702
+ let reuseOrigem = null;
703
+ if (!opts.rotacionar) {
704
+ const candidates = currentCreds(d);
705
+ const cfg = d.config.read();
706
+ let dead = false;
707
+ if (candidates.length && cfg && cfg.projectId) {
708
+ for (const cand of candidates) {
709
+ const check = await checkCreds(d, cfg, cand.creds);
710
+ if (check.motivo === 'credencial-invalida') noteLoginFailure(d, ctx.logins);
711
+ if (check.ok) {
712
+ // A chave entra. Antes de dizer "conectado": um terminal novo tem de enxerga-la e ela tem de GRAVAR.
713
+ if (!(await terminalSees(d, cand))) return notSeenResult(cand.origem);
714
+ if (pending) {
715
+ // troca de chave incompleta: segue o fluxo so para terminar de revogar a chave antiga (sem criar outra)
716
+ reuse = cand.creds;
717
+ reuseOrigem = cand.origem;
718
+ dead = false;
719
+ break;
720
+ }
721
+ if (prog.etapas.teste !== 'feito') {
722
+ out.step('Testando como um terminal novo...');
723
+ const test = await freshTest(d, out, ctx.logins);
724
+ if (!test.ok) {
725
+ return fim('erro', 'teste-falhou', `A chave entra no projeto, mas o teste de gravar e ler um segredo falhou (${test.motivo}). Nao registrei o cofre nem disse que esta pronto. Espere um minuto e rode de novo; se continuar, confira no painel do Infisical se a identidade "${IDENTITY_NAME}" do projeto tem o papel Member (ler e gravar) e, se nao tiver, apague a identidade e rode de novo.`);
726
+ }
727
+ out.ok('Gravou, leu e apagou um segredo de teste usando so a chave do agente');
728
+ }
729
+ out.ok('Ja conectado: a chave deste computador entra no projeto Auroq.');
730
+ prog.all();
731
+ d.registerCofre('infisical');
732
+ return fim('ok', 'ja-conectado', 'Ja conectado: a chave do agente entra no projeto Auroq do Infisical.');
733
+ }
734
+ if (check.motivo === 'credencial-invalida' || check.motivo === 'sem-acesso') {
735
+ dead = true;
736
+ continue;
737
+ }
738
+ // Nao deu para conferir agora (limite, rede, tempo, servidor...): NAO e prova de chave morta.
739
+ // Criar outra chave aqui duplicaria a cada tentativa.
740
+ return indisponivel(check.motivo, check.mensagem, 'Nao criei nada novo, para nao duplicar.');
741
+ }
742
+ if (dead) {
743
+ prog.reset('identidade', 'teste', 'pronto');
744
+ out.warn('A chave que estava salva neste computador nao funciona mais (revogada, ou o projeto mudou). Vou gerar uma nova.');
745
+ }
746
+ } else if (candidates.length) {
747
+ reuse = candidates[0].creds; // ha chave mas falta a config: confere de novo mais adiante, sem criar outra
748
+ reuseOrigem = candidates[0].origem;
749
+ }
750
+ } else {
751
+ prog.reset('identidade', 'teste', 'pronto');
752
+ }
753
+
754
+ // Antes de criar QUALQUER coisa: a chave so aparece uma vez, entao tem de haver onde guarda-la.
755
+ const envProblem = d.envFileProblem();
756
+ if (envProblem) {
757
+ return fim('erro', 'arquivo-ambiente', `Nao criei nada. ${envProblem}. Peca ajuda ao Arcano com o codigo arquivo-ambiente.`);
758
+ }
759
+
760
+ // 2. CLI do Infisical
761
+ out.step('Verificando a ferramenta do Infisical (infisical)...');
762
+ let cli = await d.cli.find(saved && saved.cli);
763
+ if (!cli || cli.antigo) {
764
+ out.warn(cli ? `A ferramenta infisical v${cli.version} e antiga (precisa da ${cliLib.MIN_CLI_VERSION} ou mais) — instalando a nova...` : 'Ferramenta infisical nao encontrada — instalando...');
765
+ const inst = await d.cli.install();
766
+ if (!inst || !inst.ok) {
767
+ const detalhe = inst && inst.detalhe ? ` ${clean(inst.detalhe)}` : '';
768
+ if (cli) return fim('erro', 'cli-antigo', `A ferramenta infisical v${cli.version} e antiga (precisa da ${cliLib.MIN_CLI_VERSION} ou mais) e nao consegui atualizar sozinho.${detalhe}`);
769
+ return fim('erro', 'sem-cli', `Nao consegui instalar a ferramenta do Infisical (infisical).${detalhe}`);
770
+ }
771
+ cli = { path: inst.path, version: inst.version, antigo: false };
772
+ }
773
+ prog.mark('cli');
774
+ out.ok(`infisical v${cli.version}`);
775
+ ctx.cliPath = cli.path;
776
+ // Com conexao ja gravada, o endereco novo (--regiao/--dominio) so entra na config junto com a chave nova (mais
777
+ // abaixo): se a troca for abortada, o cofre que funcionava continua apontando para o endereco que funciona.
778
+ // Sem conexao gravada a config nao e usada pelo cofre (exige projectId), entao lembrar o endereco ja aqui e inofensivo.
779
+ const jaTemConexao = Boolean(saved && saved.projectId);
780
+ d.config.write(jaTemConexao ? { cli: cli.path } : { cli: cli.path, dominio, regiao });
781
+
782
+ // 3. Login no navegador (perfil proprio "auroq": nao mexe no login que o aluno ja tenha)
783
+ out.step('Entrando no Infisical...');
784
+ let status = await d.cli.loginStatus(cli.path);
785
+ if (status && status.state === 'authenticated' && status.domain && apiLib.normalizeDomain(status.domain) !== dominio) {
786
+ status = { state: 'none' }; // sessao de outra regiao/endereco: entra de novo no certo
787
+ }
788
+ if (!status || status.state !== 'authenticated') {
789
+ prog.reset('conta', 'login');
790
+ out.log(`${BOLD}Vai abrir uma pagina do Infisical no navegador. Entre na sua conta (se ainda nao tem, crie uma: e gratis) e volte aqui.${RESET}`);
791
+ // Espera pelo navegador: o que o aluno pediu, no maximo MAX_WAIT_SECONDS MENOS o que ja foi gasto (instalar o
792
+ // CLI etc.), para a execucao inteira caber nos 10 min da ferramenta do agente. Nunca 0 (seria "sem limite").
793
+ const spentS = Math.max(0, Math.floor((d.now() - t0) / 1000));
794
+ const asked = Number(opts.esperar);
795
+ const requested = opts.esperar !== null && opts.esperar !== undefined && Number.isFinite(asked) && asked >= 0 ? asked : DEFAULT_WAIT_SECONDS;
796
+ const waitSeconds = Math.max(MIN_WAIT_SECONDS, Math.min(MAX_WAIT_SECONDS - spentS, requested));
797
+ let loginUrl = null;
798
+ const login = await d.cli.browserLogin(cli.path, {
799
+ domain: dominio,
800
+ timeoutMs: waitSeconds * 1000,
801
+ onUrl: (url) => {
802
+ loginUrl = url;
803
+ out.log(`Se o navegador nao abrir sozinho, abra este endereco: ${url}`);
804
+ },
805
+ });
806
+ if (!login || !login.ok) {
807
+ const url = (login && login.url) || loginUrl;
808
+ const extra = url ? { url } : {};
809
+ const detalhe = login && login.detalhe ? clean(login.detalhe, 160) : '';
810
+ if (login && login.motivo === 'rede') return fim('erro', 'rede', `Sem conexao com o Infisical para entrar. ${detalhe}`.trim());
811
+ return fim('aguardando', 'login-no-navegador', `O login no navegador nao terminou${login && login.motivo === 'tempo' ? ' a tempo' : ''}. Entre na sua conta do Infisical na pagina que abriu (ou crie uma, e gratis) e rode de novo.${detalhe ? ` (${detalhe})` : ''}`, extra);
812
+ }
813
+ status = await d.cli.loginStatus(cli.path);
814
+ if (!status || status.state !== 'authenticated') {
815
+ return fim('aguardando', 'login-no-navegador', 'O login terminou mas o Infisical ainda nao confirmou a sessao. Rode de novo.');
816
+ }
817
+ }
818
+ // Daqui em diante existe uma sessao do aluno que ESTA execucao usa: ela sai quando a execucao termina (closeSession),
819
+ // seja qual for o resultado. Os erros que so se resolvem trocando de conta/organizacao tambem a encerram, para a
820
+ // proxima rodada abrir o navegador de novo.
821
+ ctx.sessionUsed = true;
822
+ const tok = await d.cli.getUserToken(cli.path);
823
+ if (!tok || !tok.ok) {
824
+ prog.reset('login');
825
+ return fim('aguardando', 'login-no-navegador', 'Nao consegui ler a sessao do Infisical neste computador. Rode de novo para entrar outra vez.');
826
+ }
827
+ const jwt = tok.token;
828
+ const claims = apiLib.decodeJwtPayload(jwt) || {};
829
+ if (claims.exp && claims.exp * 1000 < d.now() + 60000) {
830
+ prog.reset('login');
831
+ return fim('aguardando', 'login-no-navegador', 'A sessao do Infisical expirou. Rode de novo para entrar outra vez.');
832
+ }
833
+ if (!claims.organizationId) {
834
+ prog.reset('login');
835
+ return fim('erro', 'sem-organizacao', 'Sua sessao do Infisical nao tem uma organizacao escolhida. Encerrei a sessao: rode de novo e, na pagina do navegador, escolha a organizacao (ou crie uma).');
836
+ }
837
+ prog.mark('conta', 'login');
838
+ out.ok(`Entrou no Infisical${status.email ? ` como ${status.email}` : ''}`);
839
+ const userApi = d.api(dominio, jwt);
840
+
841
+ // 4. Projeto Auroq
842
+ out.step('Preparando o projeto Auroq...');
843
+ let created = false;
844
+ let project = await d.ops.getProjectBySlug(userApi, PROJECT_SLUG);
845
+ if (!project.ok && project.codigo === 'nao-encontrado') {
846
+ project = await d.ops.createProject(userApi, { name: PROJECT_NAME, slug: PROJECT_SLUG });
847
+ created = project.ok;
848
+ } else if (!project.ok && project.codigo === 'sem-permissao') {
849
+ return fim('erro', 'projeto-sem-acesso', `Ja existe um projeto com o endereco "${PROJECT_SLUG}" na sua organizacao do Infisical, mas voce nao e membro dele. Encerrei a sessao: peca acesso a quem o criou, ou rode de novo e escolha outra organizacao no navegador.`);
850
+ }
851
+ if (!project.ok) {
852
+ const failed = apiFail(project, 'falha-projeto', { acao: 'criar o projeto' });
853
+ if (failed.reentrar) prog.reset('login');
854
+ return failed;
855
+ }
856
+ const proj = project.project;
857
+ const envs = Array.isArray(proj.environments) ? proj.environments.map((e) => (e && e.slug) || e) : [];
858
+ if (envs.length && !envs.includes(ENV_SLUG)) {
859
+ return fim('erro', 'falha-projeto', `O projeto ${PROJECT_NAME} nao tem o ambiente ${ENV_SLUG} (tem: ${envs.join(', ')}). Nao mexi em nada.`);
860
+ }
861
+ out.ok(created ? `Projeto ${PROJECT_NAME} criado` : `Projeto ${PROJECT_NAME} ja existe — vou usar ele (nada do que esta nele muda).`);
862
+ prog.mark('projeto');
863
+ // O que vai para o infisical.json (endereco, projeto, identidade) so e gravado depois que a chave nova esta
864
+ // guardada neste computador (mais abaixo): a config e a chave andam juntas.
865
+ const connCfg = { dominio, regiao, orgId: claims.organizationId, projectId: proj.id, projectSlug: PROJECT_SLUG, env: ENV_SLUG, cli: cli.path, atualizado_em: new Date(d.now()).toISOString() };
866
+
867
+ // 5. Identidade do agente (identidade DO PROJETO: so enxerga este projeto) e a chave deste computador
868
+ out.step('Criando a chave do agente (so no projeto Auroq, sem validade)...');
869
+ const listed = await d.ops.listProjectIdentities(userApi, proj.id, { search: IDENTITY_NAME });
870
+ if (!listed.ok) {
871
+ const failed = apiFail(listed, 'falha-identidade', { acao: 'listar as identidades do projeto' });
872
+ if (failed.reentrar) prog.reset('login');
873
+ return failed;
874
+ }
875
+ const exact = (listed.identities || []).filter((i) => i.name === IDENTITY_NAME);
876
+ // A lista nao diz quais tem acesso por chave (so o GET por identidade diz): prefere a que ja tem Universal Auth.
877
+ let identity = null;
878
+ let ua = null;
879
+ for (const cand of exact) {
880
+ const found = await d.ops.getUniversalAuth(userApi, cand.id);
881
+ if (found.ok && found.clientId) {
882
+ identity = cand;
883
+ ua = found;
884
+ break;
885
+ }
886
+ }
887
+ if (!identity) identity = exact[0] || null;
888
+ const extras = exact.length > 1;
889
+ let novaIdentidade = false;
890
+ if (!identity) {
891
+ const createdIdentity = await d.ops.createProjectIdentity(userApi, proj.id, { name: IDENTITY_NAME, role: 'member' });
892
+ if (!createdIdentity.ok) {
893
+ const failed = apiFail(createdIdentity, 'falha-identidade', { acao: 'criar a identidade do agente' });
894
+ if (failed.reentrar) prog.reset('login');
895
+ return failed;
896
+ }
897
+ identity = createdIdentity.identity;
898
+ novaIdentidade = true;
899
+ out.ok('Identidade do agente criada');
900
+ } else {
901
+ out.ok('Identidade do agente ja existe — vou usar ela.');
902
+ }
903
+ if (identity.projectId && identity.projectId !== proj.id) {
904
+ return fim('erro', 'falha-identidade', 'A identidade "Auroq" encontrada nao e do projeto Auroq (alcance maior que o combinado). Nao usei; apague ou renomeie no painel do Infisical e rode de novo.');
905
+ }
906
+
907
+ if (!ua) {
908
+ const attach = await d.ops.attachUniversalAuth(userApi, identity.id, {});
909
+ if (!attach.ok) {
910
+ const failed = apiFail(attach, 'falha-identidade', { acao: 'ligar o acesso por chave a identidade' });
911
+ if (failed.reentrar) prog.reset('login');
912
+ return failed;
913
+ }
914
+ ua = attach.clientId ? attach : await d.ops.getUniversalAuth(userApi, identity.id);
915
+ }
916
+ if (!ua.ok || !ua.clientId) {
917
+ return apiFail(ua.ok ? { codigo: 'falha', mensagem: 'sem clientId' } : ua, 'falha-identidade', { acao: 'ler o acesso por chave da identidade' });
918
+ }
919
+ if (!isClientId(ua.clientId)) {
920
+ // O clientId vai para o arquivo de ambiente que o shell executa: so entra se tiver a forma de um UUID.
921
+ return fim('erro', 'falha-identidade', 'O Infisical devolveu o identificador da chave do agente num formato inesperado. Nao guardei nada neste computador e nao criei chave. Rode de novo; se repetir, peca ajuda ao Arcano com o codigo falha-identidade.');
922
+ }
923
+ connCfg.identityId = identity.id;
924
+
925
+ // Chaves antigas desta identidade que ainda faltam revogar (troca de chave que ficou pela metade antes)
926
+ let rotacaoSemAlvo = false; // --rotacionar sem achar nenhuma chave antiga para revogar (a que vazou pode continuar valendo)
927
+ let toRevoke = [];
928
+ if (pending) {
929
+ if (!pending.identityId || pending.identityId === identity.id) toRevoke = [...pending.ids];
930
+ else out.warn('Havia uma troca de chave pendente de outra identidade do Infisical; ela nao pode ser terminada por aqui.');
931
+ }
932
+
933
+ // Ja ha uma chave valida deste computador (so faltava a config)? Reaproveita, sem criar outra.
934
+ let creds = null;
935
+ let secretId = null;
936
+ if (reuse && reuse.clientId === ua.clientId) {
937
+ const check = await d.ops.universalLogin(d.api(dominio), reuse);
938
+ if (!check.ok && check.codigo === 'credencial-invalida') noteLoginFailure(d, ctx.logins);
939
+ if (check.ok) {
940
+ creds = reuse;
941
+ out.ok('A chave que ja estava neste computador continua valendo — nao crio outra.');
942
+ if (!(await terminalSees(d, { creds, origem: reuseOrigem }))) return notSeenResult(reuseOrigem);
943
+ } else if (check.codigo !== 'credencial-invalida') {
944
+ // Falha passageira (limite, rede, bloqueio, servidor...) NAO prova chave morta: criar outra deixaria a atual solta.
945
+ return indisponivel(check.codigo, check.mensagem, 'Nao criei chave nova, para nao deixar a atual solta.');
946
+ }
947
+ }
948
+ if (!creds) {
949
+ // Rotacao: lista ANTES as chaves antigas deste computador (a nova ainda nao existe, entao nunca entra na lista)
950
+ let rotationIds = [];
951
+ if (opts.rotacionar) {
952
+ const olds = await d.ops.listClientSecrets(userApi, identity.id);
953
+ if (!olds.ok) {
954
+ const failed = apiFail(olds, 'falha-chave', { acao: 'listar as chaves antigas deste computador' });
955
+ if (failed.reentrar) prog.reset('login');
956
+ if (failed.codigo === 'falha-chave') {
957
+ const detalhe = clean(olds.mensagem || '');
958
+ return fim('erro', 'falha-chave', `Nao consegui listar as chaves antigas deste computador${detalhe ? ` (${detalhe})` : ''}. Nao troquei nada: a chave atual continua valendo. Rode de novo.`);
959
+ }
960
+ return failed;
961
+ }
962
+ rotationIds = ownKeys(olds.secrets, getComputerId()).map((s) => s.id);
963
+ // A chave EM USO aqui e a que pode ter vazado: entra por fato, nao so pela descricao (o id do computador pode ter
964
+ // mudado: pasta de usuario copiada para um computador novo, sistema que nao deu o identificador numa rodada).
965
+ // A lista devolve os 4 primeiros caracteres de cada chave; so vale se bater com UMA chave viva. O prefixo so e
966
+ // comparado aqui dentro: nunca vai para tela, log nem config.
967
+ const vivas = (olds.secrets || []).filter((s) => !s.revoked);
968
+ for (const cand of currentCreds(d)) {
969
+ if (cand.creds.clientId !== ua.clientId) continue;
970
+ const achadas = vivas.filter((s) => s.prefix && s.prefix === cand.creds.clientSecret.slice(0, 4));
971
+ if (achadas.length === 1 && !rotationIds.includes(achadas[0].id)) rotationIds.push(achadas[0].id);
972
+ }
973
+ rotacaoSemAlvo = rotationIds.length === 0 && vivas.length > 0;
974
+ }
975
+ const description = keyDescription(d.hostname(), d.now(), getComputerId());
976
+ const cs = await d.ops.createClientSecret(userApi, identity.id, { description });
977
+ if (!cs.ok || !cs.clientSecret) {
978
+ const failed = apiFail(cs.ok ? { codigo: 'falha', mensagem: 'sem chave na resposta' } : cs, 'falha-chave', { acao: 'criar a chave do agente' });
979
+ if (failed.reentrar) prog.reset('login');
980
+ return failed;
981
+ }
982
+ secretId = cs.secretId;
983
+ creds = { clientId: ua.clientId, clientSecret: cs.clientSecret };
984
+ out.ok('Chave criada (nao aparece na tela)');
985
+ if (!isCredPair(creds)) {
986
+ // Nada fora do formato de uma chave do Infisical chega ao arquivo de ambiente (o shell executa o que estiver la)
987
+ const revoked = secretId ? await d.ops.revokeClientSecret(userApi, identity.id, secretId) : null;
988
+ if (revoked && revoked.ok) {
989
+ return fim('erro', 'falha-chave', 'O Infisical devolveu a chave num formato inesperado. Nao guardei nada neste computador e revoguei a chave. Rode de novo; se repetir, peca ajuda ao Arcano com o codigo falha-chave.');
990
+ }
991
+ return fim('erro', 'falha-chave', `O Infisical devolveu a chave num formato inesperado. Nao guardei nada neste computador, mas tambem nao consegui revogar a chave que acabei de criar: ela continua valida, sem uso. No painel do Infisical, na identidade ${IDENTITY_NAME}, apague a chave "${description}".`, { aviso: 'chave-orfa' });
992
+ }
993
+ // Daqui em diante a chave so existe aqui dentro: grava ANTES de qualquer outra coisa
994
+ let persistError = null;
995
+ try {
996
+ const saidaEm = d.persistCreds(creds);
997
+ out.ok(`Chave salva em ${saidaEm}`);
998
+ } catch (e) {
999
+ persistError = clean(e && e.message);
1000
+ }
1001
+ if (!persistError) {
1002
+ const back = d.readPersistedCreds();
1003
+ if (!back || back.clientId !== creds.clientId || back.clientSecret !== creds.clientSecret) {
1004
+ persistError = 'um terminal novo nao enxerga o que foi gravado';
1005
+ }
1006
+ }
1007
+ if (persistError) {
1008
+ const revoked = secretId ? await d.ops.revokeClientSecret(userApi, identity.id, secretId) : null; // melhor esforco: nao deixa chave orfa
1009
+ if (revoked && revoked.ok) {
1010
+ return fim('erro', 'nao-gravou', `Nao consegui guardar a chave neste computador (${persistError}). Revoguei a chave que tinha acabado de criar, entao nao sobrou nada solto. Resolva isso e rode de novo.`);
1011
+ }
1012
+ return fim('erro', 'nao-gravou', `Nao consegui guardar a chave neste computador (${persistError.slice(0, 100)}) e tambem nao consegui revogar a que acabei de criar: ela continua valida no Infisical, sem uso. No painel, na identidade ${IDENTITY_NAME}, apague a chave "${description}". Depois resolva isso e rode de novo.`, { aviso: 'chave-orfa' });
1013
+ }
1014
+ // as chaves antigas deste computador viram "a revogar" (so depois do teste passar; ate la ficam lembradas na config)
1015
+ toRevoke = [...new Set([...toRevoke, ...rotationIds.filter((id) => id !== secretId)])];
1016
+ }
1017
+ // A chave nova esta guardada: agora sim a config aponta para o endereco/projeto/identidade novos e lembra a troca pendente.
1018
+ if (toRevoke.length) connCfg.rotacaoPendente = { dominio, identityId: identity.id, ids: toRevoke };
1019
+ else if (pending) connCfg.rotacaoPendente = null; // pendencia (deste endereco) que nao se aplica mais a esta identidade
1020
+ d.config.write(connCfg);
1021
+ prog.mark('identidade');
1022
+
1023
+ // 6. Teste em terminal novo: grava, le e apaga um segredo de teste (so a chave, sem a sessao do aluno)
1024
+ out.step('Testando como um terminal novo...');
1025
+ const test = await freshTest(d, out, ctx.logins);
1026
+ if (!test.ok) {
1027
+ const troca = toRevoke.length ? ' A troca de chave ainda nao terminou: a chave antiga NAO foi revogada e continua valendo; rodar de novo termina isso.' : '';
1028
+ return fim('erro', 'teste-falhou', `A chave foi salva mas o teste falhou (${test.motivo}). A chave nao se perde: rode de novo (sem --rotacionar) para conferir.${troca}`);
1029
+ }
1030
+ out.ok('Gravou, leu e apagou um segredo de teste usando so a chave do agente');
1031
+ prog.mark('teste', 'pronto');
1032
+
1033
+ // 7. Fecho: so agora (com o teste passado) a chave antiga e revogada
1034
+ let revogacao = null;
1035
+ if (toRevoke.length) {
1036
+ revogacao = await revokeKeys(d, userApi, identity.id, toRevoke.filter((id) => id !== secretId), out, secretId ? null : creds);
1037
+ d.config.write({ rotacaoPendente: revogacao.restantes.length ? { dominio, identityId: identity.id, ids: revogacao.restantes } : null });
1038
+ }
1039
+ d.registerCofre('infisical');
1040
+ const restart = d.platform === 'win32'
1041
+ ? 'Feche TODAS as janelas do terminal (se usa VS Code ou Windows Terminal, feche o programa inteiro) e abra de novo, para eles enxergarem a conexao nova.'
1042
+ : 'Feche e abra de novo o terminal (e o Claude/Codex), para eles enxergarem a conexao nova.';
1043
+ out.log('');
1044
+ out.ok(`${BOLD}Infisical conectado.${RESET} O agente usa so a chave: nao precisa entrar de novo no navegador.`);
1045
+ out.log(`${DIM}${restart}${RESET}`);
1046
+ out.log(`${DIM}Plano gratis do Infisical: 5 vagas no total (voce conta 1 e o agente Auroq, a identidade, conta 1; varias chaves da mesma identidade nao gastam vaga), projetos ilimitados e 3 ambientes. Confira os limites na tela do Infisical.${RESET}`);
1047
+ if (extras) out.log(`${DIM}Tem mais de uma identidade chamada "${IDENTITY_NAME}" no projeto; usei a que ja tinha acesso por chave. Apague a que sobrar no painel, se quiser.${RESET}`);
1048
+ if (revogacao && revogacao.restantes.length) {
1049
+ const n = revogacao.restantes.length;
1050
+ out.warn(`${n} chave(s) antiga(s) deste computador NAO foi/foram revogada(s) e continua(m) valendo.`);
1051
+ return fim('ok', novaIdentidade ? 'criada' : 'reaproveitada', `Infisical conectado, MAS ${n} chave(s) antiga(s) deste computador AINDA NAO foi/foram revogada(s) e continua(m) valendo. Rode de novo (sem --rotacionar) para terminar a troca; se persistir, revogue a chave antiga no painel do Infisical.`, { aviso: 'revogacao-pendente' });
1052
+ }
1053
+ if (rotacaoSemAlvo && !(revogacao && revogacao.revogadas)) {
1054
+ // Nao ha como saber qual era a chave antiga: dizer "ok" mudo deixaria a chave que vazou valendo sem ninguem saber.
1055
+ out.warn('Nao achei nenhuma chave antiga deste computador para revogar: nenhuma foi revogada.');
1056
+ return fim('ok', novaIdentidade ? 'criada' : 'reaproveitada', 'Infisical conectado e chave nova criada, MAS nao achei nenhuma chave antiga deste computador para revogar, entao NENHUMA foi revogada. Se a chave que vazou for uma antiga, ela continua valendo: apague-a no painel do Infisical (identidade Auroq, aba das chaves).', { aviso: 'chave-antiga-nao-identificada' });
1057
+ }
1058
+ const extra = extras ? { aviso: 'identidades-extras' } : {};
1059
+ return fim('ok', novaIdentidade ? 'criada' : 'reaproveitada', 'Infisical conectado. A chave do agente enxerga so o projeto Auroq.', extra);
1060
+ }
1061
+
1062
+ /**
1063
+ * Grava, le e apaga o segredo de teste num terminal novo. Repete o login recem-criado com espera (atraso do Infisical), sem
1064
+ * travar a chave. O nome do segredo e unico por execucao (`AUROQ_TESTE_<codigo>`): nunca pisa num segredo do aluno e dois
1065
+ * computadores testando juntos nao se atropelam. Depois de gravar, SEMPRE tenta apagar (mesmo se a leitura falhar).
1066
+ */
1067
+ async function freshTest(d, out, ledger = newLoginLedger()) {
1068
+ const crypto = require('crypto');
1069
+ const value = `teste-${crypto.randomBytes(12).toString('hex')}`;
1070
+ const name = `${TEST_SECRET}_${crypto.randomBytes(6).toString('hex').toUpperCase()}`;
1071
+ const codeOf = (r) => {
1072
+ const m = String((r && r.stderr) || '').match(/AUROQ_ERRO codigo=([a-z-]+)/);
1073
+ return m ? m[1] : null;
1074
+ };
1075
+ const apagar = async () => {
1076
+ let del = null;
1077
+ try {
1078
+ del = await d.runFresh(['segredo', 'apagar', name, '--cofre', 'infisical', '--confirmo']);
1079
+ } catch {
1080
+ del = null;
1081
+ }
1082
+ if (!del || !del.ok) out.warn(`Nao consegui apagar o segredo de teste ${name}; pode apagar no painel do Infisical.`);
1083
+ };
1084
+ for (let attempt = 0; attempt <= LOGIN_RETRY_WAITS_MS.length; attempt++) {
1085
+ await waitLoginRoom(d, ledger);
1086
+ const wrote = await d.runFresh(['segredo', 'gravar', name, '--cofre', 'infisical'], { input: value });
1087
+ if (wrote.ok) {
1088
+ try {
1089
+ const read = await d.runFresh(['segredo', 'ler', name, '--cofre', 'infisical']);
1090
+ if (!read.ok) return { ok: false, motivo: `nao consegui ler o segredo de teste${codeOf(read) ? ` (${codeOf(read)})` : ''}` };
1091
+ if (String(read.stdout).replace(/\r?\n$/, '') !== value) return { ok: false, motivo: 'o valor lido nao e o que foi gravado' };
1092
+ return { ok: true };
1093
+ } finally {
1094
+ await apagar();
1095
+ }
1096
+ }
1097
+ const code = codeOf(wrote);
1098
+ if (code === 'credencial-invalida' && attempt < LOGIN_RETRY_WAITS_MS.length) {
1099
+ noteLoginFailure(d, ledger);
1100
+ await d.sleep(LOGIN_RETRY_WAITS_MS[attempt]); // atraso do Infisical logo apos criar a chave
1101
+ continue;
1102
+ }
1103
+ return { ok: false, motivo: `nao consegui gravar o segredo de teste${code ? ` (${code})` : ''}` };
1104
+ }
1105
+ return { ok: false, motivo: 'o Infisical nao aceitou a chave nova a tempo' };
1106
+ }
1107
+
1108
+ /**
1109
+ * Revoga as chaves antigas (ids que ja foram marcados como DESTE computador, na hora da troca). Devolve
1110
+ * `{ revogadas, restantes }`: so conta como revogada a que o Infisical confirmou; o que falhar fica em `restantes`
1111
+ * para a proxima rodada. Chave que ja nao aparece (ja revogada, ou apagada) nao precisa de nada.
1112
+ */
1113
+ async function revokeKeys(d, userApi, identityId, ids, out, emUso = null) {
1114
+ const restantes = [];
1115
+ let revogadas = 0;
1116
+ const list = await d.ops.listClientSecrets(userApi, identityId);
1117
+ const vivasLista = list.ok ? (list.secrets || []).filter((s) => !s.revoked) : null;
1118
+ const vivas = vivasLista ? new Set(vivasLista.map((s) => s.id)) : null;
1119
+ // `emUso`: a chave que ESTA execucao reaproveitou (nao criou uma nova). Se ela mesma esta na lista a revogar (a nova foi
1120
+ // apagada por fora e so a antiga ainda entra), revoga-la deixaria o cofre sem nenhuma chave. Reconhece pelos 4 primeiros
1121
+ // caracteres, so se baterem com UMA chave viva.
1122
+ let poupada = null;
1123
+ if (emUso && vivasLista) {
1124
+ const achadas = vivasLista.filter((s) => s.prefix && s.prefix === emUso.clientSecret.slice(0, 4));
1125
+ if (achadas.length === 1) poupada = achadas[0].id;
1126
+ }
1127
+ for (const id of ids) {
1128
+ if (id === poupada) continue;
1129
+ if (vivas && !vivas.has(id)) continue;
1130
+ const r = await d.ops.revokeClientSecret(userApi, identityId, id);
1131
+ if (r && r.ok) revogadas += 1;
1132
+ else if (!(r && r.codigo === 'nao-encontrado')) restantes.push(id);
1133
+ }
1134
+ if (revogadas) out.ok(`${revogadas} chave(s) antiga(s) deste computador revogada(s)`);
1135
+ return { revogadas, restantes };
1136
+ }
1137
+
1138
+ // ─── Estado (onde o aluno esta), sem falar com o Infisical ────────────────
1139
+
1140
+ async function describeState(d) {
1141
+ const saved = d.state.read();
1142
+ const etapas = saved ? { ...saved.etapas } : emptyEtapas();
1143
+ const cfg = d.config.read();
1144
+ const cli = await d.cli.find(cfg && cfg.cli); // so roda `infisical --version`, local
1145
+ etapas.cli = cli && !cli.antigo ? 'feito' : 'pendente';
1146
+ const hasCreds = currentCreds(d).length > 0;
1147
+ const hasConfig = Boolean(cfg && cfg.projectId);
1148
+ if (!hasCreds || !hasConfig) {
1149
+ // chave apagada, computador novo ou config perdida: o que dependia dela volta a ficar pendente
1150
+ for (const e of ['identidade', 'teste', 'pronto']) etapas[e] = 'pendente';
1151
+ } else if (!saved) {
1152
+ etapas.identidade = 'feito'; // ha chave e config (outra execucao ou migracao); ainda nao provada
1153
+ }
1154
+ if (etapas.identidade !== 'feito' || etapas.teste !== 'feito') etapas.pronto = 'pendente';
1155
+ const feitas = ETAPAS.filter((e) => etapas[e] === 'feito');
1156
+ const proxima = nextStage(etapas);
1157
+ const resumo = !proxima ? 'pronto' : (feitas.length || saved ? 'em-andamento' : 'nao-iniciado');
1158
+ return {
1159
+ resumo,
1160
+ proxima,
1161
+ feitas,
1162
+ etapas,
1163
+ credencial: hasCreds ? 'configurado' : 'ausente',
1164
+ ultimo: saved && saved.ultimo ? saved.ultimo.codigo : null,
1165
+ atualizado_em: saved ? saved.atualizado_em : null,
1166
+ };
1167
+ }
1168
+
1169
+ function buildStateLine(st) {
1170
+ return `AUROQ_ESTADO infisical=${st.resumo} proxima=${st.proxima || 'nenhuma'} feitas=${st.feitas.join(',') || 'nenhuma'} credencial=${st.credencial} ultimo=${st.ultimo || 'nenhum'} conferido=nao`;
1171
+ }
1172
+
1173
+ /** Linha que o Ops le (mesmo padrao da rota A). */
1174
+ function buildResultLine(result) {
1175
+ // 800: cabe a pior mensagem (limite-plano com o detalhe de 300) + TROCA_PENDENTE_NOTA sem cortar a nota do fim
1176
+ const msg = clean(result.mensagem, 800).replace(/"/g, "'");
1177
+ const aviso = result.aviso ? ` aviso=${result.aviso}` : '';
1178
+ const url = result.url ? ` url="${clean(result.url, 300).replace(/"/g, "'")}"` : '';
1179
+ return `AUROQ_RESULTADO infisical=${result.status} codigo=${result.codigo}${aviso}${url} mensagem="${msg}"`;
1180
+ }
1181
+
1182
+ function showState(st) {
1183
+ console.log(`\n${PURPLE}${BOLD} Infisical — onde voce esta${RESET}`);
1184
+ for (const e of ETAPAS.filter((x) => x !== 'pronto')) {
1185
+ const feito = st.etapas[e] === 'feito';
1186
+ console.log(` ${feito ? `${GREEN}✓${RESET}` : `${GOLD}·${RESET}`} ${ETAPA_NOME[e]}`);
1187
+ }
1188
+ log('');
1189
+ if (st.resumo === 'pronto') log(`${GREEN}${BOLD}Tudo pronto.${RESET} ${DIM}(Nao conferi com o Infisical agora: rode sem --estado para provar.)${RESET}`);
1190
+ else log(`Proxima etapa: ${BOLD}${ETAPA_NOME[st.proxima]}${RESET}. Rode ${CYAN}npx auroq-os conectar-cofre --cofre infisical${RESET} para continuar de onde parou.`);
1191
+ log('');
1192
+ }
1193
+
1194
+ // Erros que o aluno (ou o plano dele) resolve: saem com 1, mas a telemetria nao os conta como instalacao quebrada.
1195
+ const STUDENT_ACTIONABLE = new Set(['sem-cli', 'cli-antigo', 'rede', 'login-no-navegador', 'sem-permissao', 'limite-plano', 'projeto-sem-acesso', 'credencial-indisponivel', 'arquivo-ambiente', 'sem-organizacao', 'bloqueada', 'limite', 'plano-restringe']);
1196
+
1197
+ function isStudentActionable(codigo) {
1198
+ return STUDENT_ACTIONABLE.has(codigo);
1199
+ }
1200
+
1201
+ // ─── Entrada do comando ──────────────────────────────────
1202
+
1203
+ /**
1204
+ * `auroq-os conectar-cofre --cofre infisical [--agente] [--estado] [--rotacionar] [--regiao us|eu] [--dominio URL] [--esperar N]`
1205
+ * Codigo de saida: 0 conectado OU falta uma acao do aluno (entrar no navegador: a linha AUROQ_RESULTADO diz
1206
+ * qual), 1 erro.
1207
+ */
1208
+ async function connect(options = {}) {
1209
+ const opts = options.argv ? parseArgs(options.argv) : { ...parseArgs([]), ...options };
1210
+ if (opts.estado) {
1211
+ const st = await describeState({ ...makeDefaultDeps(), ...(options.deps || {}) });
1212
+ if (opts.agente) console.log(buildStateLine(st));
1213
+ else showState(st);
1214
+ return st;
1215
+ }
1216
+ if (!opts.agente) console.log(`\n${PURPLE}${BOLD} Conectar Infisical (cofre gratis) — Auroq OS${RESET}`);
1217
+ let result;
1218
+ try {
1219
+ result = await connectAutomatic(opts, options.deps || {});
1220
+ } catch (e) {
1221
+ result = fim('erro', 'falha', `Falha inesperada: ${clean(e && e.message)}`);
1222
+ }
1223
+ if (result.status === 'aguardando') warn(result.mensagem);
1224
+ else if (result.status === 'erro') err(result.mensagem);
1225
+ if (opts.agente) console.log(buildResultLine(result));
1226
+ process.exitCode = EXIT_CODE[result.status] ?? 1;
1227
+ return result;
1228
+ }
1229
+
1230
+ module.exports = {
1231
+ connect,
1232
+ connectAutomatic,
1233
+ describeState,
1234
+ buildStateLine,
1235
+ buildResultLine,
1236
+ parseArgs,
1237
+ resolveDomain,
1238
+ isStudentActionable,
1239
+ // credenciais no computador
1240
+ persistCreds,
1241
+ readPersistedCreds,
1242
+ envFileProblem,
1243
+ upsertEnvFileContent,
1244
+ parseEnvFileCreds,
1245
+ hasDynamicCredDefinition,
1246
+ // arquivos e estado
1247
+ readConfig,
1248
+ writeConfig,
1249
+ configFile,
1250
+ readState,
1251
+ writeState,
1252
+ stateFile,
1253
+ nextStage,
1254
+ keyMarker,
1255
+ keyDescription,
1256
+ hostLabel,
1257
+ ownKeys,
1258
+ machineSeed,
1259
+ localComputerId,
1260
+ makeDefaultDeps,
1261
+ runInFreshShell,
1262
+ ETAPAS,
1263
+ ETAPA_NOME,
1264
+ CLIENT_ID_VAR,
1265
+ CLIENT_SECRET_VAR,
1266
+ PROJECT_NAME,
1267
+ PROJECT_SLUG,
1268
+ IDENTITY_NAME,
1269
+ ENV_SLUG,
1270
+ TEST_SECRET,
1271
+ };