vault-go 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -241,6 +241,8 @@ local, atividade MCP recente, projetos e memórias consultados na nuvem.
241
241
  Atualiza a interface a cada 5 segundos enquanto visível e consulta a nuvem
242
242
  a cada 15 segundos. Sem conexão, conserva o último cache da mesma conta.
243
243
 
244
+ Abra http://localhost:38850 e clique em **Conectar ao Vault** para entrar pelo navegador. A senha permanece no portal; o painel libera a sessão somente após confirmar o retorno OAuth com PKCE. A tela oferece próximos passos para configurar clientes e registrar a primeira memória. A logo e as cores são as mesmas do portal.
245
+
244
246
  O serviço escuta exclusivamente em 127.0.0.1:38850. `local open` verifica
245
247
  uma prova criptográfica do serviço e abre uma sessão privada no navegador;
246
248
  o segredo local é removido do fragmento imediatamente e trocado por cookie
@@ -259,3 +261,32 @@ pelo MCP Vault, sem argumentos, consultas ou credenciais. Não captura toda a
259
261
  atividade da máquina nem importa automaticamente o banco do claude-mem em
260
262
  37701. A sincronização deste painel é de leitura da nuvem; as ferramentas
261
263
  MCP continuam responsáveis pelas gravações na API.
264
+
265
+ ### Sessões, dispositivos e proteção local
266
+
267
+ **Sair** encerra a sessão desta página e cancela suas prévias de contexto.
268
+ **Desconectar dispositivo** pede confirmação, revoga as credenciais do agente
269
+ na nuvem e remove a autenticação e o cache local após sucesso. Os arquivos
270
+ pessoais permanecem intactos. Para reconectar, entre novamente pelo navegador.
271
+ O portal mostra último contato, permissões e permite revogar cada agente.
272
+
273
+ O cache e a credencial de monitoramento usam AES-256-GCM. No macOS, a chave
274
+ fica no Keychain; nos demais sistemas, em arquivo privado `0600`. Os tokens
275
+ MCP existentes continuam em arquivo privado. O painel informa essas diferenças:
276
+ cache cifrado não significa que todas as memórias sejam criptografadas ponta a
277
+ ponta. O cofre de arquivos e o canal do terminal têm seus próprios protocolos.
278
+
279
+ ### Motor de contexto
280
+
281
+ Escolha **Vault AI Resume**, **Claude Subscription** ou **OpenAI Subscription**
282
+ no painel local. A disponibilidade é verificada antes da geração. Claude usa
283
+ `claude auth login`; OpenAI usa `codex login` com uma conta ChatGPT. O agente
284
+ não pede nem copia credenciais desses provedores. O modelo gerenciado depende
285
+ de configuração e quota no servidor Vault.
286
+
287
+ A geração exige texto e consentimento explícito, usa diretório temporário,
288
+ limites de entrada/saída e timeout, e permite cancelamento. CLIs recebem apenas
289
+ o texto informado, com ferramentas/configurações locais desativadas conforme
290
+ os recursos suportados. O resultado é uma prévia privada da sessão, disponível
291
+ para copiar; não é salvo automaticamente como memória. Os planos e limites do
292
+ provedor escolhido se aplicam. Cancelar não garante estorno de uso já iniciado.
Binary file
package/dist/activity.js CHANGED
@@ -1,29 +1,44 @@
1
- import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
1
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
4
  export function recordActivity(home, operation, ok, durationMs) {
5
5
  try {
6
6
  mkdirSync(home, { recursive: true, mode: 0o700 });
7
- const file = join(home, 'local-activity.jsonl');
7
+ const file = join(home, "local-activity.jsonl");
8
8
  // Bounded journal contains metadata only: never arguments, content or credentials.
9
9
  try {
10
10
  if (statSync(file).size > 2 * 1024 * 1024)
11
- writeFileSync(file, '', { mode: 0o600 });
11
+ writeFileSync(file, "", { mode: 0o600 });
12
12
  }
13
13
  catch { }
14
- appendFileSync(file, JSON.stringify({ id: randomUUID(), time: new Date().toISOString(), operation: operation.slice(0, 180), ok, durationMs: Math.round(durationMs) }) + '\n', { mode: 0o600 });
14
+ appendFileSync(file, JSON.stringify({
15
+ id: randomUUID(),
16
+ time: new Date().toISOString(),
17
+ operation: operation.slice(0, 180),
18
+ ok,
19
+ durationMs: Math.round(durationMs),
20
+ }) + "\n", { mode: 0o600 });
15
21
  chmodSync(file, 0o600);
16
22
  }
17
- catch { /* Observability must never prevent an MCP operation. */ }
23
+ catch {
24
+ /* Observability must never prevent an MCP operation. */
25
+ }
18
26
  }
19
27
  export function readActivity(home) {
20
28
  try {
21
- return readFileSync(join(home, 'local-activity.jsonl'), 'utf8').split('\n').filter(Boolean).slice(-100).flatMap(line => { try {
22
- return [JSON.parse(line)];
23
- }
24
- catch {
25
- return [];
26
- } }).reverse();
29
+ return readFileSync(join(home, "local-activity.jsonl"), "utf8")
30
+ .split("\n")
31
+ .filter(Boolean)
32
+ .slice(-100)
33
+ .flatMap((line) => {
34
+ try {
35
+ return [JSON.parse(line)];
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ })
41
+ .reverse();
27
42
  }
28
43
  catch {
29
44
  return [];
package/dist/auth.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare function validateExistingAuthentication(home: string, fetcher?: F
10
10
  export declare function openVaultBrowser(url: string): Promise<void>;
11
11
  export declare function authenticateWithBrowser(home: string, options?: {
12
12
  locale?: Locale;
13
+ callbackHtml?: string;
14
+ signal?: AbortSignal;
13
15
  fetcher?: Fetcher;
14
16
  openBrowser?: (url: string) => Promise<void>;
15
17
  timeoutMs?: number;
package/dist/auth.js CHANGED
@@ -128,7 +128,7 @@ export async function authenticateWithBrowser(home, options = {}) {
128
128
  const server = createServer((req, res) => {
129
129
  res.setHeader('Cache-Control', 'no-store');
130
130
  res.setHeader('Referrer-Policy', 'no-referrer');
131
- res.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'");
131
+ res.setHeader('Content-Security-Policy', "default-src 'none'; img-src data:; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'");
132
132
  let url;
133
133
  try {
134
134
  url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
@@ -160,11 +160,15 @@ export async function authenticateWithBrowser(home, options = {}) {
160
160
  }
161
161
  accepted = true;
162
162
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
163
- res.end(callbackPage(locale));
163
+ res.end(options.callbackHtml ?? callbackPage(locale));
164
164
  claim?.(code);
165
165
  });
166
166
  let timer;
167
+ const abort = () => rejectClaim?.(new Error(t.denied));
168
+ options.signal?.addEventListener('abort', abort, { once: true });
167
169
  try {
170
+ if (options.signal?.aborted)
171
+ throw new Error(t.denied);
168
172
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
169
173
  const address = server.address();
170
174
  if (!address || typeof address === 'string')
@@ -197,6 +201,7 @@ export async function authenticateWithBrowser(home, options = {}) {
197
201
  return { email: account.email, method: 'browser' };
198
202
  }
199
203
  finally {
204
+ options.signal?.removeEventListener('abort', abort);
200
205
  if (timer)
201
206
  clearTimeout(timer);
202
207
  server.closeAllConnections();
@@ -1,3 +1,3 @@
1
- import { type Locale } from './locale.js';
1
+ import { type Locale } from "./locale.js";
2
2
  /** All interpolated content is from the static locale catalog, never the callback URL. */
3
- export declare function callbackPage(locale: Locale): string;
3
+ export declare function callbackPage(locale: Locale, dashboardUrl?: string): string;
@@ -1,8 +1,33 @@
1
- import { copy } from './locale.js';
1
+ import { readFileSync } from "node:fs";
2
+ import { copy } from "./locale.js";
2
3
  /** All interpolated content is from the static locale catalog, never the callback URL. */
3
- export function callbackPage(locale) {
4
- const t = copy[locale];
4
+ export function callbackPage(locale, dashboardUrl) {
5
+ const base = copy[locale];
6
+ const local = {
7
+ pt: {
8
+ body: "Sua conexão está sendo concluída. Volte ao painel local para acompanhar suas memórias.",
9
+ step2: "Retorno seguro recebido nesta máquina",
10
+ step3: "Abra o painel para acompanhar a conexão",
11
+ back: "Voltar ao painel local",
12
+ },
13
+ en: {
14
+ body: "Your connection is being completed. Return to the local dashboard to follow your memories.",
15
+ step2: "Secure callback received on this machine",
16
+ step3: "Open the dashboard to follow the connection",
17
+ back: "Return to local dashboard",
18
+ },
19
+ es: {
20
+ body: "Tu conexión se está completando. Vuelve al panel local para consultar tus memorias.",
21
+ step2: "Retorno seguro recibido en esta máquina",
22
+ step3: "Abre el panel para consultar la conexión",
23
+ back: "Volver al panel local",
24
+ },
25
+ };
26
+ const t = dashboardUrl ? { ...base, ...local[locale] } : base;
27
+ if (dashboardUrl && !/^http:\/\/localhost:\d+\/$/.test(dashboardUrl))
28
+ throw new Error("Invalid dashboard callback");
29
+ const logo = readFileSync(new URL("../assets/vault-logo-glass.png", import.meta.url)).toString("base64");
5
30
  return `<!doctype html><html lang="${locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>AI Vault Memory · ${t.received}</title><style>
6
31
  *{box-sizing:border-box}body{margin:0;min-height:100svh;display:grid;place-items:center;padding:24px;color:#f1f3fc;background:radial-gradient(ellipse at 20% 10%,#38305a60,transparent 50%),radial-gradient(ellipse at 90% 90%,#20394c40,transparent 55%),#080b12;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}.panel{width:min(100%,520px);padding:44px;border:1px solid #cdd8ff26;border-radius:32px;background:linear-gradient(135deg,#272c3edb,#131724ed);box-shadow:0 32px 100px #0008,inset 0 1px #ffffff10;animation:arrive .6s cubic-bezier(.2,.8,.2,1) both}.mark{display:block;margin:0 auto 28px;width:92px;height:92px;filter:drop-shadow(0 8px 25px #9990ff40)}.ribbon{stroke-dasharray:180;animation:draw 1.1s ease both}.ribbon:nth-child(2){animation-delay:.12s}.ribbon:nth-child(3){animation-delay:.24s}.brand{text-align:center;color:#b9b6ff;font-size:10px;letter-spacing:.22em;font-weight:650}h1{text-align:center;font-size:34px;letter-spacing:-.045em;line-height:1.1;margin:16px 0}.intro{text-align:center;color:#acb4cb;font-size:15px;line-height:1.7;margin:0 0 30px}.steps{list-style:none;padding:0;margin:0;border-top:1px solid #cdd8ff20}.steps li{display:flex;align-items:center;gap:14px;padding:15px 0;border-bottom:1px solid #cdd8ff15;font-size:13px;animation:arrive .5s both}.steps li:nth-child(2){animation-delay:.15s}.steps li:nth-child(3){animation-delay:.3s}.step{display:grid;place-items:center;flex-shrink:0;width:28px;height:28px;border:1px solid #b9b6ff40;background:#b9b6ff12;border-radius:9px;color:#c8c5ff;font-size:11px}.return{margin:26px 0 0;padding:16px 18px;background:#b9b6ff14;border:1px solid #b9b6ff30;border-radius:16px;color:#d4d1ff;font-size:14px;line-height:1.6}.privacy{margin:22px 0 0;color:#929db6;font-size:11px;line-height:1.7;text-align:center}@keyframes arrive{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes draw{from{stroke-dashoffset:180;opacity:0}to{stroke-dashoffset:0;opacity:1}}@media(max-width:480px){.panel{padding:30px 24px}h1{font-size:30px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important}}@media(forced-colors:active){.panel,.step,.return{border:1px solid CanvasText}svg{stroke:CanvasText}}
7
- </style></head><body><main class="panel"><svg class="mark" viewBox="0 0 100 100" fill="none" aria-hidden="true"><defs><linearGradient id="glass" x1="10" y1="10" x2="85" y2="90" gradientUnits="userSpaceOnUse"><stop stop-color="#e2edff"/><stop offset=".48" stop-color="#8b88df"/><stop offset="1" stop-color="#c4e6ff"/></linearGradient></defs><g stroke="url(#glass)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"><path class="ribbon" d="M12 20 50 84 88 20"/><path class="ribbon" d="M25 20 50 63 75 20"/><path class="ribbon" d="M38 20 50 41 62 20"/></g></svg><p class="brand">AI VAULT MEMORY</p><h1>${t.received}</h1><p class="intro">${t.body}</p><ol class="steps"><li><span class="step">01</span>${t.step1}</li><li><span class="step">02</span>${t.step2}</li><li><span class="step">03</span>${t.step3}</li></ol><p class="return">↗ ${t.back}</p><p class="privacy">${t.privacy}</p></main></body></html>`;
32
+ </style></head><body><main class="panel"><img class="mark" src="data:image/png;base64,${logo}" alt="AI Vault Memory"><p class="brand">AI VAULT MEMORY</p><h1>${t.received}</h1><p class="intro">${t.body}</p><ol class="steps"><li><span class="step">01</span>${t.step1}</li><li><span class="step">02</span>${t.step2}</li><li><span class="step">03</span>${t.step3}</li></ol><div class="return">${dashboardUrl ? `<a style="color:inherit" href="${dashboardUrl}">↗ ${t.back}</a>` : `↗ ${t.back}`}</div><p class="privacy">${t.privacy}</p></main></body></html>`;
8
33
  }
package/dist/cloud.d.ts CHANGED
@@ -35,6 +35,11 @@ export declare class VaultCloudClient implements VaultMemoryApi {
35
35
  private readonly journal;
36
36
  constructor(home?: string, cloudFetch?: typeof fetch, journal?: boolean);
37
37
  health(): Promise<unknown>;
38
+ observerStatus(): Promise<unknown>;
39
+ observerGenerate(text: string, signal?: AbortSignal): Promise<unknown>;
40
+ registerDevice(input: Record<string, unknown>): Promise<unknown>;
41
+ rotateDevice(id: string): Promise<unknown>;
42
+ deviceAction(id: string, token: string, action: 'heartbeat' | 'disconnect', body?: Record<string, unknown>): Promise<unknown>;
38
43
  projects(): Promise<unknown>;
39
44
  createProject(input: Record<string, unknown>): Promise<unknown>;
40
45
  startSession(input: Record<string, unknown>): Promise<unknown>;
@@ -68,3 +73,7 @@ export declare class VaultCloudClient implements VaultMemoryApi {
68
73
  private request;
69
74
  private performRequest;
70
75
  }
76
+ export declare class VaultCloudError extends Error {
77
+ readonly status: number;
78
+ constructor(status: number, message: string);
79
+ }
package/dist/cloud.js CHANGED
@@ -12,6 +12,26 @@ export class VaultCloudClient {
12
12
  async health() {
13
13
  return this.request('/health', { authenticated: false });
14
14
  }
15
+ async observerStatus() { return this.request('/memory/observer/status'); }
16
+ async observerGenerate(text, signal) {
17
+ return this.request('/memory/observer/generate', { method: 'POST', body: { text }, timeoutMs: 60_000, ...(signal ? { signal } : {}) });
18
+ }
19
+ async registerDevice(input) {
20
+ return this.request('/devices/register', { method: 'POST', body: input });
21
+ }
22
+ async rotateDevice(id) {
23
+ if (!/^[a-f0-9-]{36}$/.test(id))
24
+ throw new Error('Invalid device identifier.');
25
+ return this.request(`/devices/${id}/rotate`, { method: 'POST', body: {} });
26
+ }
27
+ async deviceAction(id, token, action, body = {}) {
28
+ if (!/^[a-f0-9-]{36}$/.test(id) || !/^vg_device_[a-f0-9]{64}$/.test(token))
29
+ throw new Error('Invalid device credential.');
30
+ const response = await this.cloudFetch(this.endpoint(`/devices/${id}/${action}`), { method: 'POST', redirect: 'error', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000) });
31
+ if (!response.ok)
32
+ throw new VaultCloudError(response.status, 'device_request_failed');
33
+ return response.status === 204 ? null : response.json();
34
+ }
15
35
  async projects() {
16
36
  return this.request('/memory/projects');
17
37
  }
@@ -146,6 +166,12 @@ export class VaultCloudClient {
146
166
  throw new Error('Sessão expirada. Execute `bunx --bun vault-go@latest login --force`.');
147
167
  }
148
168
  const refreshed = (await response.json());
169
+ // A refresh already in flight must not overwrite a newer login or logout.
170
+ const latest = loadTokens(this.home);
171
+ if (!latest)
172
+ throw new Error('Vault session was disconnected.');
173
+ if (latest.accessToken !== tokens.accessToken || latest.refreshToken !== tokens.refreshToken)
174
+ return latest.accessToken;
149
175
  const stored = {
150
176
  ...refreshed,
151
177
  expiresAt: Date.now() + (refreshed.expiresIn ?? 900) * 1000,
@@ -177,7 +203,7 @@ export class VaultCloudClient {
177
203
  method: options.method ?? 'GET',
178
204
  redirect: 'error',
179
205
  headers,
180
- signal: AbortSignal.timeout(15_000),
206
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs ?? 15_000)]) : AbortSignal.timeout(options.timeoutMs ?? 15_000),
181
207
  };
182
208
  if (options.body)
183
209
  request.body = JSON.stringify(options.body);
@@ -197,11 +223,19 @@ export class VaultCloudClient {
197
223
  const message = payload && typeof payload === 'object' && 'error' in payload
198
224
  ? String(payload.error)
199
225
  : `Vault respondeu HTTP ${response.status}`;
200
- throw new Error(message);
226
+ throw new VaultCloudError(response.status, message);
201
227
  }
202
228
  return payload;
203
229
  }
204
230
  }
231
+ export class VaultCloudError extends Error {
232
+ status;
233
+ constructor(status, message) {
234
+ super(message);
235
+ this.status = status;
236
+ this.name = 'VaultCloudError';
237
+ }
238
+ }
205
239
  function safeJson(text) {
206
240
  try {
207
241
  return JSON.parse(text);
@@ -0,0 +1,52 @@
1
+ export declare const CONTEXT_ENGINE_IDS: readonly ["vault-ai-resume", "claude-subscription", "openai-subscription"];
2
+ export type ContextEngineId = (typeof CONTEXT_ENGINE_IDS)[number];
3
+ export interface ContextResult {
4
+ title: string;
5
+ content: string;
6
+ facts: string[];
7
+ concepts: string[];
8
+ engine: ContextEngineId;
9
+ }
10
+ export interface EngineAvailability {
11
+ available: boolean;
12
+ reason?: string;
13
+ billing?: string;
14
+ }
15
+ export interface ContextEngine extends EngineAvailability {
16
+ id: ContextEngineId;
17
+ name: string;
18
+ }
19
+ export interface CommandRequest {
20
+ command: "claude" | "codex";
21
+ args: string[];
22
+ cwd: string;
23
+ env: NodeJS.ProcessEnv;
24
+ input?: string;
25
+ timeoutMs: number;
26
+ signal?: AbortSignal;
27
+ }
28
+ export type CommandRunner = (request: CommandRequest) => Promise<{
29
+ stdout: string;
30
+ stderr: string;
31
+ }>;
32
+ export declare function isContextEngine(value: unknown): value is ContextEngineId;
33
+ export declare function selectedContextEngine(home: string): ContextEngineId;
34
+ export declare function selectContextEngine(home: string, engine: unknown): ContextEngineId;
35
+ export declare function contextEnvironment(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
36
+ /** Never includes process stderr or model input in an error message. */
37
+ export declare const runContextCommand: CommandRunner;
38
+ export declare function listContextEngines(home: string, managedStatus?: EngineAvailability, options?: {
39
+ runner?: CommandRunner;
40
+ }): Promise<{
41
+ selected: ContextEngineId;
42
+ engines: ContextEngine[];
43
+ }>;
44
+ export declare function validateContextResult(value: unknown, engine: ContextEngineId): ContextResult;
45
+ export interface GenerateContextOptions {
46
+ signal?: AbortSignal;
47
+ runner?: CommandRunner;
48
+ managedGenerate?: (text: string, options: {
49
+ signal?: AbortSignal;
50
+ }) => Promise<unknown>;
51
+ }
52
+ export declare function generateContext(_home: string, engine: unknown, text: unknown, options?: GenerateContextOptions): Promise<ContextResult>;