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.
package/lib/cofre.js ADDED
@@ -0,0 +1,1453 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auroq OS — Camada comum de cofre (1Password e Infisical)
5
+ *
6
+ * O aluno escolhe UM cofre para as chaves dos agentes: o 1Password (pago, rota A, lib/onepassword.js) ou o
7
+ * Infisical (gratis, rota B, lib/infisical.js). Os agentes nao precisam saber qual e: usam
8
+ * `auroq-os segredo ler|gravar|listar|apagar|qual <ref>` e esta camada fala com o cofre certo.
9
+ *
10
+ * - `cofre.json` (em ~/.arcane, 0600, sem segredo) guarda qual cofre esta ativo;
11
+ * - backends `1password` e `infisical`: ler, gravar, listar, apagar com o MESMO formato de resposta;
12
+ * - `segredoCli`: o comando `segredo` (valor so por stdin ou arquivo, nunca em argumento; erro so no stderr);
13
+ * - `conectarCofre`: o comando `conectar-cofre` (escolha do cofre, continua a rota em andamento);
14
+ * - `migrar`: Infisical -> 1Password, sem apagar nada da origem. Ligar o 1Password sozinho NAO troca o cofre de
15
+ * quem ja usa o Infisical (as chaves ainda estao la): so a migracao troca, depois de levar tudo.
16
+ * - gravar/apagar no 1Password e o login no Infisical usam uma trava de arquivo em ~/.arcane (um comando por vez
17
+ * nesta maquina; o agente dispara comandos em paralelo).
18
+ *
19
+ * Nenhum valor de segredo, chave do agente ou token aparece em tela, log, mensagem de erro, argumento de
20
+ * comando ou arquivo de configuracao. Valor so por stdin (op), corpo de requisicao (Infisical) ou memoria.
21
+ * Todas as dependencias (`d`) sao trocaveis nos testes; os modulos pesados so carregam quando precisam.
22
+ *
23
+ * @module auroq-os/cofre
24
+ */
25
+
26
+ const crypto = require('crypto');
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const util = require('util');
30
+
31
+ const VAULT_NAME = 'Claude';
32
+ const ITEM_TAG = 'auroq-os';
33
+ const TEST_SECRET = 'AUROQ_TESTE';
34
+ // Segredo de teste da conexao: `AUROQ_TESTE` ou `AUROQ_TESTE_<letras e numeros>` (nome unico por teste). Nao e chave do aluno.
35
+ const TEST_SECRET_RE = /^AUROQ_TESTE(?:_[A-Za-z0-9]+)?$/;
36
+ const CREDENTIAL = 'credential';
37
+ const TIPOS = ['1password', 'infisical'];
38
+ const NOME_TIPO = { '1password': '1Password', infisical: 'Infisical' };
39
+ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
40
+ const MAX_VALUE_BYTES = 1024 * 1024;
41
+ const STDIN_IDLE_MS = 30000;
42
+ const OP_TIMEOUT_MS = 60000;
43
+ const LOCK_STALE_MS = 3 * 60 * 1000; // uma gravacao leva segundos: trava com mais de 3 min e de comando que morreu
44
+ const TROCA_STALE_MS = 10 * 1000; // a troca de trava velha dura milissegundos: mais de 10 s e de comando que morreu no meio
45
+ const LOCK_WAIT_MS = 60000; // quanto um comando espera a vez de gravar no 1Password
46
+ const LOGIN_LOCK_WAIT_MS = 20000; // quanto espera a vez de entrar no Infisical
47
+ const BLOQUEIO_MS = 5 * 60 * 1000; // o Infisical trava a identidade por uns 5 minutos depois de varias tentativas erradas
48
+ const NOTA_AUROQ = 'Guardado pelo Auroq OS (comando segredo). O agente le e grava aqui.';
49
+ const ESCOLHA_MSG = 'Quer o 1Password (uns 3 dolares por mes, confira o preco na tela, preenche senha no navegador, recomendado) ou o gratis (Infisical, so guarda as chaves do agente)?';
50
+
51
+ // Codigos do comando `segredo` (stderr: AUROQ_ERRO codigo=<c> mensagem="...").
52
+ // `uso` = uso errado da linha de comando (ref faltando, valor em argumento, opcao desconhecida).
53
+ const SEGREDO_CODIGOS = ['nome-invalido', 'cofre-nao-configurado', 'nao-encontrado', 'sem-valor', 'ja-existe-confirmar',
54
+ 'credencial-invalida', 'credencial-indisponivel', 'sem-permissao', 'rede', 'limite', 'bloqueada', 'falha', 'uso'];
55
+ // Saida: 0 ok, 1 erro geral, 2 uso errado, 3 nao encontrado, 4 cofre nao configurado.
56
+ const SEGREDO_EXIT = Object.freeze({ ok: 0, erro: 1, uso: 2, 'nao-encontrado': 3, 'cofre-nao-configurado': 4 });
57
+ const CODIGOS_DE_USO = new Set(['uso', 'nome-invalido', 'sem-valor', 'ja-existe-confirmar']);
58
+
59
+ function exitCodeFor(codigo) {
60
+ if (!codigo) return SEGREDO_EXIT.ok;
61
+ if (codigo === 'nao-encontrado') return SEGREDO_EXIT['nao-encontrado'];
62
+ if (codigo === 'cofre-nao-configurado') return SEGREDO_EXIT['cofre-nao-configurado'];
63
+ if (CODIGOS_DE_USO.has(codigo)) return SEGREDO_EXIT.uso;
64
+ return SEGREDO_EXIT.erro;
65
+ }
66
+
67
+ // ─── Dependencias (preguicosas) ──────────────────────────
68
+
69
+ const LAZY = {
70
+ onepassword: () => require('./onepassword'),
71
+ infisical: () => require('./infisical'),
72
+ api: () => require('./infisical-api'),
73
+ };
74
+
75
+ /** Modulo real so quando precisa; `d.<nome>` (teste) substitui o modulo INTEIRO. */
76
+ function mod(d, name) {
77
+ return (d && d[name]) || LAZY[name]();
78
+ }
79
+
80
+ function envOf(d) {
81
+ return (d && d.env) || process.env;
82
+ }
83
+
84
+ function nowOf(d) {
85
+ return d && typeof d.now === 'function' ? d.now() : Date.now();
86
+ }
87
+
88
+ // ─── Texto seguro ────────────────────────────────────────
89
+
90
+ /**
91
+ * Uma linha so, sem nada com cara de segredo. `secrets` = valores que esta execucao conhece (o valor que
92
+ * esta sendo gravado, o client secret): saem do texto antes de qualquer outra coisa.
93
+ */
94
+ function oneLine(text, max = 300, secrets = []) {
95
+ let s = String(text === undefined || text === null ? '' : text);
96
+ const known = secrets.filter((x) => typeof x === 'string' && x.length >= 6);
97
+ for (const sec of known) s = s.split(sec).join('[oculto]');
98
+ s = s
99
+ .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
100
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
101
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '')
102
+ .replace(/[\n\r\u0085\u2028\u2029\t]+/g, ' ');
103
+ for (const sec of known) s = s.split(sec.replace(/\s+/g, ' ')).join('[oculto]');
104
+ return s
105
+ .replace(/ops_[A-Za-z0-9_=+/.-]{10,}/g, 'ops_[oculto]')
106
+ .replace(/eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*/g, '[oculto]')
107
+ .replace(/\b[0-9a-fA-F]{64}\b/g, '[oculto]')
108
+ .replace(/\bst\.[A-Za-z0-9._-]{10,}/g, '[oculto]')
109
+ .replace(/\[(ERROR|WARNING)\]\s+\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s*/g, '')
110
+ .replace(/\s+/g, ' ')
111
+ .trim()
112
+ .slice(0, max);
113
+ }
114
+
115
+ function quoted(text, max = 400, secrets = []) {
116
+ return oneLine(text, max, secrets).replace(/"/g, "'");
117
+ }
118
+
119
+ function fail(codigo, mensagem) {
120
+ return { ok: false, codigo, mensagem };
121
+ }
122
+
123
+ function stripOneNewline(text) {
124
+ return String(text).replace(/\r?\n$/, '');
125
+ }
126
+
127
+ // ─── Trava de arquivo (gravacoes e logins em paralelo na mesma maquina) ───
128
+
129
+ const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
130
+
131
+ function readLockInfo(file) {
132
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
133
+ }
134
+
135
+ /** Trava velha = dono morreu, passou do prazo ou o arquivo esta estragado (vazio so conta depois de uns segundos: o dono pode estar escrevendo). */
136
+ function lockIsStale(file, staleMs) {
137
+ const info = readLockInfo(file);
138
+ if (!info || !Number.isInteger(info.pid) || info.pid <= 0 || !Number.isFinite(info.at)) {
139
+ try { return Date.now() - fs.statSync(file).mtimeMs > 5000; } catch { return true; }
140
+ }
141
+ const age = Date.now() - info.at;
142
+ if (age < -60000 || age > staleMs) return true;
143
+ try {
144
+ process.kill(info.pid, 0);
145
+ return false;
146
+ } catch (e) {
147
+ return e.code === 'ESRCH';
148
+ }
149
+ }
150
+
151
+ /** 'ok' (peguei), 'ocupada' (outro comando segura) ou 'sem-trava' (permissao ou disco: segue sem trava, como a rota A). */
152
+ function tryLock(file, nonce, staleMs) {
153
+ for (let attempt = 0; attempt < 3; attempt++) {
154
+ try {
155
+ const fd = fs.openSync(file, 'wx', 0o600);
156
+ try { fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now(), nonce })); } finally { fs.closeSync(fd); }
157
+ return 'ok';
158
+ } catch (e) {
159
+ if (e.code !== 'EEXIST') return 'sem-trava';
160
+ if (!lockIsStale(file, staleMs)) return 'ocupada';
161
+ tomarVelha(file, staleMs);
162
+ }
163
+ }
164
+ return 'ocupada';
165
+ }
166
+
167
+ /**
168
+ * Remove a trava velha SEM apagar uma trava nova: quem viu a trava velha e foi mais lento nao pode apagar a que
169
+ * outro comando acabou de criar (antes, dois comandos entravam juntos e o "um por vez" se perdia). Por isso so um
170
+ * comando por vez faz a troca (arquivo `.troca`, vida de milissegundos) e ele confere DE NOVO, ja la dentro, se a
171
+ * trava continua velha. Quem nao consegue a troca nao faz nada: tryLock tenta de novo e, se ainda houver trava,
172
+ * devolve 'ocupada'. Troca esquecida (comando morto bem nessa hora) com mais de 10 s e velha e sai.
173
+ */
174
+ function tomarVelha(file, staleMs) {
175
+ const troca = `${file}.troca`;
176
+ let fd;
177
+ try {
178
+ fd = fs.openSync(troca, 'wx', 0o600);
179
+ } catch (e) {
180
+ if (e.code === 'EEXIST') {
181
+ try { if (Date.now() - fs.statSync(troca).mtimeMs > TROCA_STALE_MS) fs.unlinkSync(troca); } catch { /* outro limpou */ }
182
+ }
183
+ return;
184
+ }
185
+ try {
186
+ fs.closeSync(fd);
187
+ fs.statSync(file); // sumiu = o dono soltou: nao ha o que tomar (lockIsStale trata "sumiu" como velha)
188
+ if (lockIsStale(file, staleMs)) fs.unlinkSync(file);
189
+ } catch { /* sumiu ou outro pegou antes */ } finally {
190
+ try { fs.unlinkSync(troca); } catch { /* ja foi */ }
191
+ }
192
+ }
193
+
194
+ function releaseLock(file, nonce) {
195
+ const info = readLockInfo(file);
196
+ if (info && info.nonce === nonce) {
197
+ try { fs.unlinkSync(file); } catch { /* ja foi */ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Roda `fn` com a trava `file` (UMA execucao por vez nesta maquina), ESPERANDO a vez ate `waitMs`.
203
+ * Acabou o prazo: `onTimeout()` decide o resultado; sem `onTimeout`, roda `fn` mesmo assim (nunca trava o aluno).
204
+ */
205
+ async function withFileLock(file, fn, { waitMs = LOCK_WAIT_MS, staleMs = LOCK_STALE_MS, onTimeout = null } = {}) {
206
+ const nonce = crypto.randomBytes(8).toString('hex');
207
+ const ate = Date.now() + waitMs;
208
+ let got = tryLock(file, nonce, staleMs);
209
+ while (got === 'ocupada' && Date.now() < ate) {
210
+ await sleep(40 + Math.floor(Math.random() * 80));
211
+ got = tryLock(file, nonce, staleMs);
212
+ }
213
+ if (got === 'ocupada' && onTimeout) return onTimeout();
214
+ try {
215
+ return await fn();
216
+ } finally {
217
+ if (got === 'ok') releaseLock(file, nonce);
218
+ }
219
+ }
220
+
221
+ /** Valor a gravar sem BOM do Windows, sem a quebra de linha final e sem espaco nas pontas (credencial nao tem). */
222
+ function cleanValue(text) {
223
+ return String(text).replace(/^\uFEFF/, '').replace(/(\r\n|\n|\r)$/, '').trim();
224
+ }
225
+
226
+ // ─── cofre.json (qual cofre esta ativo) ──────────────────
227
+
228
+ function dirOf(d) {
229
+ if (d && d.dir) {
230
+ try { fs.mkdirSync(d.dir, { recursive: true, mode: 0o700 }); } catch { /* quem escreve trata */ }
231
+ return d.dir;
232
+ }
233
+ return mod(d, 'onepassword').auroqDir();
234
+ }
235
+
236
+ function cofreFile(d) {
237
+ return path.join(dirOf(d), 'cofre.json');
238
+ }
239
+
240
+ /** `cofre.json` valido ({versao, tipo, anterior, atualizado_em}) ou null. Nunca lanca. */
241
+ function readCofre(d) {
242
+ try {
243
+ const data = JSON.parse(fs.readFileSync(cofreFile(d), 'utf8'));
244
+ if (!data || typeof data !== 'object' || data.versao !== 1 || !TIPOS.includes(data.tipo)) return null;
245
+ return {
246
+ versao: 1,
247
+ tipo: data.tipo,
248
+ anterior: TIPOS.includes(data.anterior) ? data.anterior : null,
249
+ atualizado_em: data.atualizado_em || null,
250
+ };
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Marca o cofre ativo. Mesmo cofre de antes = nao mexe no arquivo. `anterior` = o cofre que estava ativo
258
+ * antes (ou o que a migracao informa). Lanca se nao conseguir gravar: quem chama decide se isso importa.
259
+ */
260
+ function registrar(tipo, options = {}, d = {}) {
261
+ if (!TIPOS.includes(tipo)) throw new Error('tipo de cofre invalido');
262
+ const prev = readCofre(d);
263
+ const opts = options && typeof options === 'object' ? options : {};
264
+ if (prev && prev.tipo === tipo && opts.anterior === undefined) return prev;
265
+ let anterior = null;
266
+ if (opts.anterior !== undefined) anterior = TIPOS.includes(opts.anterior) ? opts.anterior : null;
267
+ else if (prev) anterior = prev.tipo !== tipo ? prev.tipo : prev.anterior;
268
+ const data = { versao: 1, tipo, anterior, atualizado_em: new Date(nowOf(d)).toISOString() };
269
+ mod(d, 'onepassword').writeFileAtomic(cofreFile(d), `${JSON.stringify(data, null, 2)}\n`);
270
+ return data;
271
+ }
272
+
273
+ const AVISO_MANTIDO = 'O 1Password ficou conectado, mas o cofre ativo continua o gratis (Infisical) porque as chaves ainda estao la. Quem troca para o 1Password e a migracao: rode "npx auroq-os conectar-cofre --migrar" (com 0 chaves tambem serve). Ate la, o segredo ler/gravar continua no Infisical.';
274
+ const AVISO_CHAVES_ANTIGAS = 'As chaves que ja estavam no 1Password continuam la e NAO foram levadas para o Infisical (nada foi copiado: o Infisical so tem o que ja foi guardado nele). Para ver quais sao: segredo listar --cofre 1password. Para usar uma antiga sem o valor passar pela conversa: segredo ler NOME --cofre 1password, sempre dentro de $(...) ou por pipe, nunca sozinho. Levar para o gratis nao e automatico.';
275
+
276
+ /** Linha de aviso (nao e erro): para o agente `AUROQ_AVISO ...`, para a pessoa o texto puro. */
277
+ function avisoLinha(agente, atributos, mensagem) {
278
+ return agente ? `AUROQ_AVISO ${atributos} mensagem="${quoted(mensagem)}"` : mensagem;
279
+ }
280
+
281
+ /**
282
+ * Depois de uma conexao: 'registrado' (cofre ativo marcado), 'mantido' (o Infisical esta ativo e pronto: ligar o
283
+ * 1Password NAO troca de cofre, so a migracao troca, depois de levar as chaves) ou 'nao' (a conexao nao terminou ok).
284
+ * `argv` = o que o aluno digitou: o modo manual do 1Password termina ok sem devolver resultado.
285
+ */
286
+ async function avaliarRegistro(result, tipo, d, argv) {
287
+ try {
288
+ const manual = result === undefined && tipo === '1password' && Array.isArray(argv) && argv.includes('--manual');
289
+ if (!manual && !(result && result.status === 'ok')) return 'nao';
290
+ const atual = readCofre(d);
291
+ if (tipo === '1password' && atual && atual.tipo === 'infisical' && (await backends(d).infisical.pronto())) return 'mantido';
292
+ registrar(tipo, {}, d);
293
+ return 'registrado';
294
+ } catch {
295
+ return 'nao'; // o registro e conforto
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Registra o cofre quando a conexao terminou `ok`. Nunca lanca e nunca falha a conexao. Devolve true so se
301
+ * registrou. Se o Infisical esta ativo e pronto, ligar o 1Password nao troca o cofre: avisa (`d.print`).
302
+ */
303
+ async function registrarSeOk(result, tipo = '1password', d = {}, argv = []) {
304
+ const r = await avaliarRegistro(result, tipo, d, argv);
305
+ if (r === 'mantido') {
306
+ try {
307
+ const print = (d && d.print) || ((line) => console.log(line));
308
+ print(avisoLinha(Array.isArray(argv) && argv.includes('--agente'), 'cofre=infisical mantido=sim', AVISO_MANTIDO));
309
+ } catch { /* o aviso e conforto */ }
310
+ }
311
+ return r === 'registrado';
312
+ }
313
+
314
+ // ─── Referencias e nomes ─────────────────────────────────
315
+
316
+ /**
317
+ * Nome de item ou de campo. `__` separa item e campo na chave do Infisical, entao nao pode aparecer dentro de
318
+ * um nome; e um nome terminado em `_` colaria o `_` ao separador (`A_` + `__` + `B` = `A___B`) e a volta
319
+ * (`refFromKey`) perderia o nome. Por isso termina em letra, numero, `.` ou `-`.
320
+ */
321
+ function validName(s) {
322
+ return typeof s === 'string' && NAME_RE.test(s) && !s.includes('__') && !s.endsWith('_');
323
+ }
324
+
325
+ function normCampo(campo) {
326
+ return String(campo).toLowerCase() === CREDENTIAL ? CREDENTIAL : campo;
327
+ }
328
+
329
+ /**
330
+ * `op://Claude/ITEM[/CAMPO]`, `cofre://ITEM[/CAMPO]` ou `ITEM[/CAMPO]` -> { item, campo } (campo padrao
331
+ * `credential`) ou null (`nome-invalido`). Outro cofre que nao o Claude e referencia com 3+ partes: null.
332
+ */
333
+ function parseRef(texto) {
334
+ if (typeof texto !== 'string') return null;
335
+ let t = texto.trim();
336
+ if (!t) return null;
337
+ let m = t.match(/^op:\/\/(.*)$/i);
338
+ if (m) {
339
+ const partes = m[1].split('/');
340
+ if (partes.length < 2 || partes.length > 3) return null;
341
+ if (partes[0].toLowerCase() !== VAULT_NAME.toLowerCase()) return null;
342
+ t = partes.slice(1).join('/');
343
+ } else {
344
+ m = t.match(/^cofre:\/\/(.*)$/i);
345
+ if (m) t = m[1];
346
+ }
347
+ const partes = t.split('/');
348
+ if (partes.length > 2) return null;
349
+ const item = partes[0];
350
+ const campo = partes.length === 2 ? partes[1] : CREDENTIAL;
351
+ if (!validName(item) || !validName(campo)) return null;
352
+ return { item, campo: normCampo(campo) };
353
+ }
354
+
355
+ function toRef(x) {
356
+ if (x && typeof x === 'object' && typeof x.item === 'string') {
357
+ const campo = x.campo === undefined || x.campo === null ? CREDENTIAL : x.campo;
358
+ return validName(x.item) && validName(campo) ? { item: x.item, campo: normCampo(campo) } : null;
359
+ }
360
+ return parseRef(x);
361
+ }
362
+
363
+ /** Chave do segredo no Infisical: `ITEM` se o campo e `credential`, senao `ITEM__CAMPO`. */
364
+ function infisicalKey(ref) {
365
+ const r = toRef(ref);
366
+ if (!r) return null;
367
+ return r.campo === CREDENTIAL ? r.item : `${r.item}__${r.campo}`;
368
+ }
369
+
370
+ /** Volta da chave para { item, campo }. Chave que nao segue o formato: null. */
371
+ function refFromKey(key) {
372
+ if (typeof key !== 'string' || !key) return null;
373
+ const i = key.indexOf('__');
374
+ const item = i === -1 ? key : key.slice(0, i);
375
+ const campo = i === -1 ? CREDENTIAL : key.slice(i + 2);
376
+ if (!validName(item) || !validName(campo)) return null;
377
+ return { item, campo: normCampo(campo) };
378
+ }
379
+
380
+ function refText(ref) {
381
+ return ref.campo === CREDENTIAL ? ref.item : `${ref.item}/${ref.campo}`;
382
+ }
383
+
384
+ function opReference(ref) {
385
+ return `op://${VAULT_NAME}/${ref.item}/${ref.campo}`;
386
+ }
387
+
388
+ // ─── Mensagens por codigo ────────────────────────────────
389
+
390
+ function messageFor(codigo, tipo, detalhe = '') {
391
+ const nome = NOME_TIPO[tipo] || 'cofre';
392
+ switch (codigo) {
393
+ case 'cofre-nao-configurado':
394
+ return tipo
395
+ ? `O ${nome} nao esta conectado neste computador (ou o cofre ${VAULT_NAME} nao esta visivel para a chave). Rode: npx auroq-os conectar-cofre --cofre ${tipo}`
396
+ : 'Nenhum cofre conectado neste computador. Rode: npx auroq-os conectar-cofre';
397
+ case 'nao-encontrado':
398
+ return `Nao achei esse segredo no ${nome}.`;
399
+ case 'credencial-invalida': {
400
+ const recente = tipo === 'infisical' ? 'Se a chave acabou de ser criada, espere um minuto e tente de novo. ' : '';
401
+ return `O ${nome} recusou a chave deste computador (revogada ou invalida). ${recente}Para gerar outra, rode: npx auroq-os conectar-cofre --cofre ${tipo} --rotacionar`;
402
+ }
403
+ case 'credencial-indisponivel':
404
+ return `Nao consegui usar a chave deste computador no ${nome} agora. Tente de novo em alguns minutos.`;
405
+ case 'sem-permissao':
406
+ return `A chave do agente nao tem permissao para isso no ${nome}.`;
407
+ case 'rede':
408
+ return `Sem conexao com o ${nome} agora.`;
409
+ case 'limite':
410
+ return `O ${nome} limitou os acessos por enquanto (limite de uso). Tente de novo em alguns minutos.`;
411
+ case 'bloqueada':
412
+ return `O ${nome} bloqueou a chave por alguns minutos (muitas tentativas erradas). Espere uns 5 minutos e tente de novo, sem repetir antes.`;
413
+ default:
414
+ return `Resposta inesperada do ${nome}.${detalhe ? ` ${detalhe}` : ''}`;
415
+ }
416
+ }
417
+
418
+ function failure(codigo, tipo, detalhe) {
419
+ return fail(codigo, messageFor(codigo, tipo, detalhe));
420
+ }
421
+
422
+ // ─── Backend 1Password ───────────────────────────────────
423
+
424
+ const OP_NOT_VAULT_RE = /(isn't a vault|is not a vault|vault[^.\n]{0,40}(?:not found|could not be found)|no vault)/i;
425
+ const OP_NOT_FOUND_RE = /(isn't an item|is not an item|isn't a field|is not a field|no item|item[^.\n]{0,40}(?:not found|could not be found)|could not be found|no field)/i;
426
+ const OP_AMBIGUOUS_RE = /more than one item/i;
427
+ const OP_PERMISSION_RE = /(forbidden|permission|not allowed|insufficient|denied|\b403\b)/i;
428
+
429
+ /** Falha de um comando do `op` -> { codigo, detalhe }. O `op` fala ingles; nome de item vem entre aspas, por isso "nao achei" vem antes. */
430
+ function classifyOp(d, r, secrets = []) {
431
+ const o = mod(d, 'onepassword');
432
+ const stderr = String((r && r.stderr) || '');
433
+ if (r && r.missing) return { codigo: 'cofre-nao-configurado', detalhe: '' };
434
+ if (OP_NOT_VAULT_RE.test(stderr)) return { codigo: 'cofre-nao-configurado', detalhe: '' };
435
+ if (OP_AMBIGUOUS_RE.test(stderr)) return { codigo: 'falha', detalhe: `Tem mais de um item com esse nome no cofre ${VAULT_NAME}; apague ou renomeie o repetido.` };
436
+ if (OP_NOT_FOUND_RE.test(stderr)) return { codigo: 'nao-encontrado', detalhe: '' };
437
+ const c = o.classifyTokenFailure(r);
438
+ if (c === 'tempo' || c === 'rede') return { codigo: 'rede', detalhe: '' };
439
+ if (c === 'limite') return { codigo: 'limite', detalhe: '' };
440
+ if (c === 'token-invalido') return { codigo: 'credencial-invalida', detalhe: '' };
441
+ if (OP_PERMISSION_RE.test(stderr)) return { codigo: 'sem-permissao', detalhe: '' };
442
+ const base = o.classifyFailure(r);
443
+ if (base === 'sem-sessao' || base === 'sessao') return { codigo: 'credencial-indisponivel', detalhe: '' };
444
+ return { codigo: 'falha', detalhe: oneLine(stderr, 200, secrets) };
445
+ }
446
+
447
+ function itemTemplate(ref, valor) {
448
+ const extra = ref.campo !== CREDENTIAL;
449
+ const empty = (id, type, label) => ({ id, type, label, value: '' });
450
+ const fields = [
451
+ { id: 'notesPlain', type: 'STRING', purpose: 'NOTES', label: 'notesPlain', value: NOTA_AUROQ },
452
+ empty('username', 'STRING', 'username'),
453
+ { id: CREDENTIAL, type: 'CONCEALED', label: CREDENTIAL, value: extra ? '' : valor },
454
+ empty('type', 'MENU', 'type'),
455
+ empty('filename', 'STRING', 'filename'),
456
+ empty('validFrom', 'DATE', 'valid from'),
457
+ empty('expires', 'DATE', 'expires'),
458
+ empty('hostname', 'STRING', 'hostname'),
459
+ ];
460
+ if (extra) fields.push({ type: 'CONCEALED', label: ref.campo, value: valor });
461
+ return { title: ref.item, category: 'API_CREDENTIAL', fields };
462
+ }
463
+
464
+ function findField(fields, campo) {
465
+ const want = String(campo).toLowerCase();
466
+ if (campo === CREDENTIAL) {
467
+ return fields.find((f) => f && f.id === CREDENTIAL) || fields.find((f) => f && String(f.label || '').toLowerCase() === CREDENTIAL);
468
+ }
469
+ return fields.find((f) => f && String(f.label || '').toLowerCase() === want);
470
+ }
471
+
472
+ function makeOpBackend(d) {
473
+ const tipo = '1password';
474
+
475
+ async function token() {
476
+ const o = mod(d, 'onepassword');
477
+ const valid = (t) => (t && o.isValidToken(String(t).trim()) ? String(t).trim() : null);
478
+ const fromEnv = valid(envOf(d)[o.TOKEN_VAR]);
479
+ if (fromEnv) return fromEnv;
480
+ try { return valid(await (d.readOpToken ? d.readOpToken() : o.readPersistedToken())); } catch { return null; }
481
+ }
482
+
483
+ async function call(tk, args, input = '') {
484
+ const o = mod(d, 'onepassword');
485
+ const run = d.op || o.runOp;
486
+ // `isolate`: o agente nunca pode cair na janela da digital do app; vale so a chave
487
+ const env = o.opEnv(String(tk).trim(), { isolate: true });
488
+ // a chave do Infisical (ID e segredo) nao tem nada a ver com o `op`: nao vai para o ambiente dele
489
+ for (const k of Object.keys(env)) {
490
+ if (/^AUROQ_INFISICAL_/i.test(k)) delete env[k];
491
+ }
492
+ return run(args, { env, input, timeoutMs: OP_TIMEOUT_MS });
493
+ }
494
+
495
+ const semChave = () => failure('cofre-nao-configurado', tipo);
496
+
497
+ /** Gravar e apagar leem o item, mudam e gravam de volta: dois comandos juntos criariam item repetido ou perderiam um campo. Um por vez. */
498
+ const comTrava = (fn) => withFileLock(path.join(dirOf(d), 'cofre-op.lock'), fn, {
499
+ waitMs: d.lockWaitMs === undefined ? LOCK_WAIT_MS : d.lockWaitMs,
500
+ onTimeout: () => fail('falha', 'Outro comando esta gravando no 1Password agora. Tente de novo em alguns instantes.'),
501
+ });
502
+
503
+ async function run(args, input, secrets) {
504
+ const tk = await token();
505
+ if (!tk) return { erro: semChave() };
506
+ const r = await call(tk, args, input);
507
+ if (r.ok) return { r, tk };
508
+ const c = classifyOp(d, r, secrets);
509
+ return { erro: failure(c.codigo, tipo, c.detalhe) };
510
+ }
511
+
512
+ const backend = {
513
+ tipo,
514
+ nome: NOME_TIPO[tipo],
515
+ async pronto() {
516
+ return Boolean(await token());
517
+ },
518
+ async ler(ref) {
519
+ const x = await run(['read', opReference(ref)], '', []);
520
+ if (x.erro) return x.erro;
521
+ return { ok: true, valor: stripOneNewline(x.r.stdout) };
522
+ },
523
+ async gravar(ref, valor) {
524
+ return comTrava(async () => {
525
+ const secrets = [valor];
526
+ const got = await run(['item', 'get', ref.item, '--vault', VAULT_NAME, '--format', 'json'], '', secrets);
527
+ let item = null;
528
+ if (got.erro) {
529
+ if (got.erro.codigo !== 'nao-encontrado') return got.erro;
530
+ } else {
531
+ try { item = JSON.parse(got.r.stdout); } catch { item = null; }
532
+ if (!item || typeof item !== 'object' || !Array.isArray(item.fields)) return failure('falha', tipo, 'Nao entendi o item que ja existe no cofre.');
533
+ }
534
+ let w;
535
+ if (!item) {
536
+ w = await run(['item', 'create', '-', '--vault', VAULT_NAME, '--tags', ITEM_TAG], JSON.stringify(itemTemplate(ref, valor)), secrets);
537
+ } else {
538
+ // item que ja existe (pode ser do aluno, com varios campos): muda ou insere SO o campo pedido
539
+ const campo = findField(item.fields, ref.campo);
540
+ if (campo) campo.value = valor;
541
+ else item.fields.push(ref.campo === CREDENTIAL ? { id: CREDENTIAL, type: 'CONCEALED', label: CREDENTIAL, value: valor } : { type: 'CONCEALED', label: ref.campo, value: valor });
542
+ w = await run(['item', 'edit', ref.item, '--vault', VAULT_NAME], JSON.stringify(item), secrets);
543
+ }
544
+ if (w.erro) return w.erro;
545
+ return { ok: true, criado: !item };
546
+ });
547
+ },
548
+ async listar() {
549
+ const x = await run(['item', 'list', '--vault', VAULT_NAME, '--format', 'json'], '', []);
550
+ if (x.erro) return x.erro;
551
+ let list = null;
552
+ try { list = JSON.parse(x.r.stdout); } catch { list = null; }
553
+ if (!Array.isArray(list)) return failure('falha', tipo, 'Nao entendi a lista de itens do cofre.');
554
+ const nomes = [...new Set(list.map((i) => oneLine(i && i.title, 120)).filter(Boolean))].sort();
555
+ return { ok: true, nomes };
556
+ },
557
+ /**
558
+ * { ok, existe, campos }. `existe` confere o item e, se o campo pedido nao e o `credential`, o campo tambem.
559
+ * `campos` = rotulos dos campos do item que TEM valor (nunca os valores): e o que o arquivamento leva junto.
560
+ */
561
+ async existe(ref) {
562
+ const l = await backend.listar();
563
+ if (!l.ok) return l;
564
+ if (!l.nomes.some((n) => n.toLowerCase() === ref.item.toLowerCase())) return { ok: true, existe: false, campos: [] };
565
+ const got = await run(['item', 'get', ref.item, '--vault', VAULT_NAME, '--format', 'json'], '', []);
566
+ if (got.erro) return got.erro.codigo === 'nao-encontrado' ? { ok: true, existe: false, campos: [] } : got.erro;
567
+ let item = null;
568
+ try { item = JSON.parse(got.r.stdout); } catch { item = null; }
569
+ if (!item || typeof item !== 'object' || !Array.isArray(item.fields)) return failure('falha', tipo, 'Nao entendi o item que ja existe no cofre.');
570
+ const campos = item.fields
571
+ .filter((f) => f && typeof f.value === 'string' && f.value !== '' && f.value !== NOTA_AUROQ)
572
+ .map((f) => oneLine(f.label || f.id, 60))
573
+ .filter(Boolean);
574
+ const achou = ref.campo === CREDENTIAL ? true : Boolean(findField(item.fields, ref.campo));
575
+ return { ok: true, existe: achou, campos: [...new Set(campos)] };
576
+ },
577
+ async apagar(ref) {
578
+ // no 1Password o item e a unidade: arquiva o item inteiro (volta pelo Arquivo do app, nao se perde)
579
+ return comTrava(async () => {
580
+ if (ref.campo !== CREDENTIAL) {
581
+ // campo que nao existe nao arquiva nada (antes, o erro de digitacao no campo arquivava o item inteiro)
582
+ const e = await backend.existe(ref);
583
+ if (!e.ok) return e;
584
+ if (!e.existe) return failure('nao-encontrado', tipo);
585
+ }
586
+ const x = await run(['item', 'delete', ref.item, '--vault', VAULT_NAME, '--archive'], '', []);
587
+ if (x.erro) return x.erro;
588
+ return { ok: true };
589
+ });
590
+ },
591
+ };
592
+ return backend;
593
+ }
594
+
595
+ // ─── Backend Infisical ───────────────────────────────────
596
+
597
+ const API_PARA_SEGREDO = {
598
+ rede: 'rede',
599
+ tempo: 'rede',
600
+ servidor: 'rede',
601
+ limite: 'limite',
602
+ 'limite-plano': 'limite',
603
+ bloqueada: 'bloqueada',
604
+ 'credencial-invalida': 'credencial-invalida',
605
+ 'sessao-expirada': 'credencial-indisponivel',
606
+ 'plano-restringe': 'sem-permissao',
607
+ 'sem-permissao': 'sem-permissao',
608
+ 'nao-encontrado': 'nao-encontrado',
609
+ };
610
+
611
+ // Cabe inteira na linha AUROQ_RESULTADO (o texto da linha corta em 400 caracteres): o comando e o "rode de novo" ficam de fora se passar.
612
+ const MSG_ORIGEM_MORTA = 'O Infisical recusou a chave deste computador (revogada, ou a identidade ou o projeto foi apagado), entao esperar nao resolve. Gere a chave de novo: npx auroq-os conectar-cofre --cofre infisical --agente (entre na sua conta do Infisical; se ela foi apagada, crie outra, e gratis). Depois rode a migracao de novo: o que ainda estiver la passa; sem nada la, o 1Password vira o cofre ativo.';
613
+
614
+ // O plano gratis barrou a acao (nao e a chave sem direito, nem falta de vaga): a frase padrao de 'sem-permissao'
615
+ // faria o aluno mexer na chave ou apagar coisa no painel sem necessidade.
616
+ const MSG_PLANO_RESTRINGE = 'O plano gratis do Infisical nao permitiu essa acao. Isso NAO e falta de vaga: nao apague nada no painel por causa disso. Nao da para contornar daqui; se o 1Password for uma opcao para voce, use ele como cofre, ou peca ajuda ao Arcano.';
617
+
618
+ function makeInfisicalBackend(d) {
619
+ const tipo = 'infisical';
620
+ let session = null; // { client, config } depois do login (so em memoria; o token nunca vai para disco)
621
+ let loginErro = null; // chave morta ou bloqueada neste processo: nao insiste (cada tentativa errada conta para a trava do Infisical)
622
+
623
+ // Valor de verdade (id/segredo) e nao uma linha do aluno que BUSCA a chave de outro jeito (ex.: $(security find-generic-password ...))
624
+ const literal = (v) => typeof v === 'string' && /^[A-Za-z0-9._~+/=-]+$/.test(v.trim());
625
+
626
+ /**
627
+ * Chaves para tentar, na ordem. A gravada vem primeiro: `--rotacionar` e a regeneracao so atualizam o arquivo,
628
+ * e um terminal/Claude aberto antes ainda tem a chave velha (revogada) no ambiente; cada login errado conta
629
+ * para a trava de 5 minutos da identidade (que vale para todos os computadores do aluno).
630
+ */
631
+ async function creds() {
632
+ const inf = mod(d, 'infisical');
633
+ const env = envOf(d);
634
+ const out = [];
635
+ const add = (id, secret) => {
636
+ const clientId = typeof id === 'string' ? id.trim() : '';
637
+ const clientSecret = typeof secret === 'string' ? secret.trim() : '';
638
+ if (clientId && clientSecret && !out.some((c) => c.clientId === clientId && c.clientSecret === clientSecret)) out.push({ clientId, clientSecret });
639
+ };
640
+ let persisted = null;
641
+ try { persisted = await inf.readPersistedCreds(); } catch { persisted = null; }
642
+ if (persisted && literal(persisted.clientId) && literal(persisted.clientSecret)) add(persisted.clientId, persisted.clientSecret);
643
+ add(env[inf.CLIENT_ID_VAR], env[inf.CLIENT_SECRET_VAR]);
644
+ return out;
645
+ }
646
+
647
+ function config() {
648
+ const inf = mod(d, 'infisical');
649
+ let cfg = null;
650
+ try { cfg = inf.readConfig(); } catch { cfg = null; }
651
+ return cfg && typeof cfg === 'object' && cfg.projectId ? cfg : null;
652
+ }
653
+
654
+ function mapFail(r, secrets = []) {
655
+ const codigo = API_PARA_SEGREDO[r && r.codigo] || 'falha';
656
+ if (r && r.codigo === 'plano-restringe') return fail(codigo, MSG_PLANO_RESTRINGE);
657
+ const detalhe = codigo === 'falha' ? oneLine(r && r.mensagem, 200, secrets) : '';
658
+ return failure(codigo, tipo, detalhe);
659
+ }
660
+
661
+ // Marca (sem segredo: so a identidade e ate quando) de que o Infisical bloqueou a identidade: os outros comandos
662
+ // deste computador nem tentam entrar enquanto durar, em vez de repetir o erro que a mantem bloqueada.
663
+ const bloqueioFile = () => path.join(dirOf(d), 'cofre-infisical-bloqueio.json');
664
+ function bloqueioAte(clientIds) {
665
+ try {
666
+ const m = JSON.parse(fs.readFileSync(bloqueioFile(), 'utf8'));
667
+ if (m && m.versao === 1 && clientIds.includes(m.clientId) && Number.isFinite(m.ate) && m.ate > nowOf(d)) return m.ate;
668
+ } catch { /* sem marca */ }
669
+ return 0;
670
+ }
671
+ function marcarBloqueio(clientId) {
672
+ try { mod(d, 'onepassword').writeFileAtomic(bloqueioFile(), `${JSON.stringify({ versao: 1, clientId, ate: nowOf(d) + BLOQUEIO_MS })}\n`); } catch { /* conforto */ }
673
+ }
674
+ function limparBloqueio() {
675
+ try { fs.unlinkSync(bloqueioFile()); } catch { /* nao tinha */ }
676
+ }
677
+
678
+ /** Entra com a chave deste computador (so uma rodada por processo e uma por vez na maquina). Tenta a seguinte so se a anterior morreu. */
679
+ async function entrar(list, cfg, baseUrl) {
680
+ const api = mod(d, 'api');
681
+ const secrets = list.map((c) => c.clientSecret);
682
+ const ate = bloqueioAte(list.map((c) => c.clientId));
683
+ if (ate) {
684
+ const min = Math.max(1, Math.ceil((ate - nowOf(d)) / 60000));
685
+ return fail('bloqueada', `${messageFor('bloqueada', tipo)} (falta uns ${min} minuto(s))`);
686
+ }
687
+ let last = null;
688
+ for (const c of list) {
689
+ const r = await api.ops.universalLogin(api.createClient({ baseUrl }), { clientId: c.clientId, clientSecret: c.clientSecret });
690
+ if (r && r.ok && r.accessToken) {
691
+ limparBloqueio();
692
+ session = { client: api.createClient({ baseUrl, token: r.accessToken }), config: cfg };
693
+ return { ok: true };
694
+ }
695
+ last = r || { codigo: 'falha' };
696
+ if (last.codigo === 'bloqueada') marcarBloqueio(c.clientId);
697
+ if (last.codigo !== 'credencial-invalida') break; // so a chave morta justifica tentar a outra (cada erro conta para a trava)
698
+ }
699
+ return mapFail(last, secrets);
700
+ }
701
+
702
+ /** Login da identidade (so com a chave deste computador). */
703
+ async function login() {
704
+ if (session) return { ok: true };
705
+ if (loginErro) return loginErro;
706
+ const cfg = config();
707
+ const list = await creds();
708
+ if (!cfg || !list.length) return failure('cofre-nao-configurado', tipo);
709
+ const api = mod(d, 'api');
710
+ const baseUrl = api.normalizeDomain(cfg.dominio || 'us');
711
+ if (!baseUrl) return failure('falha', tipo, 'O endereco do Infisical gravado neste computador e invalido.');
712
+ // varios comandos juntos (o agente dispara em paralelo) entram um de cada vez: um erro seguido de acerto nao soma na trava
713
+ const r = await withFileLock(path.join(dirOf(d), 'cofre-infisical-login.lock'), () => entrar(list, cfg, baseUrl), {
714
+ waitMs: d.loginLockWaitMs === undefined ? LOGIN_LOCK_WAIT_MS : d.loginLockWaitMs,
715
+ });
716
+ if (!r.ok && (r.codigo === 'credencial-invalida' || r.codigo === 'bloqueada')) loginErro = r;
717
+ return r;
718
+ }
719
+
720
+ /** Roda uma operacao com a sessao; se o Infisical diz que a sessao expirou, entra de novo UMA vez. */
721
+ async function withSession(fn, secrets = []) {
722
+ for (let attempt = 0; attempt < 2; attempt++) {
723
+ const l = await login();
724
+ if (!l.ok) return l;
725
+ const r = await fn(session.client, session.config);
726
+ if (r && !r.ok && r.codigo === 'sessao-expirada' && attempt === 0) {
727
+ session = null;
728
+ continue;
729
+ }
730
+ return r && r.ok ? r : mapFail(r, secrets);
731
+ }
732
+ return failure('credencial-indisponivel', tipo);
733
+ }
734
+
735
+ const scope = (cfg) => ({ projectId: cfg.projectId, environment: cfg.env || 'prod' });
736
+
737
+ const backend = {
738
+ tipo,
739
+ nome: NOME_TIPO[tipo],
740
+ async pronto() {
741
+ if (!config()) return false;
742
+ const inf = mod(d, 'infisical');
743
+ const env = envOf(d);
744
+ const doAmbiente = (v) => typeof v === 'string' && v.trim() !== '';
745
+ if (doAmbiente(env[inf.CLIENT_ID_VAR]) && doAmbiente(env[inf.CLIENT_SECRET_VAR])) return true; // sem ler a chave gravada (no Windows isso abre o PowerShell)
746
+ return (await creds()).length > 0;
747
+ },
748
+ async ler(ref) {
749
+ const name = infisicalKey(ref);
750
+ const r = await withSession((client, cfg) => mod(d, 'api').ops.readSecret(client, { ...scope(cfg), name }));
751
+ if (!r.ok) return r;
752
+ return { ok: true, valor: typeof r.value === 'string' ? r.value : '' };
753
+ },
754
+ async gravar(ref, valor) {
755
+ const name = infisicalKey(ref);
756
+ const r = await withSession((client, cfg) => mod(d, 'api').ops.writeSecret(client, { ...scope(cfg), name, value: valor }), [valor]);
757
+ if (!r.ok) return r;
758
+ return { ok: true, criado: Boolean(r.criado) };
759
+ },
760
+ /** Chaves como estao no Infisical (nomes, nunca valores). */
761
+ async chaves() {
762
+ const r = await withSession((client, cfg) => mod(d, 'api').ops.listSecretNames(client, scope(cfg)));
763
+ if (!r.ok) return r;
764
+ return { ok: true, chaves: [...new Set(Array.isArray(r.names) ? r.names.map(String) : [])].sort() };
765
+ },
766
+ async listar() {
767
+ const c = await backend.chaves();
768
+ if (!c.ok) return c;
769
+ const nomes = c.chaves.map((k) => {
770
+ const ref = refFromKey(k);
771
+ return ref && infisicalKey(ref) === k ? refText(ref) : oneLine(k, 120);
772
+ });
773
+ return { ok: true, nomes: [...new Set(nomes)].sort() };
774
+ },
775
+ async existe(ref) {
776
+ const c = await backend.chaves();
777
+ if (!c.ok) return c;
778
+ return { ok: true, existe: c.chaves.includes(infisicalKey(ref)) };
779
+ },
780
+ async apagar(ref) {
781
+ const name = infisicalKey(ref);
782
+ const r = await withSession((client, cfg) => mod(d, 'api').ops.deleteSecret(client, { ...scope(cfg), name }));
783
+ return r.ok ? { ok: true } : r;
784
+ },
785
+ };
786
+ return backend;
787
+ }
788
+
789
+ /** { '1password': backend, infisical: backend }. Criar nao toca em disco nem em rede; cada backend guarda a sua sessao. */
790
+ function backends(d = {}) {
791
+ return { '1password': makeOpBackend(d), infisical: makeInfisicalBackend(d) };
792
+ }
793
+
794
+ // ─── Qual cofre ──────────────────────────────────────────
795
+
796
+ /**
797
+ * Cofre ativo. `cofre.json` manda; sem o arquivo (aluno que conectou antes dele existir): chave do 1Password
798
+ * no ambiente ou gravada => 1password; senao credenciais do Infisical + infisical.json => infisical; senao null.
799
+ */
800
+ async function detectarTipo(d = {}) {
801
+ const c = readCofre(d);
802
+ if (c) return c.tipo;
803
+ const b = backends(d);
804
+ if (await b['1password'].pronto()) return '1password';
805
+ if (await b.infisical.pronto()) return 'infisical';
806
+ return null;
807
+ }
808
+
809
+ /**
810
+ * Nome de cofre para repetir na mensagem de erro: so se parece nome de cofre (letras minusculas, ate 12). O argumento
811
+ * pode ser o proprio valor do segredo (agente que errou a ordem); mensagem de erro nunca repete texto livre.
812
+ */
813
+ function eco(texto) {
814
+ return typeof texto === 'string' && /^[a-z][a-z-]{1,11}$/.test(texto) ? ` "${texto}"` : '';
815
+ }
816
+
817
+ async function resolveBackend(opts, d) {
818
+ const o = opts && typeof opts === 'object' ? opts : {};
819
+ const b = backends(d);
820
+ const tipo = o.cofre || (await detectarTipo(d));
821
+ if (!tipo) return { erro: failure('cofre-nao-configurado', null) };
822
+ if (!TIPOS.includes(tipo)) return { erro: fail('uso', `Cofre desconhecido${eco(tipo)}. Use 1password ou infisical.`) };
823
+ return { tipo, backend: b[tipo] };
824
+ }
825
+
826
+ /** { ok:true, valor, cofre } | { ok:false, codigo, mensagem }. `opts.cofre` forca o backend. */
827
+ async function ler(texto, opts = {}, d = {}) {
828
+ const ref = toRef(texto);
829
+ if (!ref) return failure('nome-invalido', null);
830
+ const x = await resolveBackend(opts, d);
831
+ if (x.erro) return x.erro;
832
+ const r = await x.backend.ler(ref);
833
+ if (!r.ok) return r;
834
+ // existe mas sem valor (campo vazio): exit 0 com saida vazia faria o agente usar uma chave vazia
835
+ if (r.valor === '') {
836
+ return fail('nao-encontrado', `O segredo ${refText(ref)} existe no ${x.backend.nome}, mas esta sem valor (vazio). Grave o valor de novo. Se voce o guardou em outro campo (ITEM/CAMPO), leia com o nome completo.`);
837
+ }
838
+ return { ...r, cofre: x.tipo };
839
+ }
840
+
841
+ /** Grava e CONFERE lendo de volta. O valor nunca sai desta funcao em mensagem de erro. */
842
+ async function gravar(texto, valor, opts = {}, d = {}) {
843
+ const ref = toRef(texto);
844
+ if (!ref) return failure('nome-invalido', null);
845
+ if (typeof valor !== 'string' || valor === '') return fail('sem-valor', 'O valor esta vazio.');
846
+ const x = await resolveBackend(opts, d);
847
+ if (x.erro) return x.erro;
848
+ const w = await x.backend.gravar(ref, valor);
849
+ if (!w.ok) return w;
850
+ const back = await x.backend.ler(ref);
851
+ if (!back.ok) {
852
+ // O valor JA esta gravado: "nao-encontrado" (3) e "cofre-nao-configurado" (4) diriam ao agente que nada foi gravado
853
+ const codigo = back.codigo === 'nao-encontrado' || back.codigo === 'cofre-nao-configurado' ? 'falha' : back.codigo;
854
+ return fail(codigo, `Gravei no ${x.backend.nome}, mas nao consegui conferir lendo de volta (${oneLine(back.mensagem, 200, [valor])}) Confira que o nome existe com: segredo listar`);
855
+ }
856
+ if (back.valor !== valor) {
857
+ return fail('falha', `Gravei no ${x.backend.nome}, mas o que li de volta nao e igual ao que mandei (o cofre pode ter mudado espacos ou quebras de linha nas pontas). Nao use essa chave ainda: grave de novo por stdin, sem espaco nem linha extra nas pontas (gerador | npx auroq-os segredo gravar ${refText(ref)}).`);
858
+ }
859
+ return { ok: true, criado: Boolean(w.criado), cofre: x.tipo };
860
+ }
861
+
862
+ /** { ok:true, nomes:[...], cofre }: so nomes, nunca valores. */
863
+ async function listar(opts = {}, d = {}) {
864
+ const x = await resolveBackend(opts, d);
865
+ if (x.erro) return x.erro;
866
+ const r = await x.backend.listar();
867
+ return r.ok ? { ok: true, nomes: r.nomes, cofre: x.tipo } : r;
868
+ }
869
+
870
+ /**
871
+ * Apaga. Sem `opts.confirmo` nao apaga: diz se existe, o que sera apagado e pede `--confirmo` (`ja-existe-confirmar`).
872
+ * No 1Password o item inteiro vai para o Arquivo do app (nao da para apagar so um campo): por isso a confirmacao
873
+ * diz isso ANTES, e `ITEM/CAMPO --confirmo` so apaga se o campo for o unico com valor do item; senao o agente
874
+ * precisa pedir o item inteiro de forma explicita (`apagar ITEM --confirmo`).
875
+ */
876
+ async function apagar(texto, opts = {}, d = {}) {
877
+ const ref = toRef(texto);
878
+ if (!ref) return failure('nome-invalido', null);
879
+ const x = await resolveBackend(opts, d);
880
+ if (x.erro) return x.erro;
881
+ const confirmo = Boolean(opts && opts.confirmo);
882
+ const op = x.tipo === '1password';
883
+ if (!confirmo || (op && ref.campo !== CREDENTIAL)) {
884
+ const e = await x.backend.existe(ref);
885
+ if (!e.ok) return e;
886
+ if (!e.existe) return failure('nao-encontrado', x.tipo);
887
+ const campos = op ? (e.campos || []) : [];
888
+ const lista = campos.slice(0, 8).join(', ') + (campos.length > 8 ? ` e mais ${campos.length - 8}` : '');
889
+ const comando = `npx auroq-os segredo apagar ${ref.item} --confirmo`;
890
+ if (op && ref.campo !== CREDENTIAL) {
891
+ const outros = campos.filter((c) => c.toLowerCase() !== ref.campo.toLowerCase());
892
+ if (outros.length) {
893
+ return fail('ja-existe-confirmar', `ATENCAO: o 1Password nao apaga so um campo. Apagar ${refText(ref)} arquivaria o item ${ref.item} INTEIRO, com tudo que tem nele (campos com valor: ${lista}), nao so ${ref.campo}; ele volta pelo Arquivo do app. Se e isso mesmo que o aluno quer, rode: ${comando}`);
894
+ }
895
+ }
896
+ if (!confirmo) {
897
+ const alvo = ref.campo === CREDENTIAL ? 'a chave principal' : ref.campo;
898
+ const maisQueOAlvo = campos.some((c) => c.toLowerCase() !== ref.campo.toLowerCase()) ? `, nao so ${alvo}` : '';
899
+ const inteiro = op ? ` ATENCAO: no 1Password isso arquiva o item ${ref.item} INTEIRO${lista ? ` (campos com valor: ${lista})` : ''}${maisQueOAlvo}; ele volta pelo Arquivo do app.` : '';
900
+ return fail('ja-existe-confirmar', `O segredo ${refText(ref)} existe no ${x.backend.nome}.${inteiro} Para apagar de verdade, rode de novo com --confirmo.`);
901
+ }
902
+ }
903
+ const r = await x.backend.apagar(ref);
904
+ return r.ok ? { ok: true, cofre: x.tipo, itemInteiro: op } : r;
905
+ }
906
+
907
+ /** Cofre ativo e se esta pronto, SEM rede: { tipo: '1password'|'infisical'|'nenhum', pronto: boolean }. */
908
+ async function qual(opts = {}, d = {}) {
909
+ const tipo = (opts && opts.cofre) || (await detectarTipo(d));
910
+ if (!tipo || !TIPOS.includes(tipo)) return { tipo: 'nenhum', pronto: false };
911
+ const pronto = await backends(d)[tipo].pronto();
912
+ return { tipo, pronto: Boolean(pronto) };
913
+ }
914
+
915
+ function buildQualLine(q) {
916
+ return `AUROQ_COFRE tipo=${q.tipo} pronto=${q.pronto ? 'sim' : 'nao'}`;
917
+ }
918
+
919
+ // ─── segredo (linha de comando) ──────────────────────────
920
+
921
+ const SEGREDO_USO = 'Uso: auroq-os segredo ler|gravar|apagar <ITEM[/CAMPO]> | listar | qual [--cofre 1password|infisical] (gravar: valor por stdin ou --arquivo F; apagar: --confirmo)';
922
+
923
+ function parseSegredoArgs(argv) {
924
+ const out = { cmd: null, pos: [], cofre: null, arquivo: null, confirmo: false, erro: null };
925
+ const args = Array.isArray(argv) ? argv.map(String) : [];
926
+ const valueOf = (name, i, inline) => {
927
+ if (inline !== undefined) {
928
+ if (inline === '') out.erro = out.erro || `Falta o valor de ${name}.`;
929
+ return { v: inline || null, skip: 0 };
930
+ }
931
+ const next = args[i + 1];
932
+ if (next === undefined || next.startsWith('--')) {
933
+ out.erro = out.erro || `Falta o valor de ${name}.`;
934
+ return { v: null, skip: 0 };
935
+ }
936
+ return { v: next, skip: 1 };
937
+ };
938
+ for (let i = 0; i < args.length; i++) {
939
+ const a = args[i];
940
+ if (a === '--confirmo') out.confirmo = true;
941
+ else if (a === '--cofre' || a.startsWith('--cofre=')) {
942
+ const { v, skip } = valueOf('--cofre', i, a.startsWith('--cofre=') ? a.slice(8) : undefined);
943
+ i += skip;
944
+ out.cofre = v;
945
+ } else if (a === '--arquivo' || a.startsWith('--arquivo=')) {
946
+ const { v, skip } = valueOf('--arquivo', i, a.startsWith('--arquivo=') ? a.slice(10) : undefined);
947
+ i += skip;
948
+ out.arquivo = v;
949
+ } else if (a.startsWith('--')) {
950
+ // nao repete o texto: o argumento pode ser o proprio valor do segredo (--valor=sk-...)
951
+ out.erro = out.erro || 'Opcao desconhecida (so existem --cofre, --arquivo e --confirmo).';
952
+ } else {
953
+ out.pos.push(a);
954
+ }
955
+ }
956
+ out.cmd = out.pos.shift() || null;
957
+ if (out.cofre !== null && !TIPOS.includes(out.cofre)) out.erro = out.erro || `Cofre desconhecido${eco(out.cofre)}. Use 1password ou infisical.`;
958
+ return out;
959
+ }
960
+
961
+ /** Le o stream inteiro (texto). Sem dado por `idleMs` desiste (stdin aberto e vazio nao pode travar o agente). */
962
+ function readStream(stream, { max = MAX_VALUE_BYTES, idleMs = STDIN_IDLE_MS } = {}) {
963
+ return new Promise((resolve) => {
964
+ const chunks = [];
965
+ let total = 0;
966
+ let done = false;
967
+ let timer = null;
968
+ const finish = (r, stop) => {
969
+ if (done) return;
970
+ done = true;
971
+ clearTimeout(timer);
972
+ stream.removeListener('data', onData);
973
+ stream.removeListener('end', onEnd);
974
+ stream.removeListener('error', onError);
975
+ if (stop) {
976
+ try { stream.pause(); } catch { /* ja fechado */ }
977
+ try { if (typeof stream.destroy === 'function') stream.destroy(); } catch { /* ja fechado */ }
978
+ }
979
+ resolve(r);
980
+ };
981
+ const arm = () => {
982
+ clearTimeout(timer);
983
+ timer = setTimeout(() => finish({ vazio: true }, true), idleMs);
984
+ };
985
+ const onData = (chunk) => {
986
+ const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
987
+ total += b.length;
988
+ if (total > max) { finish({ grande: true }, true); return; }
989
+ chunks.push(b);
990
+ arm();
991
+ };
992
+ const onEnd = () => finish({ texto: Buffer.concat(chunks).toString('utf8') }, false);
993
+ const onError = () => finish({ erro: true }, true);
994
+ stream.on('data', onData);
995
+ stream.on('end', onEnd);
996
+ stream.on('error', onError);
997
+ arm();
998
+ });
999
+ }
1000
+
1001
+ /** Le no maximo `max + 1` bytes do arquivo (o byte a mais mostra que passou do limite): arquivo enorme nunca vai inteiro para a memoria. */
1002
+ function readFileBounded(file, max) {
1003
+ const fd = fs.openSync(file, 'r');
1004
+ try {
1005
+ const buf = Buffer.alloc(max + 1);
1006
+ let n = 0;
1007
+ while (n < buf.length) {
1008
+ const got = fs.readSync(fd, buf, n, buf.length - n, null);
1009
+ if (got === 0) break;
1010
+ n += got;
1011
+ }
1012
+ return buf.subarray(0, n);
1013
+ } finally {
1014
+ fs.closeSync(fd);
1015
+ }
1016
+ }
1017
+
1018
+ /** Valor a gravar: `--arquivo F` ou stdin (so se nao for terminal). Tira o BOM do Windows, a quebra de linha final e os espacos das pontas. */
1019
+ async function readValue(o, d) {
1020
+ let texto;
1021
+ if (o.arquivo) {
1022
+ let lido;
1023
+ try {
1024
+ lido = d.readFile ? d.readFile(o.arquivo) : readFileBounded(o.arquivo, MAX_VALUE_BYTES);
1025
+ } catch {
1026
+ return fail('sem-valor', 'Nao consegui ler o arquivo informado em --arquivo. Confira o caminho.');
1027
+ }
1028
+ // nunca vira o texto "undefined" (nem "[object Object]") gravado como se fosse o segredo
1029
+ if (typeof lido !== 'string' && !Buffer.isBuffer(lido)) return fail('sem-valor', 'Nao consegui ler o arquivo informado em --arquivo. Confira o caminho.');
1030
+ if (Buffer.byteLength(lido) > MAX_VALUE_BYTES) return fail('uso', 'O valor passa de 1 MiB; nao grava.');
1031
+ texto = String(lido);
1032
+ } else {
1033
+ const stdin = d.stdin || process.stdin;
1034
+ if (stdin.isTTY) {
1035
+ return fail('sem-valor', 'O valor nao pode ser digitado aqui nem ir na linha de comando. Mande por pipe (gerador | npx auroq-os segredo gravar ITEM) ou use --arquivo F.');
1036
+ }
1037
+ const lido = await readStream(stdin, { idleMs: d.stdinIdleMs || STDIN_IDLE_MS });
1038
+ if (lido.grande) return fail('uso', 'O valor passa de 1 MiB; nao grava.');
1039
+ if (lido.erro || lido.vazio) return fail('sem-valor', 'Nao recebi o valor por stdin. Mande por pipe ou use --arquivo F.');
1040
+ texto = lido.texto;
1041
+ }
1042
+ const valor = cleanValue(texto);
1043
+ if (valor === '') return fail('sem-valor', 'O valor esta vazio.');
1044
+ // NUL ou U+FFFD = nao e texto UTF-8 (tipico de arquivo UTF-16 salvo pelo PowerShell com ">"): recusa em vez de gravar lixo "conferido"
1045
+ if (/[\u0000\uFFFD]/.test(valor)) {
1046
+ return fail('sem-valor', 'O valor nao parece texto comum (UTF-8); veio de um arquivo UTF-16 do Windows? Salve de novo como UTF-8 ou mande por pipe: gerador | npx auroq-os segredo gravar ITEM');
1047
+ }
1048
+ return { ok: true, valor };
1049
+ }
1050
+
1051
+ /**
1052
+ * `auroq-os segredo <ler|gravar|listar|apagar|qual> ...` -> codigo de saida (0 ok, 1 erro, 2 uso, 3 nao
1053
+ * encontrado, 4 cofre nao configurado). `ler` escreve o valor em stdout e NADA mais; erro vai so para o
1054
+ * stderr como `AUROQ_ERRO codigo=<c> mensagem="..."`, nunca com valor.
1055
+ */
1056
+ async function segredoCli(argv, d = {}) {
1057
+ const out = d.stdout || process.stdout;
1058
+ const errOut = d.stderr || process.stderr;
1059
+ const write = (stream, text) => { stream.write(text); };
1060
+ let secrets = [];
1061
+ const erro = (codigo, mensagem) => {
1062
+ write(errOut, `AUROQ_ERRO codigo=${codigo} mensagem="${quoted(mensagem, 400, secrets)}"\n`);
1063
+ return exitCodeFor(codigo);
1064
+ };
1065
+ try {
1066
+ const o = parseSegredoArgs(argv);
1067
+ if (o.erro) return erro('uso', `${o.erro} ${SEGREDO_USO}`);
1068
+ const opts = { cofre: o.cofre || undefined, confirmo: o.confirmo };
1069
+
1070
+ if (o.cmd === 'qual') {
1071
+ if (o.pos.length) return erro('uso', SEGREDO_USO);
1072
+ write(out, `${buildQualLine(await qual(opts, d))}\n`);
1073
+ return SEGREDO_EXIT.ok;
1074
+ }
1075
+ if (o.cmd === 'listar') {
1076
+ if (o.pos.length) return erro('uso', SEGREDO_USO);
1077
+ const r = await listar(opts, d);
1078
+ if (!r.ok) return erro(r.codigo, r.mensagem);
1079
+ if (r.nomes.length) write(out, `${r.nomes.join('\n')}\n`);
1080
+ return SEGREDO_EXIT.ok;
1081
+ }
1082
+ if (o.cmd === 'ler' || o.cmd === 'apagar' || o.cmd === 'gravar') {
1083
+ if (o.pos.length !== 1) {
1084
+ const dica = o.cmd === 'gravar' && o.pos.length > 1 ? ' O valor nunca vai na linha de comando: mande por stdin ou use --arquivo F.' : '';
1085
+ return erro('uso', `${SEGREDO_USO}${dica}`);
1086
+ }
1087
+ const ref = parseRef(o.pos[0]);
1088
+ // nao repete o nome recebido: quem erra a ordem dos argumentos manda o valor do segredo no lugar do nome
1089
+ if (!ref) {
1090
+ // so o tamanho, nunca o texto: nome com mais de 64 caracteres quase sempre e o valor no lugar do nome
1091
+ const parece = o.pos[0].length > 64 ? ' O que veio no lugar do nome tem mais de 64 caracteres: parece o valor? O valor nunca vai na linha de comando; mande por stdin ou use --arquivo F.' : '';
1092
+ return erro('nome-invalido', `Nome invalido. Use ITEM ou ITEM/CAMPO (letras, numeros, ponto, hifen e underscore; sem "__"; ate 64 caracteres cada).${parece}`);
1093
+ }
1094
+
1095
+ if (o.cmd === 'ler') {
1096
+ const r = await ler(ref, opts, d);
1097
+ if (!r.ok) return erro(r.codigo, r.mensagem);
1098
+ write(out, out.isTTY ? `${r.valor}\n` : r.valor);
1099
+ return SEGREDO_EXIT.ok;
1100
+ }
1101
+ if (o.cmd === 'gravar') {
1102
+ // primeiro o cofre: sem cofre conectado nao adianta esperar o valor no stdin (e o agente recebe o erro certo na hora)
1103
+ const x = await resolveBackend(opts, d);
1104
+ if (x.erro) return erro(x.erro.codigo, x.erro.mensagem);
1105
+ if (!(await x.backend.pronto())) {
1106
+ const sem = failure('cofre-nao-configurado', x.tipo);
1107
+ return erro(sem.codigo, sem.mensagem);
1108
+ }
1109
+ const v = await readValue(o, d);
1110
+ if (!v.ok) return erro(v.codigo, v.mensagem);
1111
+ secrets = [v.valor];
1112
+ const r = await gravar(ref, v.valor, { ...opts, cofre: x.tipo }, d);
1113
+ if (!r.ok) return erro(r.codigo, r.mensagem);
1114
+ write(out, `AUROQ_SEGREDO gravado ref=${refText(ref)} cofre=${r.cofre}\n`);
1115
+ return SEGREDO_EXIT.ok;
1116
+ }
1117
+ const r = await apagar(ref, opts, d);
1118
+ if (!r.ok) return erro(r.codigo, r.mensagem);
1119
+ write(out, `AUROQ_SEGREDO apagado ref=${refText(ref)} cofre=${r.cofre}${r.itemInteiro ? ' nota=item-inteiro-arquivado' : ''}\n`);
1120
+ return SEGREDO_EXIT.ok;
1121
+ }
1122
+ return erro('uso', SEGREDO_USO);
1123
+ } catch (e) {
1124
+ return erro('falha', `Falha inesperada ao falar com o cofre: ${oneLine(e && e.message, 160, secrets)}`);
1125
+ }
1126
+ }
1127
+
1128
+ // ─── conectar-cofre ──────────────────────────────────────
1129
+
1130
+ function parseConectarArgs(argv) {
1131
+ const args = Array.isArray(argv) ? argv.map(String) : [];
1132
+ const o = { cofre: null, cofreInvalido: null, estado: false, agente: false, migrar: false, ignorarForaDoFormato: false, rest: [] };
1133
+ for (let i = 0; i < args.length; i++) {
1134
+ const a = args[i];
1135
+ if (a === '--cofre' || a.startsWith('--cofre=')) {
1136
+ let v = a.startsWith('--cofre=') ? a.slice(8) : null;
1137
+ if (v === null && args[i + 1] !== undefined && !args[i + 1].startsWith('--')) v = args[++i];
1138
+ if (TIPOS.includes(v)) o.cofre = v;
1139
+ else o.cofreInvalido = v || '';
1140
+ } else if (a === '--migrar') {
1141
+ o.migrar = true;
1142
+ } else if (a === '--ignorar-fora-do-formato') {
1143
+ o.ignorarForaDoFormato = true;
1144
+ } else {
1145
+ if (a === '--estado') o.estado = true;
1146
+ if (a === '--agente') o.agente = true;
1147
+ o.rest.push(a);
1148
+ }
1149
+ }
1150
+ return o;
1151
+ }
1152
+
1153
+ /** Estado salvo de uma rota: { existe, pronto }. Arquivo ausente ou estragado = nao existe. */
1154
+ function stateInfo(d, tipo) {
1155
+ if (d && typeof d.stateInfo === 'function') return d.stateInfo(tipo);
1156
+ try {
1157
+ const file = mod(d, tipo === '1password' ? 'onepassword' : 'infisical').stateFile();
1158
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
1159
+ if (!data || typeof data !== 'object' || data.versao !== 1 || !data.etapas || typeof data.etapas !== 'object') return { existe: false, pronto: false };
1160
+ return { existe: true, pronto: data.etapas.pronto === 'feito' };
1161
+ } catch {
1162
+ return { existe: false, pronto: false };
1163
+ }
1164
+ }
1165
+
1166
+ /** Qual rota seguir: --cofre, depois cofre.json, depois a UNICA rota em andamento, depois o que ja esta conectado. null = perguntar. */
1167
+ async function escolherRota(o, d) {
1168
+ if (o.cofre) return o.cofre;
1169
+ const c = readCofre(d);
1170
+ if (c) return c.tipo;
1171
+ const andamento = TIPOS.filter((t) => {
1172
+ const s = stateInfo(d, t);
1173
+ return s.existe && !s.pronto;
1174
+ });
1175
+ if (andamento.length === 1) return andamento[0];
1176
+ if (andamento.length === 0) return detectarTipo(d);
1177
+ return null;
1178
+ }
1179
+
1180
+ /** Roda `fn` guardando o que ela escreve com console.log (a linha de estado das rotas sai por la). */
1181
+ async function capturarLinhas(fn) {
1182
+ const original = console.log;
1183
+ const linhas = [];
1184
+ console.log = (...args) => { linhas.push(...util.format(...args).split('\n')); };
1185
+ try {
1186
+ await fn();
1187
+ } finally {
1188
+ console.log = original;
1189
+ }
1190
+ return linhas;
1191
+ }
1192
+
1193
+ function resultLine(r) {
1194
+ const contagem = r.movidos !== undefined ? ` movidos=${r.movidos} iguais=${r.iguais} falhas=${r.falhas}` : '';
1195
+ return `AUROQ_RESULTADO cofre=${r.status} codigo=${r.codigo}${contagem} mensagem="${quoted(r.mensagem)}"`;
1196
+ }
1197
+
1198
+ const buildMigrationLine = resultLine;
1199
+
1200
+ function escolha(prefixo = '') {
1201
+ return { status: 'aguardando', codigo: 'escolher-cofre', mensagem: `${prefixo}${ESCOLHA_MSG}` };
1202
+ }
1203
+
1204
+ /** Erros que o aluno resolve (a telemetria do instalador nao os conta como instalacao quebrada). Aceita o resultado ou o codigo. */
1205
+ function isStudentActionable(result) {
1206
+ const codigo = typeof result === 'string' ? result : result && result.codigo;
1207
+ if (!codigo) return false;
1208
+ if (codigo === 'origem-indisponivel') return true;
1209
+ for (const nome of ['onepassword', 'infisical']) {
1210
+ try {
1211
+ const m = mod(null, nome);
1212
+ if (typeof m.isStudentActionable === 'function' && m.isStudentActionable(codigo)) return true;
1213
+ } catch { /* modulo ausente: segue */ }
1214
+ }
1215
+ return false;
1216
+ }
1217
+
1218
+ /**
1219
+ * `auroq-os conectar-cofre [--cofre 1password|infisical] [--agente] [--estado] [--migrar] [flags da rota]`.
1220
+ * Devolve o resultado da rota (ou o proprio: escolha, estado, migracao). Codigo de saida: 0 ok/aguardando, 1 erro.
1221
+ */
1222
+ async function conectarCofre(argv = [], d = {}) {
1223
+ const print = d.print || ((line) => console.log(line));
1224
+ const setExit = d.setExit || ((code) => { process.exitCode = code; });
1225
+ const o = parseConectarArgs(argv);
1226
+ const emit = (r) => {
1227
+ print(o.agente ? resultLine(r) : r.mensagem);
1228
+ setExit(r.status === 'erro' ? 1 : 0);
1229
+ return r;
1230
+ };
1231
+
1232
+ if (o.cofreInvalido !== null) return emit(escolha(`Nao conheco esse cofre${eco(o.cofreInvalido)}. `));
1233
+ if (o.migrar) return emit(await migrar({ de: 'infisical', para: '1password', ignorarForaDoFormato: o.ignorarForaDoFormato }, d));
1234
+
1235
+ const tipo = await escolherRota(o, d);
1236
+
1237
+ if (o.estado) {
1238
+ if (!tipo) {
1239
+ print(o.agente ? 'AUROQ_ESTADO cofre=nenhum proxima=escolha' : `Nenhum cofre conectado ainda. ${ESCOLHA_MSG}`);
1240
+ return { status: 'ok', codigo: 'estado', cofre: 'nenhum' };
1241
+ }
1242
+ const rota = mod(d, tipo === '1password' ? 'onepassword' : 'infisical');
1243
+ if (!o.agente) {
1244
+ await rota.connect({ argv: o.rest });
1245
+ return { status: 'ok', codigo: 'estado', cofre: tipo };
1246
+ }
1247
+ const ativo = (await detectarTipo(d)) || 'nenhum';
1248
+ const linhas = await capturarLinhas(() => rota.connect({ argv: o.rest }));
1249
+ const linha = linhas.find((l) => /^AUROQ_ESTADO\b/.test(l));
1250
+ const resto = linha ? linha.replace(/^AUROQ_ESTADO\s*/, '') : `${tipo}=desconhecido`;
1251
+ print(`AUROQ_ESTADO cofre=${ativo} ${resto}`.trim());
1252
+ return { status: 'ok', codigo: 'estado', cofre: ativo };
1253
+ }
1254
+
1255
+ if (!tipo) return emit(escolha());
1256
+
1257
+ const rota = mod(d, tipo === '1password' ? 'onepassword' : 'infisical');
1258
+ // ANTES da rota: ela mesma marca o Infisical como cofre ativo quando termina
1259
+ const tinhaChavesNoUmPassword = tipo === 'infisical' && (await umPasswordAtivo(d));
1260
+ const result = await rota.connect({ argv: o.rest });
1261
+ await registrarSeOk(result, tipo, d, o.rest);
1262
+ if (tinhaChavesNoUmPassword && result && result.status === 'ok') {
1263
+ // escolha explicita do aluno: o Infisical passa a ser o cofre ativo, mas as chaves do 1Password nao vieram junto
1264
+ print(avisoLinha(o.agente, 'cofre=infisical chaves-antigas=1password', AVISO_CHAVES_ANTIGAS));
1265
+ }
1266
+ return result;
1267
+ }
1268
+
1269
+ /** O 1Password e o cofre ativo e esta pronto neste computador (sem rede)? */
1270
+ async function umPasswordAtivo(d) {
1271
+ try {
1272
+ return (await detectarTipo(d)) === '1password' && Boolean(await backends(d)['1password'].pronto());
1273
+ } catch {
1274
+ return false;
1275
+ }
1276
+ }
1277
+
1278
+ // ─── Migracao Infisical -> 1Password ─────────────────────
1279
+
1280
+ function migracao(status, codigo, mensagem, n = {}) {
1281
+ return { status, codigo, movidos: n.movidos || 0, iguais: n.iguais || 0, falhas: n.falhas || 0, mensagem };
1282
+ }
1283
+
1284
+ /**
1285
+ * Leva as chaves do Infisical para o cofre Claude do 1Password e, so se TUDO foi, troca o cofre ativo (e a UNICA
1286
+ * coisa que troca para o 1Password quem ja usa o Infisical: conectar o 1Password sozinho nao troca). Nao apaga
1287
+ * nada da origem. Cada valor e lido, gravado e relido para conferir, sempre em memoria: nunca aparece em
1288
+ * mensagem. `AUROQ_TESTE` e `AUROQ_TESTE_<codigo>` (segredo de teste da conexao) ficam de fora. Nomes que so diferem na caixa (abc e ABC)
1289
+ * sao falha (o 1Password os juntaria num item so); valor diferente que ja estava no 1Password e trocado pelo do
1290
+ * Infisical e a mensagem diz quais (so nomes). `options.ignorarForaDoFormato`: nomes que o Auroq nao usa ficam
1291
+ * no Infisical, sem impedir a troca.
1292
+ */
1293
+ async function migrar(options = {}, d = {}) {
1294
+ const de = (options && options.de) || 'infisical';
1295
+ const para = (options && options.para) || '1password';
1296
+ if (de !== 'infisical' || para !== '1password') {
1297
+ return migracao('erro', 'falha', 'So existe a migracao do Infisical para o 1Password.');
1298
+ }
1299
+ const b = backends(d);
1300
+ const origem = b.infisical;
1301
+ const destino = b['1password'];
1302
+
1303
+ if (!(await origem.pronto())) {
1304
+ return migracao('erro', 'origem-indisponivel', 'O Infisical nao esta conectado neste computador, entao nao tenho de onde levar as chaves.');
1305
+ }
1306
+ if (!(await destino.pronto())) {
1307
+ return migracao('aguardando', 'conectar-1password-primeiro', 'Falta conectar o 1Password neste computador. Rode: npx auroq-os conectar-cofre --cofre 1password --agente. Depois rode a migracao de novo.');
1308
+ }
1309
+ // o cofre Claude precisa estar visivel para a chave do 1Password
1310
+ const vis = await destino.listar();
1311
+ if (!vis.ok) {
1312
+ if (['cofre-nao-configurado', 'credencial-invalida', 'sem-permissao'].includes(vis.codigo)) {
1313
+ return migracao('aguardando', 'conectar-1password-primeiro', `A chave do 1Password deste computador nao enxerga o cofre ${VAULT_NAME}. Rode: npx auroq-os conectar-cofre --cofre 1password --agente. Depois rode a migracao de novo.`);
1314
+ }
1315
+ return migracao('erro', 'falha', `Nao consegui olhar o cofre ${VAULT_NAME} do 1Password agora (${vis.codigo}). Tente de novo em alguns minutos.`);
1316
+ }
1317
+
1318
+ const lista = await origem.chaves();
1319
+ if (!lista.ok) {
1320
+ // chave revogada ou identidade/projeto apagado: esperar nunca resolve; o caminho e gerar a chave de novo.
1321
+ // 'sem-permissao' por causa do plano gratis (plano-restringe) NAO e isso: regenerar a chave nao muda nada.
1322
+ const morta = lista.codigo === 'credencial-invalida' || lista.codigo === 'nao-encontrado'
1323
+ || (lista.codigo === 'sem-permissao' && lista.mensagem !== MSG_PLANO_RESTRINGE);
1324
+ if (morta) {
1325
+ return migracao('erro', 'origem-indisponivel', MSG_ORIGEM_MORTA);
1326
+ }
1327
+ return migracao('erro', 'origem-indisponivel', `Nao consegui listar as chaves do Infisical agora (${lista.codigo}). Tente de novo em alguns minutos.`);
1328
+ }
1329
+ const chaves = lista.chaves.filter((k) => !ehSegredoDeTeste(k));
1330
+ const colide = colisoesDeCaixa(chaves);
1331
+ const ignorarForaDoFormato = Boolean(options && options.ignorarForaDoFormato);
1332
+
1333
+ let movidos = 0;
1334
+ let iguais = 0;
1335
+ const falhou = []; // erro de verdade (rede, cofre, conferencia): rodar de novo pode resolver
1336
+ const colidiram = []; // so diferem na caixa (abc e ABC): no 1Password sao o MESMO nome, uma apagaria a outra
1337
+ const foraDoFormato = []; // nome que o Auroq nao usa (espaco, mais de 64...): rodar de novo nunca resolve
1338
+ const trocadas = []; // o 1Password ja tinha OUTRO valor com esse nome e ficou com o do Infisical
1339
+ for (const chave of chaves) {
1340
+ const ref = refFromKey(chave);
1341
+ // chave fora do formato (ou que nao volta igual) nao tem lugar certo no 1Password: nao se perde em silencio
1342
+ if (!ref || infisicalKey(ref) !== chave) { foraDoFormato.push(oneLine(chave, 80)); continue; }
1343
+ if (colide.has(chave)) { colidiram.push(chave); continue; }
1344
+ const src = await origem.ler(ref);
1345
+ if (!src.ok) { falhou.push(chave); continue; }
1346
+ const atual = await destino.ler(ref);
1347
+ if (atual.ok && atual.valor === src.valor) { iguais += 1; continue; }
1348
+ if (!atual.ok && atual.codigo !== 'nao-encontrado') { falhou.push(chave); continue; }
1349
+ const w = await destino.gravar(ref, src.valor);
1350
+ if (!w.ok) { falhou.push(chave); continue; }
1351
+ const back = await destino.ler(ref);
1352
+ if (back.ok && back.valor === src.valor) {
1353
+ movidos += 1;
1354
+ if (atual.ok && atual.valor !== '') trocadas.push(oneLine(chave, 80));
1355
+ } else {
1356
+ falhou.push(chave);
1357
+ }
1358
+ }
1359
+
1360
+ const lista10 = (a) => a.slice(0, 10).join(', ') + (a.length > 10 ? ` e mais ${a.length - 10}` : '');
1361
+ const aviso = trocadas.length
1362
+ ? ` Atencao: ${trocadas.length} chave(s) ja existiam no 1Password com outro valor e ficaram com o do Infisical (${lista10(trocadas)}); o valor anterior fica no historico do item, no app do 1Password.`
1363
+ : '';
1364
+ const deixadas = ignorarForaDoFormato ? foraDoFormato : [];
1365
+ const bloqueiam = ignorarForaDoFormato ? [] : foraDoFormato;
1366
+ const n = { movidos, iguais, falhas: falhou.length + colidiram.length + bloqueiam.length };
1367
+ if (n.falhas) {
1368
+ let msg = `Passei ${movidos} chave(s) para o 1Password e ${iguais} ja estavam iguais.`;
1369
+ if (falhou.length) msg += ` ${falhou.length} deram erro (${lista10(falhou)}): rode a migracao de novo.`;
1370
+ if (colidiram.length) {
1371
+ msg += ` ${colidiram.length} tem o mesmo nome mudando so maiuscula/minuscula (${lista10(colidiram)}); no 1Password isso e o mesmo nome e uma apagaria a outra, entao nao passei nenhuma delas. No painel do Infisical, deixe so uma de cada par (ou renomeie) e rode de novo.`;
1372
+ }
1373
+ if (bloqueiam.length) {
1374
+ msg += ` ${bloqueiam.length} tem nome que o Auroq nao usa (${lista10(bloqueiam)}): so vale letra, numero, ponto, hifen e underline, ate 64 caracteres, sem espaco e sem dois underlines seguidos. Rodar de novo nao resolve: renomeie ou apague essas chaves no painel do Infisical e rode a migracao, ou, se quiser deixa-las no Infisical (o Auroq nao le esses nomes de qualquer jeito), rode: npx auroq-os conectar-cofre --migrar --ignorar-fora-do-formato.`;
1375
+ }
1376
+ msg += ` Nao troquei o cofre e nao apaguei nada do Infisical.${aviso}`;
1377
+ return migracao('erro', 'migrado-parcial', msg, n);
1378
+ }
1379
+ try {
1380
+ registrar('1password', { anterior: 'infisical' }, d);
1381
+ } catch (e) {
1382
+ return migracao('erro', 'falha', `Passei as chaves, mas nao consegui marcar o 1Password como cofre ativo (${oneLine(e && e.message, 120)}). Rode a migracao de novo.`, n);
1383
+ }
1384
+ const ficaram = deixadas.length ? ` Deixei ${deixadas.length} chave(s) no Infisical porque o nome nao serve para o Auroq (${lista10(deixadas)}); nao apaguei nada.` : '';
1385
+ if (!chaves.length) {
1386
+ return migracao('ok', 'nada-a-migrar', 'O Infisical nao tinha chaves para passar. O 1Password passa a ser o cofre ativo; o Infisical continua como estava.', n);
1387
+ }
1388
+ return migracao('ok', 'migrado', `Passei ${movidos} chave(s) para o 1Password (${iguais} ja estavam iguais). O 1Password e o cofre ativo agora; nao apaguei nada do Infisical.${ficaram}${aviso}`, n);
1389
+ }
1390
+
1391
+ /** E um segredo de teste da conexao (a migracao nao leva)? So o nome `AUROQ_TESTE` ou `AUROQ_TESTE_<letras e numeros>`. */
1392
+ function ehSegredoDeTeste(nome) {
1393
+ return typeof nome === 'string' && TEST_SECRET_RE.test(nome);
1394
+ }
1395
+
1396
+ /** Chaves canonicas que so diferem na caixa (abc e ABC): o 1Password nao diferencia caixa no titulo nem no campo. */
1397
+ function colisoesDeCaixa(chaves) {
1398
+ const porNome = new Map();
1399
+ for (const chave of chaves) {
1400
+ const r = refFromKey(chave);
1401
+ if (!r || infisicalKey(r) !== chave) continue; // fora do formato: ja tem tratamento proprio
1402
+ const nome = `${r.item}\u0000${r.campo}`.toLowerCase();
1403
+ porNome.set(nome, (porNome.get(nome) || []).concat(chave));
1404
+ }
1405
+ const colide = new Set();
1406
+ for (const ks of porNome.values()) if (ks.length > 1) ks.forEach((k) => colide.add(k));
1407
+ return colide;
1408
+ }
1409
+
1410
+ module.exports = {
1411
+ cofreFile,
1412
+ readCofre,
1413
+ registrar,
1414
+ registrarSeOk,
1415
+ detectarTipo,
1416
+ parseRef,
1417
+ infisicalKey,
1418
+ refFromKey,
1419
+ backends,
1420
+ ler,
1421
+ gravar,
1422
+ listar,
1423
+ apagar,
1424
+ qual,
1425
+ segredoCli,
1426
+ conectarCofre,
1427
+ migrar,
1428
+ buildQualLine,
1429
+ buildMigrationLine,
1430
+ SEGREDO_EXIT,
1431
+ // auxiliares (testes e fiacao)
1432
+ SEGREDO_CODIGOS,
1433
+ exitCodeFor,
1434
+ isStudentActionable,
1435
+ parseSegredoArgs,
1436
+ parseConectarArgs,
1437
+ readStream,
1438
+ oneLine,
1439
+ validName,
1440
+ refText,
1441
+ opReference,
1442
+ itemTemplate,
1443
+ classifyOp,
1444
+ VAULT_NAME,
1445
+ ITEM_TAG,
1446
+ TEST_SECRET,
1447
+ ehSegredoDeTeste,
1448
+ withFileLock,
1449
+ tryLock,
1450
+ tomarVelha,
1451
+ TIPOS,
1452
+ ESCOLHA_MSG,
1453
+ };