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.
- package/README.md +39 -0
- package/assets/vault-logo-glass.png +0 -0
- package/dist/activity.js +28 -13
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +7 -2
- package/dist/callback-page.d.ts +2 -2
- package/dist/callback-page.js +29 -4
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +56 -0
- package/dist/cloud.d.ts +11 -0
- package/dist/cloud.js +36 -2
- package/dist/context-engines.d.ts +60 -0
- package/dist/context-engines.js +546 -0
- package/dist/index.js +5 -1
- package/dist/local-dashboard.js +321 -22
- package/dist/local-device.d.ts +22 -0
- package/dist/local-device.js +185 -0
- package/dist/local-install.d.ts +8 -1
- package/dist/local-install.js +172 -59
- package/dist/local-service.d.ts +13 -3
- package/dist/local-service.js +506 -84
- package/dist/locale.d.ts +1 -1
- package/dist/locale.js +102 -5
- package/dist/private-store.d.ts +8 -0
- package/dist/private-store.js +130 -0
- package/dist/server.js +50 -0
- package/package.json +8 -1
package/dist/local-service.js
CHANGED
|
@@ -1,89 +1,253 @@
|
|
|
1
|
-
import { createServer } from
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync,
|
|
3
|
-
import { join } from
|
|
4
|
-
import { createHash, createHmac, randomBytes, timingSafeEqual } from
|
|
5
|
-
import { loadConfig, loadTokens, vaultHome } from
|
|
6
|
-
import { VaultCloudClient } from
|
|
7
|
-
import { readActivity } from
|
|
8
|
-
import { dashboardHtml, dashboardScript } from
|
|
9
|
-
import pkg from
|
|
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 &&
|
|
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,
|
|
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(
|
|
26
|
+
writeFileSync(file, JSON.stringify({ token: randomBytes(32).toString("base64url") }), { mode: 0o600, flag: "wx" });
|
|
19
27
|
}
|
|
20
28
|
catch (e) {
|
|
21
|
-
if (e.code !==
|
|
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,
|
|
27
|
-
if (typeof value.token !==
|
|
28
|
-
|
|
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 ===
|
|
35
|
-
for (const key of [
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
size
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
115
|
-
res.setHeader(
|
|
116
|
-
res.setHeader(
|
|
117
|
-
res.setHeader(
|
|
118
|
-
res.setHeader(
|
|
119
|
-
const json = (status, 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:
|
|
289
|
+
json(403, { error: "invalid_host" });
|
|
124
290
|
return;
|
|
125
291
|
}
|
|
126
292
|
const origin = req.headers.origin;
|
|
127
|
-
if (origin && !allowed.map(h =>
|
|
128
|
-
json(403, { error:
|
|
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[
|
|
132
|
-
|
|
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 ??
|
|
304
|
+
url = new URL(req.url ?? "/", `http://${host}`);
|
|
138
305
|
}
|
|
139
306
|
catch {
|
|
140
|
-
json(400, { error:
|
|
307
|
+
json(400, { error: "invalid_url" });
|
|
141
308
|
return;
|
|
142
309
|
}
|
|
143
|
-
if (url.pathname ===
|
|
144
|
-
const challenge = url.searchParams.get(
|
|
145
|
-
json(200, {
|
|
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 ===
|
|
149
|
-
res.setHeader(
|
|
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 ===
|
|
154
|
-
res.setHeader(
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
427
|
+
setSessionCookie(session);
|
|
173
428
|
json(200, { ok: true });
|
|
174
429
|
return;
|
|
175
430
|
}
|
|
176
|
-
const bearer = req.headers.authorization?.replace(/^Bearer /,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
json(401, { error:
|
|
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 ===
|
|
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, {
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
596
|
+
json(404, { error: "not_found" });
|
|
200
597
|
}
|
|
201
|
-
catch {
|
|
202
|
-
if (!res.headersSent)
|
|
203
|
-
|
|
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
|
-
|
|
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 !==
|
|
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() {
|
|
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
|
}
|