auroq-os 2.8.2 → 2.8.5

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.
@@ -478,6 +478,7 @@ const BACKUP_TAG = 'auroq-os';
478
478
  // Contas pessoais (Individual e Families) entram em my.1password.<regiao>; Teams e Business em <time>.1password.*.
479
479
  const PERSONAL_ACCOUNT_URL = /^my\.1password\.(com|eu|ca)$/;
480
480
  const LOCK_STALE_MS = 2 * 60 * 60 * 1000;
481
+ const APPROVAL_TIMEOUT_MS = 180000; // o aluno tem ate 3 min para aprovar a janela da digital
481
482
  const MAX_WAIT_SECONDS = 30 * 60; // espera da chave do app nunca passa disso (a trava de instancia vale 2 h)
482
483
  // "Aguardando" (falta uma acao do aluno) e uma ETAPA do fluxo, nao falha: sai com 0 e a linha
483
484
  // AUROQ_RESULTADO diz o que falta. Sair com erro faria a telemetria do instalador registrar
@@ -642,13 +643,14 @@ function pickAccount(accounts, wanted) {
642
643
  /** Argumentos de `auroq-os conectar-1password [...]`. */
643
644
  function parseArgs(argv) {
644
645
  const args = Array.isArray(argv) ? argv : [];
645
- const opts = { manual: false, rotacionar: false, agente: false, estado: false, conta: null, esperar: null };
646
+ const opts = { manual: false, rotacionar: false, agente: false, estado: false, instalarApp: false, conta: null, esperar: null };
646
647
  for (let i = 0; i < args.length; i++) {
647
648
  const a = args[i];
648
649
  if (a === '--manual') opts.manual = true;
649
650
  else if (a === '--rotacionar') opts.rotacionar = true;
650
651
  else if (a === '--agente') opts.agente = true;
651
652
  else if (a === '--estado') opts.estado = true;
653
+ else if (a === '--instalar-app') opts.instalarApp = true;
652
654
  else if (a === '--conta' && args[i + 1] && !args[i + 1].startsWith('--')) opts.conta = args[++i];
653
655
  else if (a.startsWith('--conta=')) opts.conta = a.slice('--conta='.length) || null;
654
656
  else if (a === '--esperar' && args[i + 1] && !args[i + 1].startsWith('--')) opts.esperar = Number(args[++i]);
@@ -664,8 +666,8 @@ function enableIntegrationSteps(platform) {
664
666
  ? 'Abra as Configuracoes: no menu de cima do Mac, 1Password > Configuracoes (ou Cmd + virgula). Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.'
665
667
  : 'Abra as Configuracoes: clique no nome da sua conta (canto de cima, a esquerda) > Configuracoes. Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.';
666
668
  const digital = platform === 'darwin'
667
- ? 'Para aprovar com a digital: Configuracoes > Seguranca > "Desbloquear com Touch ID". Sem isso o app pede a senha do Mac.'
668
- : 'Para aprovar com o Windows Hello: Configuracoes > Seguranca > "Desbloquear com Windows Hello". Sem isso o app pede a senha.';
669
+ ? 'Para aprovar com a digital: Configuracoes > Seguranca > "Desbloquear com Touch ID". Sem isso o Mac pede a senha do seu usuario do Mac.'
670
+ : 'Para aprovar com o Windows Hello: Configuracoes > Seguranca > "Desbloquear com Windows Hello". Sem o Windows Hello ligado a janela de aprovacao pode nem aparecer.';
669
671
  return [
670
672
  'Abra o app do 1Password, entre na sua conta e deixe o app aberto.',
671
673
  abrir,
@@ -674,20 +676,42 @@ function enableIntegrationSteps(platform) {
674
676
  ];
675
677
  }
676
678
 
679
+ /**
680
+ * Receita curta para ligar a digital (Windows Hello / Touch ID) quando nao esta ligada. A documentacao da integracao
681
+ * manda ligar o Windows Hello no Windows; no Mac o Touch ID e opcional (sem ele vale a senha do usuario do Mac). A
682
+ * senha da conta 1Password NAO faz esse papel. O codigo nao liga nada disso por ele.
683
+ */
684
+ function biometricSteps(platform) {
685
+ if (platform === 'darwin') {
686
+ return [
687
+ 'No Mac, cadastre a digital: Ajustes do Sistema > Touch ID e Senha > Adicionar digital.',
688
+ 'No 1Password: Configuracoes > Seguranca > marque "Desbloquear com Touch ID".',
689
+ 'O Touch ID e opcional: se o seu Mac nao tem, o Mac pede a senha do seu usuario do Mac na janela de aprovacao.',
690
+ ];
691
+ }
692
+ return [
693
+ 'No Windows, ligue o Windows Hello: Configuracoes do Windows > Contas > Opcoes de entrada, e configure o PIN, a digital ou o reconhecimento facial (o que o seu computador tiver).',
694
+ 'No 1Password: Configuracoes > Seguranca > marque "Desbloquear com Windows Hello".',
695
+ 'No Windows o 1Password pede o Windows Hello ligado (PIN, digital ou rosto: serve o que o computador tiver); sem ele a janela de aprovacao pode nem aparecer.',
696
+ ];
697
+ }
698
+
677
699
  // ─── Dependencias reais (trocaveis nos testes) ───────────
678
700
 
679
701
  /**
680
702
  * Roda o `op`. Nunca lanca. A saida pode ter segredo (token): quem chama decide o que mostrar.
681
703
  * O token NUNCA vai em `args`: so por `env` ou por `input` (stdin).
682
704
  */
683
- function runOp(args, { env, input, timeoutMs = 120000 } = {}) {
705
+ function runOp(args, { env, input, timeoutMs = 120000, noStdin = false } = {}) {
684
706
  // Sem texto para mandar e com terminal de verdade: o `op` herda o terminal, como se o aluno
685
707
  // tivesse digitado (a aprovacao do app e amarrada ao terminal; com pipe nao ha terminal).
686
- const useTerminal = input === undefined && Boolean(process.stdin.isTTY);
708
+ // `noStdin`: o `op` recusa `--template` se o stdin for um pipe, ate vazio ("You can't use both piping and the
709
+ // --template flag"; o `input: ''` do Node vira pipe), entao esse comando roda com o stdin desligado (NUL/dev/null).
710
+ const useTerminal = input === undefined && !noStdin && Boolean(process.stdin.isTTY);
687
711
  const r = spawnSync('op', args, {
688
712
  encoding: 'utf8',
689
713
  env: env || opEnv(),
690
- ...(useTerminal ? { stdio: ['inherit', 'pipe', 'pipe'] } : { input: input === undefined ? '' : input }),
714
+ ...(noStdin ? { stdio: ['ignore', 'pipe', 'pipe'] } : useTerminal ? { stdio: ['inherit', 'pipe', 'pipe'] } : { input: input === undefined ? '' : input }),
691
715
  timeout: timeoutMs,
692
716
  maxBuffer: 16 * 1024 * 1024,
693
717
  windowsHide: true,
@@ -767,6 +791,181 @@ function refreshWindowsPath() {
767
791
  if (parts.length) process.env.PATH = [...parts, process.env.PATH || ''].join(';');
768
792
  }
769
793
 
794
+ // ─── O app do 1Password (instalacao) ─────────────────────
795
+
796
+ const WINGET_APP_ID = 'AgileBits.1Password';
797
+ // Instalador oficial (reserva do winget, que falhou no teste real de 27/10 com 0x80070002 depois de conferir o hash).
798
+ const APP_INSTALLER_URL = 'https://downloads.1password.com/win/1PasswordSetup-latest.exe';
799
+ // Assinante do instalador oficial: "Valid | CN=Agilebits, O=Agilebits, L=Toronto, S=Ontario, C=CA".
800
+ const APP_SIGNER_RE = /(?:^|,\s*)CN=Agilebits(?:,|$)/i;
801
+
802
+ /** O app do 1Password esta neste computador? true | false | null (sistema sem instalacao automatica: nao sabe). */
803
+ function appInstalled(platform = process.platform, env = process.env) {
804
+ // AUROQ_APP_1PASSWORD=instalado|ausente: so para os testes de processo real, que rodam em Mac/Windows de verdade
805
+ // (onde o app nao esta) e nao podem depender de o app existir na maquina.
806
+ const forcado = String(env.AUROQ_APP_1PASSWORD || '').toLowerCase();
807
+ if (forcado === 'instalado') return true;
808
+ if (forcado === 'ausente') return false;
809
+ if (platform === 'darwin') {
810
+ return ['/Applications/1Password.app', path.join(os.homedir(), 'Applications', '1Password.app')].some((p) => fs.existsSync(p));
811
+ }
812
+ if (platform === 'win32') {
813
+ // Instalador do aluno: %LOCALAPPDATA%\1Password\app\8\1Password.exe; instalacao para a maquina: Arquivos de Programas.
814
+ const roots = [env.LOCALAPPDATA, env.ProgramFiles, env['ProgramFiles(x86)']].filter(Boolean);
815
+ return roots.some((root) => {
816
+ const appDir = path.join(root, '1Password', 'app');
817
+ try {
818
+ return fs.readdirSync(appDir).some((v) => fs.existsSync(path.join(appDir, v, '1Password.exe')));
819
+ } catch {
820
+ return false;
821
+ }
822
+ });
823
+ }
824
+ return null;
825
+ }
826
+
827
+ /**
828
+ * Confere a assinatura Authenticode do instalador ANTES de rodar: precisa ser valida e do publicador "Agilebits".
829
+ * O caminho do arquivo vai por variavel de ambiente (nunca dentro do texto do comando). Nao lanca.
830
+ */
831
+ function checkInstallerSignature(file, run = spawnSync) {
832
+ const script = "$s = Get-AuthenticodeSignature -LiteralPath $env:AUROQ_INSTALADOR; $a = ''; if ($s.SignerCertificate) { $a = $s.SignerCertificate.Subject }; Write-Output ($s.Status.ToString() + '|' + $a)";
833
+ let r;
834
+ try {
835
+ r = run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
836
+ encoding: 'utf8', windowsHide: true, timeout: 120000, env: { ...opEnv(), AUROQ_INSTALADOR: file },
837
+ });
838
+ } catch (e) {
839
+ return { ok: false, motivo: `nao consegui conferir a assinatura (${oneLine(e && e.message, 80)})` };
840
+ }
841
+ if (!r || r.error || r.status !== 0) return { ok: false, motivo: 'nao consegui conferir a assinatura' };
842
+ const line = String(r.stdout || '').trim().split(/\r?\n/).pop() || '';
843
+ const cut = line.indexOf('|');
844
+ const status = cut === -1 ? line : line.slice(0, cut);
845
+ const subject = cut === -1 ? '' : line.slice(cut + 1);
846
+ if (status !== 'Valid') return { ok: false, motivo: `a assinatura do arquivo nao e valida (${oneLine(status, 40) || 'sem resposta'})` };
847
+ if (!APP_SIGNER_RE.test(subject)) return { ok: false, motivo: 'o arquivo e assinado por outro publicador, nao pelo 1Password' };
848
+ return { ok: true, assunto: oneLine(subject, 120) };
849
+ }
850
+
851
+ /** Baixa um arquivo grande direto para o disco (https so). `curl` primeiro (respeita o proxy do sistema); reserva: fetch do Node. */
852
+ async function downloadToFile(url, dest) {
853
+ const curl = process.platform === 'win32' ? 'curl.exe' : 'curl';
854
+ const r = spawnSync(curl, ['-fL', '--proto', '=https', '--proto-redir', '=https', '--silent', '--show-error', '--connect-timeout', '30', '--max-time', '900', '-o', dest, url], {
855
+ encoding: 'utf8', windowsHide: true, timeout: 16 * 60 * 1000, stdio: ['ignore', 'ignore', 'pipe'], env: opEnv(),
856
+ });
857
+ if (!r.error && r.status === 0 && fs.existsSync(dest)) return;
858
+ if (typeof fetch !== 'function') throw new Error(`download falhou (${r.error ? r.error.code || r.error.message : `curl saiu com ${r.status}`})`);
859
+ try { fs.rmSync(dest, { force: true }); } catch { /* recomeca */ }
860
+ const res = await fetch(url, { redirect: 'follow', headers: { 'User-Agent': 'auroq-os' } });
861
+ if (!res.ok || !res.body) throw new Error(`download falhou (HTTP ${res.status})`);
862
+ const { Readable } = require('stream');
863
+ const { pipeline } = require('stream/promises');
864
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest, { flags: 'w' }));
865
+ }
866
+
867
+ /** Roda um programa sem terminal. Nunca lanca. O ambiente sai sem a chave do agente (opEnv). */
868
+ function runProgram(cmd, args, { timeoutMs = 15 * 60 * 1000 } = {}) {
869
+ const r = spawnSync(cmd, args, { encoding: 'utf8', env: opEnv(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
870
+ return {
871
+ ok: !r.error && r.status === 0,
872
+ status: r.status,
873
+ stdout: r.stdout || '',
874
+ stderr: r.stderr || '',
875
+ timedOut: !!(r.error && r.error.code === 'ETIMEDOUT'),
876
+ missing: !!(r.error && r.error.code === 'ENOENT'),
877
+ };
878
+ }
879
+
880
+ function makeAppDeps() {
881
+ return {
882
+ platform: process.platform,
883
+ lock: makeLock(),
884
+ hasCommand,
885
+ appInstalled: () => appInstalled(),
886
+ refreshPath: refreshWindowsPath,
887
+ run: runProgram,
888
+ download: downloadToFile,
889
+ checkSignature: checkInstallerSignature,
890
+ mkdtemp: () => fs.mkdtempSync(path.join(os.tmpdir(), 'auroq-1p-app-')),
891
+ out: { log, step, ok, warn, err },
892
+ };
893
+ }
894
+
895
+ const APP_NEXT_STEPS = 'Agora abra o app do 1Password, entre na sua conta (ou crie uma: guarde a Secret Key e o Kit de Emergencia que ele oferece) e ligue "Integrar com a CLI do 1Password" em Configuracoes > Desenvolvedor. Depois rode o conectar-1password de novo.';
896
+
897
+ /**
898
+ * Instala o app do 1Password (`conectar-1password --instalar-app`). Windows: winget e, se ele falhar, o instalador
899
+ * oficial, so rodado depois de conferir a assinatura (valida e do publicador Agilebits). Mac: Homebrew (cask).
900
+ * O app e um programa novo no computador do aluno: quem chama pede o OK dele antes.
901
+ */
902
+ async function installApp(deps = {}) {
903
+ const d = { ...makeAppDeps(), ...deps };
904
+ const out = d.out;
905
+ if (d.platform !== 'darwin' && d.platform !== 'win32') {
906
+ return fim('erro', 'app-nao-instalou', 'A instalacao automatica do app so existe no macOS e no Windows. Baixe em https://1password.com/downloads');
907
+ }
908
+ if (!d.lock.acquire()) {
909
+ return fim('aguardando', 'outra-execucao', 'Ja tem outra execucao do conectar-1password em andamento neste computador. Espere ela terminar e rode de novo; nao instalei nada em duplicidade.');
910
+ }
911
+ try {
912
+ out.step('Verificando o app do 1Password...');
913
+ if (d.appInstalled()) {
914
+ out.ok('O app do 1Password ja esta instalado.');
915
+ return fim('aguardando', 'app-instalado', `O app do 1Password ja esta instalado neste computador. ${APP_NEXT_STEPS}`);
916
+ }
917
+ const falhou = (msg) => fim('erro', 'app-nao-instalou', `${msg} Instale a mao em https://1password.com/downloads e rode o conectar-1password de novo.`);
918
+ const pronto = (via) => {
919
+ d.refreshPath();
920
+ out.ok(`App do 1Password instalado (${via}).`);
921
+ return fim('aguardando', 'app-instalado', `App do 1Password instalado. ${APP_NEXT_STEPS}`);
922
+ };
923
+
924
+ if (d.platform === 'darwin') {
925
+ if (!d.hasCommand('brew')) return falhou('Homebrew nao encontrado, entao nao consigo instalar o app daqui.');
926
+ out.log(`${DIM}Instalando via Homebrew (pode levar um minuto)...${RESET}`);
927
+ const r = d.run('brew', ['install', '--cask', '1password']);
928
+ if (r.ok || d.appInstalled()) return pronto('Homebrew');
929
+ return falhou(`A instalacao via Homebrew falhou (${r.timedOut ? 'demorou demais' : `codigo de saida ${r.status}`}).`);
930
+ }
931
+
932
+ // Windows 1: winget (rapido, mas falhou no teste real com 0x80070002 mesmo depois de conferir o hash)
933
+ if (d.hasCommand('winget')) {
934
+ out.log(`${DIM}Instalando via winget (pode levar um minuto)...${RESET}`);
935
+ const w = d.run('winget', ['install', '--id', WINGET_APP_ID, '-e', '--source', 'winget', '--accept-source-agreements', '--accept-package-agreements', '--silent']);
936
+ d.refreshPath();
937
+ if (w.ok && d.appInstalled()) return pronto('winget');
938
+ out.warn(`O winget nao conseguiu instalar o app (${w.timedOut ? 'demorou demais' : `codigo de saida ${w.status === null || w.status === undefined ? 'desconhecido' : w.status}`}). Vou usar o instalador oficial do 1Password.`);
939
+ if (d.appInstalled()) return pronto('winget');
940
+ } else {
941
+ out.warn('O winget nao esta disponivel. Vou usar o instalador oficial do 1Password.');
942
+ }
943
+
944
+ // Windows 2: instalador oficial, com a assinatura conferida ANTES de rodar
945
+ let dir = null;
946
+ try {
947
+ dir = d.mkdtemp();
948
+ const file = path.join(dir, '1PasswordSetup.exe');
949
+ out.log(`${DIM}Baixando o instalador oficial (${APP_INSTALLER_URL})...${RESET}`);
950
+ try {
951
+ await d.download(APP_INSTALLER_URL, file);
952
+ } catch (e) {
953
+ return falhou(`Nao consegui baixar o instalador oficial (${oneLine(e && e.message, 120)}).`);
954
+ }
955
+ const sig = d.checkSignature(file);
956
+ if (!sig.ok) return falhou(`Nao executei o instalador baixado: ${sig.motivo}.`);
957
+ out.ok(`Assinatura conferida: ${sig.assunto || 'Agilebits'}`);
958
+ const r = d.run(file, ['--silent']);
959
+ if (!r.ok) return falhou(`O instalador oficial nao terminou bem (${r.timedOut ? 'demorou demais' : `codigo de saida ${r.status}`}).`);
960
+ return pronto('instalador oficial');
961
+ } finally {
962
+ if (dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* fica na pasta temporaria do usuario */ } }
963
+ }
964
+ } finally {
965
+ d.lock.release();
966
+ }
967
+ }
968
+
770
969
  // Pasta do aluno fora do git, a mesma onde ficam as credenciais do Auroq (~/.arcane). Aqui vivem a trava de
771
970
  // instancia unica e o estado das etapas (nenhum segredo).
772
971
  function auroqDir() {
@@ -905,6 +1104,7 @@ function makeDefaultDeps() {
905
1104
  platform: process.platform,
906
1105
  openUrl,
907
1106
  refreshPath: refreshWindowsPath,
1107
+ appInstalled: () => appInstalled(),
908
1108
  lock: makeLock(),
909
1109
  state: { read: readState, write: writeState },
910
1110
  envFileProblem,
@@ -1017,11 +1217,45 @@ function backupItemTemplate(token) {
1017
1217
  function saveBackup(d, account, token) {
1018
1218
  // Sem --vault: o `op` guarda no cofre pessoal da conta, que se chama Personal, Private ou Employee
1019
1219
  // conforme o plano (e pode vir traduzido), por isso nunca se usa o nome.
1020
- const r = d.op(
1021
- ['item', 'create', '-', '--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'],
1022
- { env: opEnv(), input: JSON.stringify(backupItemTemplate(token)) },
1023
- );
1024
- return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1220
+ const body = JSON.stringify(backupItemTemplate(token));
1221
+ const common = ['--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'];
1222
+ if (d.platform !== 'win32') {
1223
+ const r = d.op(['item', 'create', '-', ...common], { env: opEnv(), input: body });
1224
+ return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1225
+ }
1226
+ // Windows: o `op item create -` por pipe falha no PowerShell 5.1 ("invalid JSON in piped input", teste real de 27/10),
1227
+ // entao o molde vai num arquivo temporario (UTF-8 sem BOM) so deste usuario, que o `op` le com --template. O token
1228
+ // continua fora de argumento e de log; o arquivo e apagado em seguida, aconteca o que acontecer. O `op` roda com o
1229
+ // stdin desligado (`noStdin`): com `--template` ele nao aceita nenhum pipe, nem vazio.
1230
+ let tmp = null;
1231
+ try {
1232
+ tmp = writeTemplateFile(body);
1233
+ } catch (e) {
1234
+ return { ok: false, detalhe: `nao consegui preparar o arquivo temporario do molde (${oneLine(e && e.message, 120)})` };
1235
+ }
1236
+ try {
1237
+ const r = d.op(['item', 'create', '--template', tmp.file, ...common], { env: opEnv(), noStdin: true });
1238
+ return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1239
+ } finally {
1240
+ try { fs.rmSync(tmp.dir, { recursive: true, force: true }); } catch { /* fica na pasta temporaria do usuario */ }
1241
+ }
1242
+ }
1243
+
1244
+ /**
1245
+ * Grava `content` num arquivo novo, dentro de uma pasta nova e de nome aleatorio na pasta temporaria do usuario
1246
+ * (%TEMP% do perfil, que so ele, o SYSTEM e os administradores leem; o modo 0600 vale no Mac e no Linux e e ignorado no
1247
+ * Windows). UTF-8 SEM BOM: o BOM faria o `op` recusar o JSON. Devolve { dir, file }; quem chama apaga a pasta.
1248
+ */
1249
+ function writeTemplateFile(content) {
1250
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'auroq-op-'));
1251
+ const file = path.join(dir, 'molde.json');
1252
+ try {
1253
+ fs.writeFileSync(file, Buffer.from(content, 'utf8'), { flag: 'wx', mode: 0o600 });
1254
+ } catch (e) {
1255
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* nada a fazer */ }
1256
+ throw e;
1257
+ }
1258
+ return { dir, file };
1025
1259
  }
1026
1260
 
1027
1261
  /**
@@ -1084,12 +1318,57 @@ function showIntegrationHelp(d, out) {
1084
1318
  out.log('');
1085
1319
  }
1086
1320
 
1087
- const APP_PRIORITY_MSG = 'O 1Password continua respondendo pelo app (pedindo digital) em vez de usar a chave. Sem isso o agente nao fica sem pedir digital. Nao criei nada novo; peca ajuda ao Arcano com este codigo: app-tem-prioridade.';
1321
+ /**
1322
+ * Ajuda quando a conta aparece mas a aprovacao nao passou. Nao abre a tela do app (a chave provavelmente ja esta
1323
+ * ligada): so lista o que conferir. `semSessao`: o `op` disse que nao ha sessao, entao tambem lembra da chave do app.
1324
+ */
1325
+ function showApprovalHelp(d, out, semSessao) {
1326
+ out.warn('O app do 1Password nao liberou o acesso ainda. Confira:');
1327
+ const passos = ['Abra o app do 1Password e deixe-o desbloqueado (com a sua senha).'];
1328
+ if (semSessao) passos.push('Em Configuracoes > Desenvolvedor, deixe marcado "Integrar com a CLI do 1Password" (Integrate with 1Password CLI).');
1329
+ passos.push('Quando a janela da digital aparecer, aprove.');
1330
+ passos.push(...biometricSteps(d.platform).map((s, i) => (i === 0 ? `Se a digital nunca aparece: ${s}` : s)));
1331
+ passos.forEach((line, i) => out.log(` ${i + 1}. ${line}`));
1332
+ out.log('');
1333
+ }
1334
+
1335
+ const APPROVAL_REFUSED_RE = /(dismiss|cancel|declin|denied|reject|refus)/i;
1336
+
1337
+ /**
1338
+ * Vale tentar o `op signin`? So quando o `op` disse que nao ha sessao. Janela da digital dispensada ou negada pelo
1339
+ * aluno nao se repete sozinha (seriam mais janelas na cara dele), nem tempo esgotado.
1340
+ */
1341
+ function needsSignin(r) {
1342
+ if (!r || r.timedOut) return false;
1343
+ if (APPROVAL_REFUSED_RE.test(String(r.stderr || ''))) return false;
1344
+ return ['sem-sessao', 'sessao'].includes(classifyFailure(r));
1345
+ }
1346
+
1347
+ /**
1348
+ * Pede a aprovacao do app. O `op whoami` NAO serve: ele nao abre a janela da digital e, sem sessao aberta, responde na
1349
+ * hora "account is not signed in" (teste real no Windows, 27/10). Quem pede a aprovacao e qualquer comando que precise
1350
+ * de login; o exemplo da documentacao oficial da integracao com o app e o `op vault list` (so leitura). Segunda
1351
+ * tentativa, so se a primeira voltar "sem sessao" (needsSignin): `op signin --account`. A sessao que o app abre pertence a este
1352
+ * processo: os `op` seguintes sao filhos do mesmo node e a reaproveitam (e por isso a aprovacao NUNCA e feita num
1353
+ * processo a parte).
1354
+ */
1355
+ function requestApproval(d, account) {
1356
+ const key = accountKey(account);
1357
+ const approve = () => d.op(['vault', 'list', '--account', key, '--format', 'json'], { env: opEnv(), timeoutMs: APPROVAL_TIMEOUT_MS });
1358
+ let r = approve();
1359
+ if (!r.ok && needsSignin(r)) {
1360
+ d.op(['signin', '--account', key], { env: opEnv(), timeoutMs: APPROVAL_TIMEOUT_MS }); // a saida nao interessa (e nunca e mostrada)
1361
+ r = approve();
1362
+ }
1363
+ return r;
1364
+ }
1365
+
1366
+ const APP_PRIORITY_MSG ='O 1Password continua respondendo pelo app (pedindo digital) em vez de usar a chave. Sem isso o agente nao fica sem pedir digital. Nao criei nada novo; peca ajuda ao Arcano com este codigo: app-tem-prioridade.';
1088
1367
  const NET_STRONG_RE = /(dial tcp|no such host|tls handshake|network is unreachable|no route to host|i\/o timeout)/i;
1089
1368
 
1090
1369
  // Erros que o aluno (ou o plano dele) resolve: a linha AUROQ_RESULTADO ja diz o que fazer. Saem com 1 mas a
1091
1370
  // telemetria do instalador nao os conta como instalacao quebrada.
1092
- const STUDENT_ACTIONABLE = new Set(['sem-op', 'op-antigo', 'rede', 'conta-nao-encontrada', 'conta-ambigua', 'cofre-duplicado', 'sem-permissao', 'token-indisponivel', 'arquivo-ambiente']);
1371
+ const STUDENT_ACTIONABLE = new Set(['sem-op', 'op-antigo', 'rede', 'conta-nao-encontrada', 'conta-ambigua', 'cofre-duplicado', 'sem-permissao', 'token-indisponivel', 'arquivo-ambiente', 'app-nao-instalou']);
1093
1372
 
1094
1373
  function isStudentActionable(codigo) {
1095
1374
  return STUDENT_ACTIONABLE.has(codigo);
@@ -1203,6 +1482,11 @@ async function runConnect(opts, d, prog) {
1203
1482
  // 2. Sessao do aluno: o app do 1Password com a chave "Integrate with 1Password CLI" ligada
1204
1483
  out.step('Procurando a sua conta no app do 1Password...');
1205
1484
  let accounts = listAccounts(d);
1485
+ if (!accounts.length && d.appInstalled() === false) {
1486
+ // Sem app nao ha o que ligar: dizer "ligue a chave" mandaria o aluno procurar uma tela que nao existe (teste real de 27/10).
1487
+ prog.reset('app');
1488
+ return fim('aguardando', 'instalar-app', 'Nao achei o app do 1Password neste computador. Instale com: npx -y auroq-os@latest conectar-1password --instalar-app --agente (ou baixe em https://1password.com/downloads). Se ele ja esta instalado num lugar fora do comum, abra o app, entre na sua conta e ligue "Integrar com a CLI do 1Password" em Configuracoes > Desenvolvedor; depois rode de novo.');
1489
+ }
1206
1490
  if (!accounts.length) {
1207
1491
  showIntegrationHelp(d, out);
1208
1492
  if (waitSeconds > 0) {
@@ -1242,18 +1526,22 @@ async function runConnect(opts, d, prog) {
1242
1526
  out.ok(`Conta: ${accountLabel(account)}`);
1243
1527
 
1244
1528
  out.step('Pedindo sua aprovacao ao app do 1Password...');
1245
- out.log(`${BOLD}Vai abrir uma janela do 1Password pedindo a sua digital (Touch ID / Windows Hello) ou a senha. Aprove. Se ela aparecer mais de uma vez, e so aprovar de novo.${RESET}`);
1246
- const auth = d.op(['whoami', '--account', accountKey(account), '--format', 'json'], { env: opEnv(), timeoutMs: 180000 });
1529
+ const digital = d.platform === 'win32' ? 'Windows Hello' : (d.platform === 'darwin' ? 'Touch ID' : 'Touch ID / Windows Hello');
1530
+ out.log(`${BOLD}Vai abrir uma janela do 1Password pedindo a sua digital (${digital}) ou a senha. Aprove. Se ela aparecer mais de uma vez, e so aprovar de novo.${RESET}`);
1531
+ const auth = requestApproval(d, account);
1247
1532
  if (!auth.ok) {
1248
1533
  const text = String(auth.stderr || '');
1249
1534
  if (NET_STRONG_RE.test(text)) return fim('erro', 'rede', `Sem conexao com o 1Password. ${oneLine(text)}`.trim());
1250
- if (!auth.timedOut && classifyFailure(auth) === 'sem-sessao') {
1251
- // Conta so adicionada na mao (`op account add`): aparece na lista mas sem sessao; falta a chave do app.
1252
- showIntegrationHelp(d, out);
1253
- return fim('aguardando', 'ligar-integracao', 'Falta ligar "Integrate with 1Password CLI" nas configuracoes do app do 1Password (Configuracoes > Desenvolvedor). Depois e so rodar de novo.');
1254
- }
1535
+ // A conta JA apareceu na lista do app, entao a chave "Integrar com a CLI" nao e o que falta: nunca devolve
1536
+ // 'ligar-integracao' aqui (o laco do teste real no Windows: o aluno ligava a chave, dizia "pronto" e ouvia de novo
1537
+ // que faltava ligar). Falta a aprovacao no app, ou, so no caso da conta adicionada na mao (`op account add`), a chave.
1538
+ const semSessao = !auth.timedOut && ['sem-sessao', 'sessao'].includes(classifyFailure(auth));
1539
+ showApprovalHelp(d, out, semSessao);
1255
1540
  const detail = oneLine(text, 160);
1256
- return fim('aguardando', 'aprovar-no-app', `Nao consegui entrar na conta${auth.timedOut ? ': a janela da digital nao foi aprovada a tempo' : ''}. Confira se o app do 1Password esta aberto e desbloqueado, aprove a janela da digital e rode de novo.${detail ? ` (O 1Password disse: ${detail})` : ''}`);
1541
+ const falta = semSessao
1542
+ ? 'A conta aparece no app, mas ele nao liberou o acesso. Desbloqueie o app, confira "Integrar com a CLI" e aprove a janela (Windows: o Windows Hello precisa estar ligado). Rode de novo.'
1543
+ : 'Abra e desbloqueie o app do 1Password e aprove a janela da digital (no Windows, o Windows Hello precisa estar ligado). Rode de novo.';
1544
+ return fim('aguardando', 'aprovar-no-app', `Nao consegui entrar na conta${auth.timedOut ? ': a janela da digital nao foi aprovada a tempo' : ''}. ${falta}${detail ? ` (O 1Password disse: ${detail})` : ''}`);
1257
1545
  }
1258
1546
  prog.mark('aprovacao');
1259
1547
  out.ok('Aprovado');
@@ -1486,7 +1774,7 @@ function showState(st) {
1486
1774
  // ─── Entrada do comando ──────────────────────────────────
1487
1775
 
1488
1776
  /**
1489
- * `auroq-os conectar-1password [--agente] [--conta X] [--rotacionar] [--esperar N] [--manual]`
1777
+ * `auroq-os conectar-1password [--agente] [--conta X] [--rotacionar] [--esperar N] [--manual] [--instalar-app]`
1490
1778
  * Codigo de saida: 0 conectado OU falta uma acao do aluno (ligar a chave no app, escolher conta: a
1491
1779
  * linha AUROQ_RESULTADO diz qual), 1 erro.
1492
1780
  */
@@ -1503,7 +1791,7 @@ async function connect(options = {}) {
1503
1791
  if (!opts.agente) console.log(`\n${PURPLE}${BOLD} Conectar 1Password — Auroq OS${RESET}`);
1504
1792
  let result;
1505
1793
  try {
1506
- result = await connectAutomatic(opts);
1794
+ result = opts.instalarApp ? await installApp() : await connectAutomatic(opts);
1507
1795
  } catch (e) {
1508
1796
  // Excecao inesperada: o Ops sempre recebe a linha de resultado (e a mensagem nunca leva token).
1509
1797
  result = fim('erro', 'falha', `Falha inesperada: ${oneLine(e && e.message)}`);
@@ -1556,6 +1844,13 @@ module.exports = {
1556
1844
  shellKind,
1557
1845
  freshShellFor,
1558
1846
  clearClipboard,
1847
+ requestApproval,
1848
+ biometricSteps,
1849
+ appInstalled,
1850
+ installApp,
1851
+ checkInstallerSignature,
1852
+ writeTemplateFile,
1853
+ APP_INSTALLER_URL,
1559
1854
  ETAPAS,
1560
1855
  makeLock,
1561
1856
  isPersonalAccount,
@@ -2,7 +2,7 @@
2
2
  "schema": 1,
3
3
  "algorithm": "sha256",
4
4
  "scope": "packaged-runtime-and-cascas",
5
- "version": "2.8.2",
5
+ "version": "2.8.5",
6
6
  "files": [
7
7
  {
8
8
  "path": ".auroq-core/constitution.md",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": ".auroq-core/core-config.yaml",
13
- "sha256": "5279389b3f0227711bc930cf2b30f09672ac86e9c85b0fa9e8ca2005b97e3395"
13
+ "sha256": "dd74b039850ad9a109e644f60489b65745c85a86295c694c478e341026e00ad8"
14
14
  },
15
15
  {
16
16
  "path": ".auroq-core/core/synapse/context/context-builder.js",
@@ -158,7 +158,7 @@
158
158
  },
159
159
  {
160
160
  "path": ".claude/commands/AuroqOS/agents/ops.md",
161
- "sha256": "03442300808805fe99ac558b21fdc80b1bdffc034929bdd5d0526db6f7040547"
161
+ "sha256": "6c615d5d99781865b0256be9166837d851fdd5eafdf02a48b9c7c0ce5764c793"
162
162
  },
163
163
  {
164
164
  "path": ".claude/commands/auroq-companion.md",
@@ -188,6 +188,10 @@
188
188
  "path": ".claude/hooks/ahi/worker.cjs",
189
189
  "sha256": "3eabd293f3c9c6887f6850ca4be9ad7021d6659967f2f5eecadefb2bb78e9b85"
190
190
  },
191
+ {
192
+ "path": ".claude/hooks/auroq-ate-o-fim.cjs",
193
+ "sha256": "eafc89d69d18a7ff821f586715e703e3b8fd4c4c4a941000e0fca9cbe2bf98c7"
194
+ },
191
195
  {
192
196
  "path": ".claude/hooks/auroq-sync.cjs",
193
197
  "sha256": "f916fbac4b1236dded1f5253807a5147674081e416ebe215bebd6d6f97b1469b"
@@ -202,7 +206,7 @@
202
206
  },
203
207
  {
204
208
  "path": ".claude/settings.json",
205
- "sha256": "cec96fc50b88a418cf880fa8f96272b0dd3d7092459b72e7f495613e5e022fb9"
209
+ "sha256": "a059b6e15cfe6822f00d1a39c563d6335839c8ac63a5763f6a4cf1af204df284"
206
210
  },
207
211
  {
208
212
  "path": ".claude/settings.local.json",
@@ -210,7 +214,7 @@
210
214
  },
211
215
  {
212
216
  "path": "bin/auroq-os.js",
213
- "sha256": "f6486360804d4410475e8e750818395847695320c11e3411690e066192c91dfa"
217
+ "sha256": "937e6e3b748e05f324b196891ea46663b950aa12e1ca20c9aa9dacf6d2bd8962"
214
218
  },
215
219
  {
216
220
  "path": "lib/ahi/client.mjs",
@@ -250,7 +254,7 @@
250
254
  },
251
255
  {
252
256
  "path": "lib/cofre.js",
253
- "sha256": "98e3a471b613cd26a23aeb8affc0b6d2a1de881fade35a6902db1c8e1fa45c2f"
257
+ "sha256": "8dff20e576fda737f5039ceaca64e8e18fe79e19460c3ce0bac1262f7fa490df"
254
258
  },
255
259
  {
256
260
  "path": "lib/credentials.js",
@@ -278,7 +282,7 @@
278
282
  },
279
283
  {
280
284
  "path": "lib/onepassword.js",
281
- "sha256": "0be8cb4e4f7bf1ff5ae3f0ad1ac26f8402963908c0b4010253becc7decf8b34b"
285
+ "sha256": "0f5ec74682668925ea078334dcceeb2bbe32e2aad7211b57ad47a41230d64146"
282
286
  },
283
287
  {
284
288
  "path": "lib/supabase.js",
@@ -294,7 +298,7 @@
294
298
  },
295
299
  {
296
300
  "path": "package.json",
297
- "sha256": "6a645f5cc731d474415f71b3af759c8109d31c7c2500ccd71a019617854f98e7"
301
+ "sha256": "43885cac0063a63f1d1bb1ad5281daaec8a0c2de8c240c23708073f356dd0c2a"
298
302
  },
299
303
  {
300
304
  "path": "scripts/build-pack.mjs",
@@ -314,7 +318,7 @@
314
318
  },
315
319
  {
316
320
  "path": "scripts/runtime-integrity.mjs",
317
- "sha256": "3d9c15e96e58f9eb883edab46533ddaacc18895c5255f06767de3f149469efb2"
321
+ "sha256": "e185ab4937628ac1be733f087638a4121b3c16e0c2a3ea76fd5c68cae571f802"
318
322
  },
319
323
  {
320
324
  "path": "scripts/sync-codex-skills.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auroq-os",
3
- "version": "2.8.2",
3
+ "version": "2.8.5",
4
4
  "arcaneMcpPin": "0.7.1",
5
5
  "description": "Auroq OS — Sistema Operacional de IA para o negócio do aluno da Arcane",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "auroq:sync:codex:check": "npm run sync:codex:check",
20
20
  "auroq:validate": "npm run validate:hybrid",
21
21
  "validate:hybrid": "node scripts/validate-hybrid.mjs",
22
- "lint": "node --check bin/auroq-os.js && node --check lib/barra/index.js && node --check lib/barra/barra.cjs && node --check lib/auth.js && node --check lib/diagnostico.js && node --check lib/credentials.js && node --check lib/onepassword.js && node --check lib/infisical.js && node --check lib/infisical-api.js && node --check lib/infisical-cli.js && node --check lib/cofre.js && node --check lib/supabase.js && node --check scripts/sync-codex-skills.mjs && node --check scripts/validate-hybrid.mjs && node --check scripts/prepack-casca.mjs && node --check scripts/postpack-casca.mjs && node --check scripts/assert-mcp-pin.mjs && node --check lib/telemetry.mjs && node --check scripts/synapse-hashes.mjs && node --check lib/ahi/historico.mjs && node --check .claude/hooks/ahi-capture.cjs && node --check .claude/hooks/precompact-session-digest.cjs && node --check .claude/hooks/ahi/security.cjs && node --check .claude/hooks/ahi/runtime.cjs && node --check .claude/hooks/ahi/worker.cjs && node --check lib/ahi/install.cjs && node --check lib/codex-windows.cjs && node --check .claude/hooks/ahi/auth-config.cjs && node --check lib/ahi/client.mjs && node --check scripts/build-pack.mjs && node --check scripts/assert-native-release.mjs && node --check scripts/runtime-integrity.mjs && node --check scripts/pack-state.mjs && node --check tests/rodar.mjs",
22
+ "lint": "node --check bin/auroq-os.js && node --check lib/barra/index.js && node --check lib/barra/barra.cjs && node --check lib/auth.js && node --check lib/diagnostico.js && node --check lib/credentials.js && node --check lib/onepassword.js && node --check lib/infisical.js && node --check lib/infisical-api.js && node --check lib/infisical-cli.js && node --check lib/cofre.js && node --check lib/supabase.js && node --check scripts/sync-codex-skills.mjs && node --check scripts/validate-hybrid.mjs && node --check scripts/prepack-casca.mjs && node --check scripts/postpack-casca.mjs && node --check scripts/assert-mcp-pin.mjs && node --check lib/telemetry.mjs && node --check scripts/synapse-hashes.mjs && node --check lib/ahi/historico.mjs && node --check .claude/hooks/ahi-capture.cjs && node --check .claude/hooks/precompact-session-digest.cjs && node --check .claude/hooks/auroq-ate-o-fim.cjs && node --check .claude/hooks/ahi/security.cjs && node --check .claude/hooks/ahi/runtime.cjs && node --check .claude/hooks/ahi/worker.cjs && node --check lib/ahi/install.cjs && node --check lib/codex-windows.cjs && node --check .claude/hooks/ahi/auth-config.cjs && node --check lib/ahi/client.mjs && node --check scripts/build-pack.mjs && node --check scripts/assert-native-release.mjs && node --check scripts/runtime-integrity.mjs && node --check scripts/pack-state.mjs && node --check tests/rodar.mjs",
23
23
  "typecheck": "npm run validate:hybrid",
24
24
  "test": "node tests/rodar.mjs",
25
25
  "release:verify": "node scripts/assert-mcp-pin.mjs && node scripts/assert-native-release.mjs",
@@ -25,7 +25,7 @@ export const REQUIRED_FILES = [
25
25
  ...SYNAPSE_LAYERS.map(layer => `${SYNAPSE_ROOT}layers/${layer}.js`),
26
26
  '.claude/settings.json', '.claude/settings.local.json',
27
27
  '.claude/commands/AuroqOS/agents/ops.md', '.claude/commands/auroq-companion.md',
28
- '.claude/hooks/synapse-engine.cjs', '.claude/hooks/auroq-sync.cjs', '.claude/hooks/precompact-session-digest.cjs',
28
+ '.claude/hooks/synapse-engine.cjs', '.claude/hooks/auroq-sync.cjs', '.claude/hooks/precompact-session-digest.cjs', '.claude/hooks/auroq-ate-o-fim.cjs',
29
29
  '.claude/hooks/ahi-capture.cjs', '.claude/hooks/ahi/runtime.cjs',
30
30
  '.claude/hooks/ahi/security.cjs', '.claude/hooks/ahi/win-vault.ps1', '.claude/hooks/ahi/worker.cjs', '.claude/hooks/ahi/auth-config.cjs',
31
31
  ];
@@ -6,6 +6,15 @@
6
6
 
7
7
  **Ultima atualizacao:** {data}
8
8
  **Proximo review:** {data}
9
+ **Proxima revisao do mes:** {data}
10
+
11
+ ---
12
+
13
+ ## FOCO DO MES
14
+
15
+ > Definido na revisao do mes (1 a 3 focos). As revisoes da semana conferem se o trabalho serve a ele.
16
+
17
+ -
9
18
 
10
19
  ---
11
20