vault-go 0.11.0 → 0.13.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.
@@ -1,89 +1,253 @@
1
- import { createServer } from 'node:http';
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
5
- import { loadConfig, loadTokens, vaultHome } from './config.js';
6
- import { VaultCloudClient } from './cloud.js';
7
- import { readActivity } from './activity.js';
8
- import { dashboardHtml, dashboardScript } from './local-dashboard.js';
9
- import pkg from '../package.json' with { type: 'json' };
1
+ import { createServer } from "node:http";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
5
+ import { authPath, loadConfig, loadTokens, vaultHome } from "./config.js";
6
+ import { VaultCloudClient } from "./cloud.js";
7
+ import { readActivity } from "./activity.js";
8
+ import { dashboardHtml, dashboardScript } from "./local-dashboard.js";
9
+ import pkg from "../package.json" with { type: "json" };
10
+ import { authenticateWithBrowser } from "./auth.js";
11
+ import { resolveLocale } from "./locale.js";
12
+ import { callbackPage } from "./callback-page.js";
13
+ import { privateStore } from "./private-store.js";
14
+ import { localDevice } from "./local-device.js";
15
+ import { generateContext, isApiEngine, isContextEngine, listContextEngines, saveEngineKey, selectContextEngine, selectedContextEngine, } from "./context-engines.js";
10
16
  export const LOCAL_PORT = 38850;
11
- const safeEqual = (a, b) => a.length === b.length && /^[A-Za-z0-9_-]{43}$/.test(a) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
17
+ const safeEqual = (a, b) => a.length === b.length &&
18
+ /^[A-Za-z0-9_-]{43}$/.test(a) &&
19
+ timingSafeEqual(Buffer.from(a), Buffer.from(b));
12
20
  export function localAccessToken(home) {
13
21
  mkdirSync(home, { recursive: true, mode: 0o700 });
14
22
  chmodSync(home, 0o700);
15
- const file = join(home, 'local-access.json');
23
+ const file = join(home, "local-access.json");
16
24
  if (!existsSync(file)) {
17
25
  try {
18
- writeFileSync(file, JSON.stringify({ token: randomBytes(32).toString('base64url') }), { mode: 0o600, flag: 'wx' });
26
+ writeFileSync(file, JSON.stringify({ token: randomBytes(32).toString("base64url") }), { mode: 0o600, flag: "wx" });
19
27
  }
20
28
  catch (e) {
21
- if (e.code !== 'EEXIST')
29
+ if (e.code !== "EEXIST")
22
30
  throw e;
23
31
  }
24
32
  }
25
33
  chmodSync(file, 0o600);
26
- const value = JSON.parse(readFileSync(file, 'utf8'));
27
- if (typeof value.token !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(value.token))
28
- throw new Error('Invalid local access file.');
34
+ const value = JSON.parse(readFileSync(file, "utf8"));
35
+ if (typeof value.token !== "string" ||
36
+ !/^[A-Za-z0-9_-]{43}$/.test(value.token))
37
+ throw new Error("Invalid local access file.");
29
38
  return value.token;
30
39
  }
31
40
  function array(value) {
32
41
  if (Array.isArray(value))
33
42
  return value.slice(0, 100);
34
- if (value && typeof value === 'object')
35
- for (const key of ['items', 'memories', 'observations', 'projects', 'events', 'data']) {
43
+ if (value && typeof value === "object")
44
+ for (const key of [
45
+ "items",
46
+ "memories",
47
+ "observations",
48
+ "projects",
49
+ "events",
50
+ "data",
51
+ ]) {
36
52
  const items = value[key];
37
53
  if (Array.isArray(items))
38
54
  return items.slice(0, 100);
39
55
  }
40
56
  return [];
41
57
  }
42
- async function readBody(req) {
43
- const chunks = [];
44
- let size = 0;
45
- for await (const chunk of req) {
46
- size += chunk.length;
47
- if (size > 8192)
48
- throw new Error('Body too large');
49
- chunks.push(Buffer.from(chunk));
50
- }
51
- return JSON.parse(Buffer.concat(chunks).toString() || '{}');
58
+ class BodyTooLarge extends Error {
59
+ }
60
+ function readBody(req, limit = 8192) {
61
+ return new Promise((resolve, reject) => {
62
+ let chunks = [], size = 0, settled = false;
63
+ const fail = (error) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ chunks = [];
68
+ reject(error);
69
+ };
70
+ req.on("data", (chunk) => {
71
+ if (settled)
72
+ return;
73
+ size += chunk.length;
74
+ if (size > limit) {
75
+ fail(new BodyTooLarge());
76
+ return;
77
+ }
78
+ chunks.push(Buffer.from(chunk));
79
+ });
80
+ req.once("end", () => {
81
+ if (settled)
82
+ return;
83
+ try {
84
+ const data = JSON.parse(Buffer.concat(chunks).toString() || "{}");
85
+ settled = true;
86
+ resolve(data);
87
+ }
88
+ catch {
89
+ fail(new Error("Invalid JSON"));
90
+ }
91
+ });
92
+ req.once("aborted", () => fail(new Error("Request aborted")));
93
+ req.once("error", () => fail(new Error("Request failed")));
94
+ });
52
95
  }
53
96
  export async function startLocalService(options = {}) {
54
97
  const home = options.home ?? vaultHome();
55
98
  const token = localAccessToken(home);
56
99
  const cloud = options.cloud ?? new VaultCloudClient(home, fetch, false);
100
+ let storage;
101
+ try {
102
+ storage = privateStore(home, options.cacheKey);
103
+ }
104
+ catch {
105
+ /* Remain online without a cache if the OS key store is locked. */
106
+ }
107
+ const accountCloud = new VaultCloudClient(home, fetch, false);
108
+ const monitor = options.device === false
109
+ ? undefined
110
+ : (options.device ??
111
+ localDevice(home, accountCloud, storage, pkg.version));
112
+ const jobs = new Map();
113
+ let engineCache;
114
+ let checkingEngines;
115
+ function clearJobs(session) {
116
+ for (const [id, job] of jobs)
117
+ if (!session || job.session === session) {
118
+ job.controller.abort();
119
+ jobs.delete(id);
120
+ }
121
+ }
122
+ async function engines() {
123
+ const requestOwner = identity();
124
+ if (engineCache && Date.now() - engineCache.at < 30000)
125
+ return { ...engineCache.value, selected: selectCurrent() };
126
+ if (checkingEngines)
127
+ return checkingEngines;
128
+ checkingEngines = (async () => {
129
+ if (options.context)
130
+ return options.context.list();
131
+ let managed = {
132
+ available: false,
133
+ reason: "Vault AI Resume is not configured or unavailable for this account.",
134
+ billing: "Uses the Vault observer allowance.",
135
+ };
136
+ try {
137
+ const status = (await accountCloud.observerStatus());
138
+ managed = {
139
+ ...managed,
140
+ available: status.configured === true && status.entitlement?.allowed === true,
141
+ reason: status.configured
142
+ ? "Provider readiness is checked when generating."
143
+ : "Managed observer is not configured.",
144
+ };
145
+ }
146
+ catch { }
147
+ return listContextEngines(home, managed);
148
+ })();
149
+ try {
150
+ const value = await checkingEngines;
151
+ if (requestOwner === identity())
152
+ engineCache = { at: Date.now(), value };
153
+ return value;
154
+ }
155
+ finally {
156
+ checkingEngines = undefined;
157
+ }
158
+ }
159
+ function selectCurrent() {
160
+ return selectedContextEngine(home);
161
+ }
57
162
  const sessions = new Map();
163
+ let loginAbort = new AbortController();
164
+ let login = null;
58
165
  let port = options.port ?? LOCAL_PORT;
59
166
  let connected = false, cloudError, updatedAt = null;
60
167
  let projects = [], memories = [];
61
- let owner = '';
168
+ let owner = "";
62
169
  let syncing = null;
63
170
  let closed = false;
64
- const cache = join(home, 'local-cache.json');
65
- function identity() { const config = loadConfig(home); return createHash('sha256').update(config.apiUrl + '|' + (process.env['VAULT_GO_TOKEN'] ?? config.email ?? loadTokens(home)?.accessToken ?? '') + '|' + (loadTokens(home) || process.env['VAULT_GO_TOKEN'] ? 'authenticated' : 'anonymous')).digest('hex'); }
66
- function resetOwner() { const next = identity(); if (next === owner)
67
- return; owner = next; projects = []; memories = []; updatedAt = null; connected = false; cloudError = undefined; try {
68
- const stored = JSON.parse(readFileSync(cache, 'utf8'));
69
- if (stored.owner === owner && (loadTokens(home) || process.env['VAULT_GO_TOKEN'])) {
171
+ const cache = "local-cache.json";
172
+ if (storage) {
173
+ try {
174
+ const old = JSON.parse(readFileSync(join(home, cache), "utf8"));
175
+ if (old.v !== 1)
176
+ storage.remove(cache);
177
+ }
178
+ catch { }
179
+ }
180
+ function identity() {
181
+ const config = loadConfig(home);
182
+ return createHash("sha256")
183
+ .update(config.apiUrl +
184
+ "|" +
185
+ (process.env["VAULT_GO_TOKEN"] ??
186
+ config.email ??
187
+ loadTokens(home)?.accessToken ??
188
+ "") +
189
+ "|" +
190
+ (loadTokens(home) || process.env["VAULT_GO_TOKEN"]
191
+ ? "authenticated"
192
+ : "anonymous"))
193
+ .digest("hex");
194
+ }
195
+ function resetOwner() {
196
+ const next = identity();
197
+ if (next === owner)
198
+ return;
199
+ clearJobs();
200
+ engineCache = undefined;
201
+ owner = next;
202
+ projects = [];
203
+ memories = [];
204
+ updatedAt = null;
205
+ connected = false;
206
+ cloudError = undefined;
207
+ const stored = storage?.read(cache);
208
+ if (stored?.owner === owner &&
209
+ (loadTokens(home) || process.env["VAULT_GO_TOKEN"])) {
70
210
  projects = array(stored.projects);
71
211
  memories = array(stored.memories);
72
- updatedAt = typeof stored.updatedAt === 'string' ? stored.updatedAt : null;
212
+ updatedAt =
213
+ typeof stored.updatedAt === "string" ? stored.updatedAt : null;
214
+ }
215
+ }
216
+ function disconnectLocal() {
217
+ loginAbort.abort();
218
+ if (login?.status === "pending")
219
+ login.status = "failed";
220
+ clearJobs();
221
+ sessions.clear();
222
+ try {
223
+ unlinkSync(authPath(home));
73
224
  }
225
+ catch { }
226
+ storage?.remove(cache);
227
+ projects = [];
228
+ memories = [];
229
+ connected = false;
230
+ updatedAt = null;
231
+ engineCache = undefined;
74
232
  }
75
- catch { } }
76
233
  async function refresh() {
77
234
  resetOwner();
78
235
  if (syncing)
79
236
  return syncing;
80
- if (!loadTokens(home) && !process.env['VAULT_GO_TOKEN']) {
237
+ if (login?.status === "pending")
238
+ return;
239
+ if (!loadTokens(home) && !process.env["VAULT_GO_TOKEN"]) {
81
240
  connected = false;
82
241
  return;
83
242
  }
84
243
  const requestOwner = owner;
85
244
  syncing = (async () => {
86
245
  try {
246
+ await monitor?.heartbeat();
247
+ if (monitor?.status().status === "revoked") {
248
+ disconnectLocal();
249
+ return;
250
+ }
87
251
  const newProjects = await cloud.projects();
88
252
  const newMemories = await cloud.feed({ limit: 50 });
89
253
  if (closed || requestOwner !== identity())
@@ -93,15 +257,12 @@ export async function startLocalService(options = {}) {
93
257
  updatedAt = new Date().toISOString();
94
258
  connected = true;
95
259
  cloudError = undefined;
96
- const temp = cache + '.tmp';
97
- writeFileSync(temp, JSON.stringify({ owner, projects, memories, updatedAt }), { mode: 0o600 });
98
- renameSync(temp, cache);
99
- chmodSync(cache, 0o600);
260
+ storage?.write(cache, { owner, projects, memories, updatedAt });
100
261
  }
101
262
  catch {
102
263
  if (requestOwner === identity()) {
103
264
  connected = false;
104
- cloudError = 'cloud_unavailable';
265
+ cloudError = "cloud_unavailable";
105
266
  }
106
267
  }
107
268
  finally {
@@ -111,107 +272,368 @@ export async function startLocalService(options = {}) {
111
272
  return syncing;
112
273
  }
113
274
  const server = createServer(async (req, res) => {
114
- res.setHeader('Cache-Control', 'no-store');
115
- res.setHeader('Referrer-Policy', 'no-referrer');
116
- res.setHeader('X-Content-Type-Options', 'nosniff');
117
- res.setHeader('X-Frame-Options', 'DENY');
118
- res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
119
- const json = (status, value) => { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(value)); };
275
+ res.setHeader("Cache-Control", "no-store");
276
+ res.setHeader("Referrer-Policy", "no-referrer");
277
+ res.setHeader("X-Content-Type-Options", "nosniff");
278
+ res.setHeader("X-Frame-Options", "DENY");
279
+ res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
280
+ const json = (status, value) => {
281
+ res.writeHead(status, {
282
+ "content-type": "application/json; charset=utf-8",
283
+ });
284
+ res.end(JSON.stringify(value));
285
+ };
120
286
  const host = req.headers.host?.toLowerCase();
121
287
  const allowed = [`localhost:${port}`, `127.0.0.1:${port}`];
122
288
  if (!host || !allowed.includes(host)) {
123
- json(403, { error: 'invalid_host' });
289
+ json(403, { error: "invalid_host" });
124
290
  return;
125
291
  }
126
292
  const origin = req.headers.origin;
127
- if (origin && !allowed.map(h => 'http://' + h).includes(origin)) {
128
- json(403, { error: 'invalid_origin' });
293
+ if (origin && !allowed.map((h) => "http://" + h).includes(origin)) {
294
+ json(403, { error: "invalid_origin" });
129
295
  return;
130
296
  }
131
- if (req.headers['sec-fetch-site'] === 'cross-site' && req.url?.startsWith('/api/')) {
132
- json(403, { error: 'cross_site_request' });
297
+ if (req.headers["sec-fetch-site"] === "cross-site" &&
298
+ req.url?.startsWith("/api/")) {
299
+ json(403, { error: "cross_site_request" });
133
300
  return;
134
301
  }
135
302
  let url;
136
303
  try {
137
- url = new URL(req.url ?? '/', `http://${host}`);
304
+ url = new URL(req.url ?? "/", `http://${host}`);
138
305
  }
139
306
  catch {
140
- json(400, { error: 'invalid_url' });
307
+ json(400, { error: "invalid_url" });
141
308
  return;
142
309
  }
143
- if (url.pathname === '/health' && req.method === 'GET') {
144
- const challenge = url.searchParams.get('challenge');
145
- json(200, { service: 'vault-go', version: pkg.version, port, ...(challenge && /^[A-Za-z0-9_-]{43}$/.test(challenge) ? { proof: createHmac('sha256', token).update(challenge).digest('base64url') } : {}) });
310
+ if (url.pathname === "/health" && req.method === "GET") {
311
+ const challenge = url.searchParams.get("challenge");
312
+ json(200, {
313
+ service: "vault-go",
314
+ version: pkg.version,
315
+ port,
316
+ ...(challenge && /^[A-Za-z0-9_-]{43}$/.test(challenge)
317
+ ? {
318
+ proof: createHmac("sha256", token)
319
+ .update(challenge)
320
+ .digest("base64url"),
321
+ }
322
+ : {}),
323
+ });
146
324
  return;
147
325
  }
148
- if (url.pathname === '/' && req.method === 'GET') {
149
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
326
+ if (url.pathname === "/" && req.method === "GET") {
327
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
150
328
  res.end(dashboardHtml());
151
329
  return;
152
330
  }
153
- if (url.pathname === '/app.js' && req.method === 'GET') {
154
- res.setHeader('Content-Type', 'text/javascript; charset=utf-8');
331
+ if (url.pathname === "/app.js" && req.method === "GET") {
332
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
155
333
  res.end(dashboardScript());
156
334
  return;
157
335
  }
336
+ if (url.pathname === "/logo.png" && req.method === "GET") {
337
+ try {
338
+ res.setHeader("Content-Type", "image/png");
339
+ res.end(readFileSync(new URL("../assets/vault-logo-glass.png", import.meta.url)));
340
+ }
341
+ catch {
342
+ res.writeHead(404).end();
343
+ }
344
+ return;
345
+ }
158
346
  try {
159
- if (url.pathname === '/api/session' && req.method === 'POST') {
160
- const data = await readBody(req);
161
- if (typeof data.token !== 'string' || !safeEqual(data.token, token)) {
162
- json(401, { error: 'unauthorized' });
347
+ const cookie = req.headers.cookie
348
+ ?.split(";")
349
+ .map((s) => s.trim())
350
+ .find((s) => s.startsWith("vault_local_session="))
351
+ ?.slice("vault_local_session=".length) ?? "";
352
+ const setSessionCookie = (session) => res.setHeader("Set-Cookie", `vault_local_session=${session}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`);
353
+ if (url.pathname === "/api/connect/status" && req.method === "GET") {
354
+ json(200, {
355
+ status: (sessions.get(cookie) ?? 0) > Date.now()
356
+ ? "connected"
357
+ : login?.session === cookie
358
+ ? login.status
359
+ : "idle",
360
+ });
361
+ return;
362
+ }
363
+ if (url.pathname === "/api/connect" && req.method === "POST") {
364
+ // A website can navigate here, but only this origin may initiate login.
365
+ if (!origin || !allowed.map((h) => "http://" + h).includes(origin)) {
366
+ json(403, { error: "invalid_origin" });
367
+ return;
368
+ }
369
+ if (login?.status === "pending") {
370
+ json(409, { error: "login_pending" });
163
371
  return;
164
372
  }
165
- const session = randomBytes(32).toString('base64url');
373
+ const data = (await readBody(req));
374
+ const locale = resolveLocale(typeof data.locale === "string" ? data.locale : undefined);
375
+ loginAbort = new AbortController();
376
+ const attemptAbort = loginAbort;
377
+ const attempt = {
378
+ session: randomBytes(32).toString("base64url"),
379
+ status: "pending",
380
+ };
381
+ login = attempt;
382
+ let announce;
383
+ let rejectStart;
384
+ const started = new Promise((resolve, reject) => {
385
+ announce = resolve;
386
+ rejectStart = reject;
387
+ });
388
+ void (options.authenticate ?? authenticateWithBrowser)(home, {
389
+ locale,
390
+ signal: attemptAbort.signal,
391
+ report: () => { },
392
+ callbackHtml: callbackPage(locale, `http://localhost:${port}/`),
393
+ openBrowser: async (authUrl) => announce(authUrl),
394
+ })
395
+ .then(async () => {
396
+ if (closed || attemptAbort.signal.aborted)
397
+ return;
398
+ await monitor?.reauthorize();
399
+ if (closed || attemptAbort.signal.aborted)
400
+ return;
401
+ attempt.status = "connected";
402
+ sessions.set(attempt.session, Date.now() + 12 * 60 * 60 * 1000);
403
+ await refresh();
404
+ })
405
+ .catch((error) => {
406
+ attempt.status = "failed";
407
+ rejectStart(error);
408
+ });
409
+ const authUrl = await started;
410
+ setSessionCookie(attempt.session);
411
+ json(200, { url: authUrl });
412
+ return;
413
+ }
414
+ if (url.pathname === "/api/session" && req.method === "POST") {
415
+ const data = (await readBody(req));
416
+ if (typeof data.token !== "string" || !safeEqual(data.token, token)) {
417
+ json(401, { error: "unauthorized" });
418
+ return;
419
+ }
420
+ const session = randomBytes(32).toString("base64url");
166
421
  for (const [key, expiry] of sessions)
167
422
  if (expiry < Date.now())
168
423
  sessions.delete(key);
169
424
  if (sessions.size >= 32)
170
425
  sessions.delete(sessions.keys().next().value);
171
426
  sessions.set(session, Date.now() + 12 * 60 * 60 * 1000);
172
- res.setHeader('Set-Cookie', `vault_local_session=${session}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`);
427
+ setSessionCookie(session);
173
428
  json(200, { ok: true });
174
429
  return;
175
430
  }
176
- const bearer = req.headers.authorization?.replace(/^Bearer /, '') ?? '';
177
- const cookie = req.headers.cookie?.split(';').map(s => s.trim()).find(s => s.startsWith('vault_local_session='))?.slice('vault_local_session='.length) ?? '';
178
- if (!safeEqual(bearer, token) && (sessions.get(cookie) ?? 0) <= Date.now()) {
179
- json(401, { error: 'unauthorized' });
431
+ const bearer = req.headers.authorization?.replace(/^Bearer /, "") ?? "";
432
+ if (!safeEqual(bearer, token) &&
433
+ (sessions.get(cookie) ?? 0) <= Date.now()) {
434
+ json(401, { error: "unauthorized" });
435
+ return;
436
+ }
437
+ if (url.pathname === "/api/logout" && req.method === "POST") {
438
+ clearJobs(cookie || bearer);
439
+ sessions.delete(cookie);
440
+ res.setHeader("Set-Cookie", "vault_local_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0");
441
+ json(200, { ok: true });
180
442
  return;
181
443
  }
182
444
  resetOwner();
183
- if (url.pathname === '/api/status' && req.method === 'GET') {
445
+ if (url.pathname === "/api/device/disconnect" && req.method === "POST") {
446
+ if (login?.status === "pending") {
447
+ json(409, { error: "login_in_progress" });
448
+ return;
449
+ }
450
+ await monitor?.disconnect();
451
+ disconnectLocal();
452
+ res.setHeader("Set-Cookie", "vault_local_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0");
453
+ json(200, { ok: true });
454
+ return;
455
+ }
456
+ if (url.pathname === "/api/context/engines" && req.method === "GET") {
457
+ json(200, await engines());
458
+ return;
459
+ }
460
+ if (url.pathname === "/api/context/engine" && req.method === "PUT") {
461
+ const data = (await readBody(req));
462
+ if (!isContextEngine(data.engine)) {
463
+ json(400, { error: "invalid_engine" });
464
+ return;
465
+ }
466
+ selectContextEngine(home, data.engine);
467
+ json(200, { selected: data.engine });
468
+ return;
469
+ }
470
+ if (url.pathname === "/api/context/secret" && req.method === "PUT") {
471
+ const data = (await readBody(req));
472
+ if (!isApiEngine(data.provider) || typeof data.key !== "string") {
473
+ json(400, { error: "invalid_engine_key" });
474
+ return;
475
+ }
476
+ saveEngineKey(home, data.provider, data.key);
477
+ engineCache = undefined;
478
+ json(200, { configured: data.provider });
479
+ return;
480
+ }
481
+ if (url.pathname === "/api/context/generate" && req.method === "POST") {
482
+ const data = (await readBody(req, 65536));
483
+ if (!loadTokens(home) ||
484
+ !isContextEngine(data.engine) ||
485
+ typeof data.text !== "string" ||
486
+ !data.text.trim() ||
487
+ data.text.length > 16000 ||
488
+ data.consent !== true) {
489
+ json(400, { error: "invalid_generation_request" });
490
+ return;
491
+ }
492
+ for (const [id, job] of jobs)
493
+ if (Date.now() - job.createdAt > 600000) {
494
+ job.controller.abort();
495
+ jobs.delete(id);
496
+ }
497
+ if ([...jobs.values()].some((j) => j.status === "running")) {
498
+ json(409, { error: "generation_in_progress" });
499
+ return;
500
+ }
501
+ if (jobs.size >= 8)
502
+ jobs.delete(jobs.keys().next().value);
503
+ const id = randomBytes(16).toString("hex");
504
+ const job = {
505
+ id,
506
+ owner: identity(),
507
+ session: cookie || bearer,
508
+ status: "running",
509
+ controller: new AbortController(),
510
+ createdAt: Date.now(),
511
+ };
512
+ jobs.set(id, job);
513
+ void (options.context?.generate ?? generateContext)(home, data.engine, data.text, {
514
+ signal: job.controller.signal,
515
+ managedGenerate: (text, opts) => accountCloud.observerGenerate(text, opts.signal),
516
+ })
517
+ .then((result) => {
518
+ if (job.controller.signal.aborted || job.owner !== identity())
519
+ return;
520
+ job.result = result;
521
+ job.status = "completed";
522
+ })
523
+ .catch(() => {
524
+ if (job.controller.signal.aborted)
525
+ return;
526
+ job.status = "failed";
527
+ job.error = "generation_failed";
528
+ });
529
+ json(202, { jobId: id });
530
+ return;
531
+ }
532
+ const jobMatch = url.pathname.match(/^\/api\/context\/jobs\/([a-f0-9]{32})$/);
533
+ if (jobMatch) {
534
+ const job = jobs.get(jobMatch[1]);
535
+ if (!job ||
536
+ job.owner !== identity() ||
537
+ job.session !== (cookie || bearer)) {
538
+ json(404, { error: "job_not_found" });
539
+ return;
540
+ }
541
+ if (req.method === "DELETE") {
542
+ job.controller.abort();
543
+ jobs.delete(job.id);
544
+ json(200, { ok: true });
545
+ return;
546
+ }
547
+ if (req.method === "GET") {
548
+ json(200, {
549
+ status: job.status,
550
+ result: job.result,
551
+ error: job.error,
552
+ });
553
+ return;
554
+ }
555
+ }
556
+ if (url.pathname === "/api/status" && req.method === "GET") {
184
557
  const config = loadConfig(home);
185
- json(200, { service: 'vault-go', version: pkg.version, port, authenticated: Boolean(loadTokens(home) || process.env['VAULT_GO_TOKEN']), email: config.email, cloud: { connected, error: cloudError, syncing: Boolean(syncing) }, activity: readActivity(home), projects, memories, updatedAt });
558
+ json(200, {
559
+ service: "vault-go",
560
+ version: pkg.version,
561
+ port,
562
+ authenticated: Boolean(loadTokens(home) || process.env["VAULT_GO_TOKEN"]),
563
+ email: config.email,
564
+ cloud: { connected, error: cloudError, syncing: Boolean(syncing) },
565
+ device: monitor?.status() ?? { status: "unregistered" },
566
+ security: {
567
+ cache: {
568
+ encrypted: Boolean(storage),
569
+ keyStorage: storage?.keyStorage ?? "unavailable",
570
+ },
571
+ credentials: "file-permissions",
572
+ transport: {
573
+ local: "loopback-http",
574
+ cloud: new URL(config.apiUrl).protocol === "https:" ? "https" : "http",
575
+ },
576
+ endToEndEncrypted: false,
577
+ },
578
+ activity: readActivity(home),
579
+ projects,
580
+ memories,
581
+ updatedAt,
582
+ });
186
583
  return;
187
584
  }
188
- if (url.pathname === '/api/sync' && req.method === 'POST') {
585
+ if (url.pathname === "/api/sync" && req.method === "POST") {
189
586
  await readBody(req);
190
587
  await refresh();
191
588
  json(200, { ok: connected });
192
589
  return;
193
590
  }
194
- if (url.pathname === '/api/shutdown' && req.method === 'POST') {
591
+ if (url.pathname === "/api/shutdown" && req.method === "POST") {
195
592
  json(200, { ok: true });
196
593
  setTimeout(() => void close(), 30);
197
594
  return;
198
595
  }
199
- json(404, { error: 'not_found' });
596
+ json(404, { error: "not_found" });
200
597
  }
201
- catch {
202
- if (!res.headersSent)
203
- json(400, { error: 'invalid_request' });
598
+ catch (error) {
599
+ if (!res.headersSent) {
600
+ if (error instanceof BodyTooLarge)
601
+ res.setHeader("Connection", "close");
602
+ json(error instanceof BodyTooLarge ? 413 : 400, {
603
+ error: error instanceof BodyTooLarge
604
+ ? "body_too_large"
605
+ : "invalid_request",
606
+ });
607
+ }
204
608
  else
205
609
  res.end();
206
610
  }
207
611
  });
208
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
612
+ server.headersTimeout = 5000;
613
+ server.requestTimeout = 15000;
614
+ server.keepAliveTimeout = 5000;
615
+ server.maxHeadersCount = 40;
616
+ server.maxConnections = 64;
617
+ await new Promise((resolve, reject) => {
618
+ server.once("error", reject);
619
+ server.listen(port, "127.0.0.1", resolve);
620
+ });
209
621
  const address = server.address();
210
- if (address && typeof address !== 'string')
622
+ if (address && typeof address !== "string")
211
623
  port = address.port;
212
624
  const timer = setInterval(() => void refresh(), options.pollMs ?? 15_000);
213
625
  timer.unref();
214
626
  void refresh();
215
- async function close() { closed = true; clearInterval(timer); sessions.clear(); server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())); }
627
+ async function close() {
628
+ closed = true;
629
+ loginAbort.abort();
630
+ monitor?.close();
631
+ clearJobs();
632
+ clearInterval(timer);
633
+ sessions.clear();
634
+ server.closeAllConnections();
635
+ await new Promise((resolve) => server.close(() => resolve()));
636
+ storage?.close();
637
+ }
216
638
  return { server, port, close };
217
639
  }