@schlessera/brain-ui-server 0.13.0 → 0.14.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 +51 -12
- package/dist/agent/backend.d.ts +163 -37
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +281 -286
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts +46 -5
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +94 -35
- package/dist/app.js.map +1 -1
- package/dist/brain/client.d.ts +41 -30
- package/dist/brain/client.d.ts.map +1 -1
- package/dist/brain/client.js +138 -132
- package/dist/brain/client.js.map +1 -1
- package/dist/config/env.d.ts +104 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +296 -0
- package/dist/config/env.js.map +1 -0
- package/dist/cron/scheduler.d.ts +16 -8
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +54 -53
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/brain-db.d.ts +50 -0
- package/dist/db/brain-db.d.ts.map +1 -0
- package/dist/db/brain-db.js +75 -0
- package/dist/db/brain-db.js.map +1 -0
- package/dist/db/client.d.ts +8 -6
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +12 -25
- package/dist/db/client.js.map +1 -1
- package/dist/db/settings.d.ts +5 -4
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +9 -12
- package/dist/db/settings.js.map +1 -1
- package/dist/files/walker.d.ts +6 -7
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +6 -9
- package/dist/files/walker.js.map +1 -1
- package/dist/graph/reader.d.ts +6 -6
- package/dist/graph/reader.d.ts.map +1 -1
- package/dist/graph/reader.js +22 -12
- package/dist/graph/reader.js.map +1 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +17 -6
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +34 -43
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/origin.d.ts +1 -2
- package/dist/middleware/origin.d.ts.map +1 -1
- package/dist/middleware/origin.js +1 -7
- package/dist/middleware/origin.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +29 -5
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +47 -76
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/middleware/tailscale.d.ts +14 -11
- package/dist/middleware/tailscale.d.ts.map +1 -1
- package/dist/middleware/tailscale.js +11 -10
- package/dist/middleware/tailscale.js.map +1 -1
- package/dist/routes/brain.d.ts +9 -154
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +187 -188
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts +4 -88
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +74 -71
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/graph.d.ts +4 -255
- package/dist/routes/graph.d.ts.map +1 -1
- package/dist/routes/graph.js +67 -64
- package/dist/routes/graph.js.map +1 -1
- package/dist/routes/health.d.ts +8 -22
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +10 -10
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/models.d.ts +7 -90
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +53 -51
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/providers.d.ts +5 -34
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +13 -11
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/sessions.d.ts +7 -78
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +35 -34
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/share.d.ts +7 -60
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +80 -77
- package/dist/routes/share.js.map +1 -1
- package/dist/routes/voice.d.ts +8 -91
- package/dist/routes/voice.d.ts.map +1 -1
- package/dist/routes/voice.js +62 -59
- package/dist/routes/voice.js.map +1 -1
- package/dist/share/staging.d.ts +4 -4
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +7 -8
- package/dist/share/staging.js.map +1 -1
- package/dist/voice/deepgram-token.d.ts +1 -1
- package/dist/voice/deepgram-token.d.ts.map +1 -1
- package/dist/voice/deepgram-token.js +1 -2
- package/dist/voice/deepgram-token.js.map +1 -1
- package/dist/voice/keyterm-builder.d.ts +23 -8
- package/dist/voice/keyterm-builder.d.ts.map +1 -1
- package/dist/voice/keyterm-builder.js +67 -43
- package/dist/voice/keyterm-builder.js.map +1 -1
- package/dist/voice/speech-providers.d.ts +6 -4
- package/dist/voice/speech-providers.d.ts.map +1 -1
- package/dist/voice/speech-providers.js +28 -25
- package/dist/voice/speech-providers.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +2 -3
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +18 -10
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +35 -37
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +5 -7
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +1 -2
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +16 -12
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +14 -23
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/routing.d.ts +2 -1
- package/dist/ws/routing.d.ts.map +1 -1
- package/dist/ws/routing.js +5 -6
- package/dist/ws/routing.js.map +1 -1
- package/dist/ws/run-session.js +1 -1
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/session-catalog.d.ts +2 -2
- package/dist/ws/session-catalog.d.ts.map +1 -1
- package/dist/ws/session-catalog.js +2 -3
- package/dist/ws/session-catalog.js.map +1 -1
- package/package.json +8 -5
- package/src/agent/backend.ts +509 -340
- package/src/app.ts +157 -36
- package/src/brain/client.ts +166 -143
- package/src/config/env.ts +401 -0
- package/src/cron/scheduler.ts +77 -59
- package/src/db/brain-db.ts +104 -0
- package/src/db/client.ts +12 -31
- package/src/db/settings.ts +12 -14
- package/src/files/walker.ts +6 -10
- package/src/graph/reader.ts +26 -15
- package/src/index.ts +56 -17
- package/src/middleware/auth.ts +51 -45
- package/src/middleware/origin.ts +1 -8
- package/src/middleware/passkeys.ts +72 -68
- package/src/middleware/tailscale.ts +22 -17
- package/src/routes/brain.ts +31 -30
- package/src/routes/files.ts +10 -7
- package/src/routes/graph.ts +9 -6
- package/src/routes/health.ts +17 -10
- package/src/routes/models.ts +19 -15
- package/src/routes/providers.ts +14 -15
- package/src/routes/sessions.ts +18 -11
- package/src/routes/share.ts +35 -24
- package/src/routes/voice.ts +78 -68
- package/src/share/staging.ts +10 -8
- package/src/voice/deepgram-token.ts +4 -2
- package/src/voice/keyterm-builder.ts +90 -56
- package/src/voice/speech-providers.ts +28 -24
- package/src/ws/bridge.ts +2 -3
- package/src/ws/clients.ts +38 -41
- package/src/ws/connection.ts +5 -7
- package/src/ws/dispatch.ts +1 -2
- package/src/ws/host.ts +24 -27
- package/src/ws/routing.ts +6 -10
- package/src/ws/run-session.ts +1 -1
- package/src/ws/session-catalog.ts +2 -3
- package/dist/ws/handler.d.ts +0 -34
- package/dist/ws/handler.d.ts.map +0 -1
- package/dist/ws/handler.js +0 -33
- package/dist/ws/handler.js.map +0 -1
- package/src/ws/handler.ts +0 -50
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { Context } from "hono";
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
3
4
|
import {
|
|
4
5
|
generateAuthenticationOptions,
|
|
5
6
|
generateRegistrationOptions,
|
|
@@ -13,13 +14,14 @@ import type {
|
|
|
13
14
|
} from "@simplewebauthn/server";
|
|
14
15
|
import {
|
|
15
16
|
type AuthMode,
|
|
17
|
+
type AuthRuntime,
|
|
16
18
|
issueSessionCookie,
|
|
17
19
|
consumeLoginToken,
|
|
18
20
|
LOGIN_RATE_LIMIT,
|
|
19
21
|
GLOBAL_LOGIN_RATE_LIMIT,
|
|
20
22
|
} from "./auth.js";
|
|
21
23
|
import { clientIp } from "./tailscale.js";
|
|
22
|
-
import {
|
|
24
|
+
import type { WebAuthnConfig } from "../config/env.js";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* WebAuthn passkeys as an extension of `password` mode: the password bootstraps
|
|
@@ -27,27 +29,33 @@ import { getDb } from "../db/client.js";
|
|
|
27
29
|
* login. A successful assertion mints the same session cookie as password login
|
|
28
30
|
* (issueSessionCookie), so authGuard / isWsAuthorized / TTL are untouched.
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* Everything environmental is injected as a {@link PasskeyContext} — the
|
|
33
|
+
* resolved WebAuthn identity, the auth runtime (for trust/proxy semantics and
|
|
34
|
+
* the cookie secret), the origin allowlist, and the app's own database.
|
|
32
35
|
*
|
|
33
36
|
* Credentials are RP-scoped: a credential registered on localhost does not
|
|
34
37
|
* exist for the production domain and vice versa. Rows carry rp_id so all
|
|
35
38
|
* environments share the table.
|
|
36
39
|
*/
|
|
37
40
|
|
|
41
|
+
/** Injected per-app dependencies for every passkey route and helper. */
|
|
42
|
+
export interface PasskeyContext {
|
|
43
|
+
db: Database;
|
|
44
|
+
webauthn: WebAuthnConfig;
|
|
45
|
+
auth: AuthRuntime;
|
|
46
|
+
/** ALLOWED_ORIGINS — split-topology origins also valid for ceremonies. */
|
|
47
|
+
allowedOrigins: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
const CHALLENGE_TTL_MS = 120_000; // > the 60s ceremony timeout
|
|
39
51
|
// Pending challenges are unauthenticated state; cap and evict oldest-first so
|
|
40
52
|
// login-options spam can't grow memory. Losing one costs a retried ceremony.
|
|
41
53
|
const MAX_PENDING_CHALLENGES = 100;
|
|
42
54
|
const OPTIONS_RATE_LIMIT = 10; // login-options per IP per minute (own bucket)
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
// The single "user" every passkey belongs to. Stable across registrations so
|
|
49
|
-
// an authenticator overwrites its existing entry instead of stacking
|
|
50
|
-
// duplicates for the same site.
|
|
55
|
+
|
|
56
|
+
// The single "user" every passkey belongs to (webauthn.userId). Stable across
|
|
57
|
+
// registrations so an authenticator overwrites its existing entry instead of
|
|
58
|
+
// stacking duplicates for the same site.
|
|
51
59
|
//
|
|
52
60
|
// WARNING: the user handle is part of the WebAuthn wire contract — it is
|
|
53
61
|
// burned into every resident credential at registration. Changing
|
|
@@ -55,25 +63,20 @@ function rpName(): string {
|
|
|
55
63
|
// (authenticators will present a handle the server no longer recognizes as
|
|
56
64
|
// the same user and re-registration stacks a second entry). Set it before the
|
|
57
65
|
// first passkey is registered and never change it.
|
|
58
|
-
function
|
|
59
|
-
return
|
|
66
|
+
function userId(webauthn: WebAuthnConfig): Uint8Array<ArrayBuffer> {
|
|
67
|
+
return new TextEncoder().encode(webauthn.userId) as Uint8Array<ArrayBuffer>;
|
|
60
68
|
}
|
|
69
|
+
|
|
61
70
|
/** WebAuthn caps `user.id` at 64 bytes; browsers reject anything longer. */
|
|
62
71
|
const MAX_USER_ID_BYTES = 64;
|
|
63
72
|
|
|
64
|
-
function userId(): Uint8Array<ArrayBuffer> {
|
|
65
|
-
return new TextEncoder().encode(
|
|
66
|
-
process.env.WEBAUTHN_USER_ID || "brain-ui-owner"
|
|
67
|
-
) as Uint8Array<ArrayBuffer>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
73
|
/**
|
|
71
74
|
* Refuse to boot on a WebAuthn user handle the browser would reject. Called
|
|
72
75
|
* from createApp(): an oversized handle otherwise produces registration
|
|
73
76
|
* options that look fine server-side and fail silently in every browser.
|
|
74
77
|
*/
|
|
75
|
-
export function assertPasskeyConfig(): void {
|
|
76
|
-
const bytes = userId().byteLength;
|
|
78
|
+
export function assertPasskeyConfig(webauthn: WebAuthnConfig): void {
|
|
79
|
+
const bytes = userId(webauthn).byteLength;
|
|
77
80
|
if (bytes > MAX_USER_ID_BYTES) {
|
|
78
81
|
throw new Error(
|
|
79
82
|
`WEBAUTHN_USER_ID is ${bytes} bytes; WebAuthn allows at most ${MAX_USER_ID_BYTES}. ` +
|
|
@@ -136,13 +139,6 @@ function challengeFromResponse(response: {
|
|
|
136
139
|
|
|
137
140
|
// --- RP / origin resolution ---
|
|
138
141
|
|
|
139
|
-
function envList(name: string): string[] {
|
|
140
|
-
return (process.env[name] ?? "")
|
|
141
|
-
.split(",")
|
|
142
|
-
.map((s) => s.trim())
|
|
143
|
-
.filter(Boolean);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
142
|
function isLoopbackHostname(hostname: string): boolean {
|
|
147
143
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
148
144
|
}
|
|
@@ -153,7 +149,7 @@ function isLoopbackHostname(hostname: string): boolean {
|
|
|
153
149
|
* unconfigured. WEBAUTHN_ORIGINS / WEBAUTHN_RP_ID remain as overrides for
|
|
154
150
|
* proxies that rewrite Host.
|
|
155
151
|
*/
|
|
156
|
-
function resolveRp(c: Context): { rpID: string; origin: string } | null {
|
|
152
|
+
function resolveRp(c: Context, ctx: PasskeyContext): { rpID: string; origin: string } | null {
|
|
157
153
|
const origin = c.req.header("origin");
|
|
158
154
|
if (!origin) return null;
|
|
159
155
|
|
|
@@ -165,22 +161,21 @@ function resolveRp(c: Context): { rpID: string; origin: string } | null {
|
|
|
165
161
|
}
|
|
166
162
|
|
|
167
163
|
const allowed =
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
ctx.webauthn.origins.includes(origin) ||
|
|
165
|
+
ctx.allowedOrigins.includes(origin) ||
|
|
170
166
|
url.host === c.req.header("host") ||
|
|
171
167
|
// Dev escape hatch: the vite proxy sets changeOrigin, so the server sees
|
|
172
168
|
// Host localhost:3000 but Origin http://localhost:5173. Opt-in via an
|
|
173
169
|
// explicit flag — NOT NODE_ENV, whose absence must never widen the RP.
|
|
174
|
-
(
|
|
175
|
-
isLoopbackHostname(url.hostname));
|
|
170
|
+
(ctx.webauthn.allowLoopbackOrigin && isLoopbackHostname(url.hostname));
|
|
176
171
|
if (!allowed) return null;
|
|
177
172
|
|
|
178
|
-
return { rpID:
|
|
173
|
+
return { rpID: ctx.webauthn.rpId ?? url.hostname, origin };
|
|
179
174
|
}
|
|
180
175
|
|
|
181
176
|
/** rpID for endpoints where same-origin GETs legitimately omit Origin. */
|
|
182
|
-
function rpIdForRead(c: Context): string {
|
|
183
|
-
if (
|
|
177
|
+
function rpIdForRead(c: Context, ctx: PasskeyContext): string {
|
|
178
|
+
if (ctx.webauthn.rpId) return ctx.webauthn.rpId;
|
|
184
179
|
const origin = c.req.header("origin");
|
|
185
180
|
if (origin) {
|
|
186
181
|
try {
|
|
@@ -224,14 +219,14 @@ function toSummary(row: CredentialRow) {
|
|
|
224
219
|
};
|
|
225
220
|
}
|
|
226
221
|
|
|
227
|
-
function credentialsForRp(rpID: string): CredentialRow[] {
|
|
228
|
-
return
|
|
222
|
+
function credentialsForRp(db: Database, rpID: string): CredentialRow[] {
|
|
223
|
+
return db
|
|
229
224
|
.query("SELECT * FROM passkey_credentials WHERE rp_id = ?")
|
|
230
225
|
.all(rpID) as CredentialRow[];
|
|
231
226
|
}
|
|
232
227
|
|
|
233
|
-
function credentialById(id: string): CredentialRow | null {
|
|
234
|
-
return
|
|
228
|
+
function credentialById(db: Database, id: string): CredentialRow | null {
|
|
229
|
+
return db
|
|
235
230
|
.query("SELECT * FROM passkey_credentials WHERE id = ?")
|
|
236
231
|
.get(id) as CredentialRow | null;
|
|
237
232
|
}
|
|
@@ -263,11 +258,11 @@ function notEnabled(c: Context) {
|
|
|
263
258
|
* until it has its own credential. Host-derived rpID is fine here: a forged
|
|
264
259
|
* Host can at most re-enable password auth, which still requires the password.
|
|
265
260
|
*/
|
|
266
|
-
export function passwordLoginDisabled(c: Context): boolean {
|
|
267
|
-
if (
|
|
268
|
-
const row =
|
|
261
|
+
export function passwordLoginDisabled(c: Context, ctx: PasskeyContext): boolean {
|
|
262
|
+
if (ctx.auth.allowPassword) return false;
|
|
263
|
+
const row = ctx.db
|
|
269
264
|
.query("SELECT COUNT(*) AS n FROM passkey_credentials WHERE rp_id = ?")
|
|
270
|
-
.get(rpIdForRead(c)) as { n: number };
|
|
265
|
+
.get(rpIdForRead(c, ctx)) as { n: number };
|
|
271
266
|
return row.n > 0;
|
|
272
267
|
}
|
|
273
268
|
|
|
@@ -275,7 +270,11 @@ export function passwordLoginDisabled(c: Context): boolean {
|
|
|
275
270
|
* Unauthenticated passkey routes. Mount BEFORE the auth guard, next to
|
|
276
271
|
* authRoutes.
|
|
277
272
|
*/
|
|
278
|
-
export function passkeyPublicRoutes(
|
|
273
|
+
export function passkeyPublicRoutes(
|
|
274
|
+
mode: AuthMode,
|
|
275
|
+
ctx: PasskeyContext,
|
|
276
|
+
deps: PasskeyDeps = {}
|
|
277
|
+
): Hono {
|
|
279
278
|
const verifyAuthentication = deps.verifyAuthenticationResponse ?? realVerifyAuthentication;
|
|
280
279
|
const now = deps.now ?? Date.now;
|
|
281
280
|
const app = new Hono();
|
|
@@ -286,21 +285,21 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
286
285
|
if (mode !== "password") {
|
|
287
286
|
return c.json({ password: false, passkey: false });
|
|
288
287
|
}
|
|
289
|
-
const row =
|
|
288
|
+
const row = ctx.db
|
|
290
289
|
.query("SELECT COUNT(*) AS n FROM passkey_credentials WHERE rp_id = ?")
|
|
291
|
-
.get(rpIdForRead(c)) as { n: number };
|
|
292
|
-
return c.json({ password: !passwordLoginDisabled(c), passkey: row.n > 0 });
|
|
290
|
+
.get(rpIdForRead(c, ctx)) as { n: number };
|
|
291
|
+
return c.json({ password: !passwordLoginDisabled(c, ctx), passkey: row.n > 0 });
|
|
293
292
|
});
|
|
294
293
|
|
|
295
294
|
app.post("/auth/passkey/login-options", async (c) => {
|
|
296
295
|
if (mode !== "password") return notEnabled(c);
|
|
297
|
-
const ip = clientIp(c,
|
|
296
|
+
const ip = clientIp(c, ctx.auth.trustProxy, ctx.auth.trustProxyHops) || "unknown";
|
|
298
297
|
// Own bucket: conditional-UI mounts fire this on every login-screen load
|
|
299
298
|
// and must not starve real verify attempts of their shared budget.
|
|
300
299
|
if (!consumeLoginToken(`pk-opt:${ip}`, OPTIONS_RATE_LIMIT)) {
|
|
301
300
|
return c.json({ error: "Too many attempts. Try again in a minute." }, 429);
|
|
302
301
|
}
|
|
303
|
-
const rp = resolveRp(c);
|
|
302
|
+
const rp = resolveRp(c, ctx);
|
|
304
303
|
if (!rp) return c.json({ error: "Origin not allowed" }, 400);
|
|
305
304
|
|
|
306
305
|
// Empty allowCredentials + discoverable credentials = usernameless login.
|
|
@@ -315,14 +314,14 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
315
314
|
|
|
316
315
|
app.post("/auth/passkey/login-verify", async (c) => {
|
|
317
316
|
if (mode !== "password") return notEnabled(c);
|
|
318
|
-
const ip = clientIp(c,
|
|
317
|
+
const ip = clientIp(c, ctx.auth.trustProxy, ctx.auth.trustProxyHops) || "unknown";
|
|
319
318
|
// Same buckets as password login: one combined online-guess budget.
|
|
320
319
|
const perIpOk = consumeLoginToken(`ip:${ip}`, LOGIN_RATE_LIMIT);
|
|
321
320
|
const globalOk = consumeLoginToken("global", GLOBAL_LOGIN_RATE_LIMIT);
|
|
322
321
|
if (!perIpOk || !globalOk) {
|
|
323
322
|
return c.json({ error: "Too many attempts. Try again in a minute." }, 429);
|
|
324
323
|
}
|
|
325
|
-
const rp = resolveRp(c);
|
|
324
|
+
const rp = resolveRp(c, ctx);
|
|
326
325
|
if (!rp) return c.json({ error: "Origin not allowed" }, 400);
|
|
327
326
|
|
|
328
327
|
let response: AuthenticationResponseJSON;
|
|
@@ -334,7 +333,8 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
334
333
|
|
|
335
334
|
const fail = () => c.json({ error: "Passkey verification failed" }, 401);
|
|
336
335
|
|
|
337
|
-
const row =
|
|
336
|
+
const row =
|
|
337
|
+
typeof response?.id === "string" ? credentialById(ctx.db, response.id) : null;
|
|
338
338
|
if (!row || row.rp_id !== rp.rpID) return fail();
|
|
339
339
|
|
|
340
340
|
const challenge = challengeFromResponse(response);
|
|
@@ -351,7 +351,7 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
351
351
|
credential: toWebAuthnCredential(row),
|
|
352
352
|
});
|
|
353
353
|
if (!result.verified) return fail();
|
|
354
|
-
|
|
354
|
+
ctx.db
|
|
355
355
|
.prepare(
|
|
356
356
|
"UPDATE passkey_credentials SET counter = ?, last_used_at = ? WHERE id = ?"
|
|
357
357
|
)
|
|
@@ -371,7 +371,7 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
371
371
|
return fail();
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
await issueSessionCookie(c);
|
|
374
|
+
await issueSessionCookie(c, ctx.auth);
|
|
375
375
|
return c.json({ ok: true });
|
|
376
376
|
});
|
|
377
377
|
|
|
@@ -382,24 +382,28 @@ export function passkeyPublicRoutes(mode: AuthMode, deps: PasskeyDeps = {}): Hon
|
|
|
382
382
|
* Session-gated passkey routes. Mount AFTER the auth guard — gating comes from
|
|
383
383
|
* mount position, not per-route checks.
|
|
384
384
|
*/
|
|
385
|
-
export function passkeyManagementRoutes(
|
|
385
|
+
export function passkeyManagementRoutes(
|
|
386
|
+
mode: AuthMode,
|
|
387
|
+
ctx: PasskeyContext,
|
|
388
|
+
deps: PasskeyDeps = {}
|
|
389
|
+
): Hono {
|
|
386
390
|
const verifyRegistration = deps.verifyRegistrationResponse ?? realVerifyRegistration;
|
|
387
391
|
const now = deps.now ?? Date.now;
|
|
388
392
|
const app = new Hono();
|
|
389
393
|
|
|
390
394
|
app.post("/auth/passkey/register-options", async (c) => {
|
|
391
395
|
if (mode !== "password") return notEnabled(c);
|
|
392
|
-
const rp = resolveRp(c);
|
|
396
|
+
const rp = resolveRp(c, ctx);
|
|
393
397
|
if (!rp) return c.json({ error: "Origin not allowed" }, 400);
|
|
394
398
|
|
|
395
399
|
const options = await generateRegistrationOptions({
|
|
396
|
-
rpName: rpName
|
|
400
|
+
rpName: ctx.webauthn.rpName,
|
|
397
401
|
rpID: rp.rpID,
|
|
398
|
-
userName: userName
|
|
399
|
-
userID: userId(),
|
|
400
|
-
userDisplayName: rpName
|
|
402
|
+
userName: ctx.webauthn.userName,
|
|
403
|
+
userID: userId(ctx.webauthn),
|
|
404
|
+
userDisplayName: ctx.webauthn.rpName,
|
|
401
405
|
attestationType: "none",
|
|
402
|
-
excludeCredentials: credentialsForRp(rp.rpID).map((row) => ({
|
|
406
|
+
excludeCredentials: credentialsForRp(ctx.db, rp.rpID).map((row) => ({
|
|
403
407
|
id: row.id,
|
|
404
408
|
transports: toWebAuthnCredential(row).transports,
|
|
405
409
|
})),
|
|
@@ -415,7 +419,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
415
419
|
|
|
416
420
|
app.post("/auth/passkey/register-verify", async (c) => {
|
|
417
421
|
if (mode !== "password") return notEnabled(c);
|
|
418
|
-
const rp = resolveRp(c);
|
|
422
|
+
const rp = resolveRp(c, ctx);
|
|
419
423
|
if (!rp) return c.json({ error: "Origin not allowed" }, 400);
|
|
420
424
|
|
|
421
425
|
let body: { response?: RegistrationResponseJSON; label?: unknown };
|
|
@@ -444,7 +448,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
444
448
|
const { credential, aaguid, credentialDeviceType, credentialBackedUp } =
|
|
445
449
|
result.registrationInfo;
|
|
446
450
|
const createdAt = now();
|
|
447
|
-
|
|
451
|
+
ctx.db
|
|
448
452
|
.prepare(
|
|
449
453
|
`INSERT INTO passkey_credentials
|
|
450
454
|
(id, public_key, counter, transports, rp_id, aaguid, device_type,
|
|
@@ -463,7 +467,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
463
467
|
sanitizeLabel(body.label),
|
|
464
468
|
createdAt
|
|
465
469
|
);
|
|
466
|
-
const row = credentialById(credential.id);
|
|
470
|
+
const row = credentialById(ctx.db, credential.id);
|
|
467
471
|
return c.json({ ok: true, credential: row ? toSummary(row) : null });
|
|
468
472
|
} catch (err) {
|
|
469
473
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -476,7 +480,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
476
480
|
// delete stale localhost/dev credentials. The client badges foreign RPs.
|
|
477
481
|
app.get("/auth/passkey/list", (c) => {
|
|
478
482
|
if (mode !== "password") return notEnabled(c);
|
|
479
|
-
const rows =
|
|
483
|
+
const rows = ctx.db
|
|
480
484
|
.query("SELECT * FROM passkey_credentials ORDER BY created_at DESC")
|
|
481
485
|
.all() as CredentialRow[];
|
|
482
486
|
return c.json({ credentials: rows.map(toSummary) });
|
|
@@ -490,7 +494,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
490
494
|
} catch {
|
|
491
495
|
return c.json({ error: "Invalid request body" }, 400);
|
|
492
496
|
}
|
|
493
|
-
const result =
|
|
497
|
+
const result = ctx.db
|
|
494
498
|
.prepare("UPDATE passkey_credentials SET label = ? WHERE id = ?")
|
|
495
499
|
.run(sanitizeLabel(body.label), c.req.param("id"));
|
|
496
500
|
if (result.changes === 0) return c.json({ error: "Unknown passkey" }, 404);
|
|
@@ -499,7 +503,7 @@ export function passkeyManagementRoutes(mode: AuthMode, deps: PasskeyDeps = {}):
|
|
|
499
503
|
|
|
500
504
|
app.delete("/auth/passkey/:id", (c) => {
|
|
501
505
|
if (mode !== "password") return notEnabled(c);
|
|
502
|
-
const result =
|
|
506
|
+
const result = ctx.db
|
|
503
507
|
.prepare("DELETE FROM passkey_credentials WHERE id = ?")
|
|
504
508
|
.run(c.req.param("id"));
|
|
505
509
|
if (result.changes === 0) return c.json({ error: "Unknown passkey" }, 404);
|
|
@@ -6,21 +6,21 @@ import { getConnInfo } from "hono/bun";
|
|
|
6
6
|
* localhost.
|
|
7
7
|
*
|
|
8
8
|
* SECURITY: header trust is OFF by default. An exposed port lets anyone spoof
|
|
9
|
-
* `x-forwarded-for`, so the real socket address is used unless
|
|
10
|
-
* — set that ONLY when a trusted
|
|
11
|
-
* forwarding header.
|
|
9
|
+
* `x-forwarded-for`, so the real socket address is used unless the resolved
|
|
10
|
+
* config says `trustProxy` (TRUST_PROXY=1) — set that ONLY when a trusted
|
|
11
|
+
* reverse proxy in front of brain-ui sets the forwarding header.
|
|
12
12
|
*/
|
|
13
13
|
export interface TailscaleGuardOptions {
|
|
14
|
-
/** Trust x-forwarded-for / x-real-ip.
|
|
15
|
-
trustProxy
|
|
14
|
+
/** Trust x-forwarded-for / x-real-ip. */
|
|
15
|
+
trustProxy: boolean;
|
|
16
|
+
/** How many trusted proxies front the app (default 1). */
|
|
17
|
+
trustProxyHops?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function tailscaleGuard(
|
|
19
|
-
|
|
20
|
-
): MiddlewareHandler {
|
|
21
|
-
const trustProxy = options.trustProxy ?? process.env.TRUST_PROXY === "1";
|
|
20
|
+
export function tailscaleGuard(options: TailscaleGuardOptions): MiddlewareHandler {
|
|
21
|
+
const { trustProxy, trustProxyHops } = options;
|
|
22
22
|
return async (c, next) => {
|
|
23
|
-
if (isTailscaleAllowed(c, trustProxy)) {
|
|
23
|
+
if (isTailscaleAllowed(c, trustProxy, trustProxyHops)) {
|
|
24
24
|
await next();
|
|
25
25
|
} else {
|
|
26
26
|
return c.json({ error: "VPN access required" }, 403);
|
|
@@ -29,8 +29,12 @@ export function tailscaleGuard(
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** True when the request's client IP is a Tailscale or loopback address. */
|
|
32
|
-
export function isTailscaleAllowed(
|
|
33
|
-
|
|
32
|
+
export function isTailscaleAllowed(
|
|
33
|
+
c: Context,
|
|
34
|
+
trustProxy: boolean,
|
|
35
|
+
trustProxyHops = 1
|
|
36
|
+
): boolean {
|
|
37
|
+
const ip = clientIp(c, trustProxy, trustProxyHops);
|
|
34
38
|
return isTailscaleIp(ip) || isLocalIp(ip);
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -42,11 +46,12 @@ export function isTailscaleAllowed(c: Context, trustProxy: boolean): boolean {
|
|
|
42
46
|
*
|
|
43
47
|
* X-Forwarded-For is parsed RIGHT-TO-LEFT: each proxy appends the address it saw,
|
|
44
48
|
* so the rightmost entries are the trusted hops we control and the leftmost is
|
|
45
|
-
* client-controllable. `
|
|
46
|
-
* this app; the client IP is the entry just before them. Taking the
|
|
47
|
-
* entry (the old behaviour) let a client spoof its address by
|
|
49
|
+
* client-controllable. `trustProxyHops` (TRUST_PROXY_HOPS, default 1) is how many
|
|
50
|
+
* proxies front this app; the client IP is the entry just before them. Taking the
|
|
51
|
+
* leftmost entry (the old behaviour) let a client spoof its address by
|
|
52
|
+
* pre-seeding XFF.
|
|
48
53
|
*/
|
|
49
|
-
export function clientIp(c: Context, trustProxy: boolean): string {
|
|
54
|
+
export function clientIp(c: Context, trustProxy: boolean, trustProxyHops = 1): string {
|
|
50
55
|
if (trustProxy) {
|
|
51
56
|
const forwarded = c.req.header("x-forwarded-for");
|
|
52
57
|
if (forwarded) {
|
|
@@ -55,7 +60,7 @@ export function clientIp(c: Context, trustProxy: boolean): string {
|
|
|
55
60
|
.map((part) => part.trim())
|
|
56
61
|
.filter(Boolean);
|
|
57
62
|
if (parts.length > 0) {
|
|
58
|
-
const hops = Math.max(1,
|
|
63
|
+
const hops = Math.max(1, trustProxyHops);
|
|
59
64
|
const ip = parts[Math.max(0, parts.length - hops)];
|
|
60
65
|
if (ip) return ip;
|
|
61
66
|
}
|
package/src/routes/brain.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { streamSSE } from "hono/streaming";
|
|
3
|
+
import type { BrainClient } from "../brain/client.js";
|
|
4
|
+
import { subprocessEnv } from "../config/env.js";
|
|
3
5
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
brainAdd,
|
|
9
|
-
brainCliCommand,
|
|
10
|
-
} from "../brain/client.js";
|
|
11
|
-
import { buildKeyterms, writeCache } from "../voice/keyterm-builder.js";
|
|
6
|
+
buildKeyterms,
|
|
7
|
+
writeCache,
|
|
8
|
+
type KeytermSettings,
|
|
9
|
+
} from "../voice/keyterm-builder.js";
|
|
12
10
|
import { existsSync } from "fs";
|
|
13
11
|
import { join } from "path";
|
|
14
12
|
|
|
13
|
+
export interface BrainRoutesDeps {
|
|
14
|
+
brain: BrainClient;
|
|
15
|
+
brainPath: string;
|
|
16
|
+
keyterms: KeytermSettings;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Locate the brain repo's `whatsup` script, relative to the repo root.
|
|
17
21
|
*
|
|
@@ -31,14 +35,17 @@ function findWhatsupScript(brainPath: string): string | null {
|
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export
|
|
38
|
+
export function createBrainRoutes(deps: BrainRoutesDeps): Hono {
|
|
39
|
+
const { brain, brainPath, keyterms } = deps;
|
|
40
|
+
|
|
41
|
+
return new Hono()
|
|
35
42
|
.get("/brain/search", async (c) => {
|
|
36
43
|
const q = c.req.query("q");
|
|
37
44
|
if (!q) {
|
|
38
45
|
return c.json({ error: "Query parameter 'q' is required" }, 400);
|
|
39
46
|
}
|
|
40
47
|
try {
|
|
41
|
-
const { results, warnings } = await
|
|
48
|
+
const { results, warnings } = await brain.search(q, {
|
|
42
49
|
type: c.req.query("type"),
|
|
43
50
|
tag: c.req.query("tag"),
|
|
44
51
|
limit: c.req.query("limit") ? Number(c.req.query("limit")) : undefined,
|
|
@@ -55,7 +62,7 @@ export const brainRoutes = new Hono()
|
|
|
55
62
|
|
|
56
63
|
.get("/brain/briefing", async (c) => {
|
|
57
64
|
try {
|
|
58
|
-
const content = await
|
|
65
|
+
const content = await brain.briefing();
|
|
59
66
|
return c.json({ content });
|
|
60
67
|
} catch (err) {
|
|
61
68
|
return c.json(
|
|
@@ -66,10 +73,6 @@ export const brainRoutes = new Hono()
|
|
|
66
73
|
})
|
|
67
74
|
|
|
68
75
|
.post("/brain/whatsup", async (c) => {
|
|
69
|
-
const BRAIN_PATH =
|
|
70
|
-
process.env.BRAIN_PATH ||
|
|
71
|
-
`${process.env.HOME || "/root"}/brain`;
|
|
72
|
-
|
|
73
76
|
// Defeat reverse-proxy buffering for SSE
|
|
74
77
|
c.header("X-Accel-Buffering", "no");
|
|
75
78
|
|
|
@@ -91,7 +94,7 @@ export const brainRoutes = new Hono()
|
|
|
91
94
|
try {
|
|
92
95
|
await send({ type: "start", text: "Running whatsup briefing..." });
|
|
93
96
|
|
|
94
|
-
const script = findWhatsupScript(
|
|
97
|
+
const script = findWhatsupScript(brainPath);
|
|
95
98
|
if (!script) {
|
|
96
99
|
// Terminate with the frames the client actually handles: it only
|
|
97
100
|
// reads "progress" and "done", so an "error" type would leave the
|
|
@@ -109,10 +112,10 @@ export const brainRoutes = new Hono()
|
|
|
109
112
|
// --claude backend uses --bare mode, which can't read the
|
|
110
113
|
// CLAUDE_CODE_OAUTH_TOKEN either).
|
|
111
114
|
const proc = Bun.spawn(["bun", script, "--gemini"], {
|
|
112
|
-
cwd:
|
|
115
|
+
cwd: brainPath,
|
|
113
116
|
stdout: "pipe",
|
|
114
117
|
stderr: "pipe",
|
|
115
|
-
env: {
|
|
118
|
+
env: subprocessEnv({ NO_COLOR: "1" }),
|
|
116
119
|
});
|
|
117
120
|
|
|
118
121
|
const reader = proc.stdout.getReader();
|
|
@@ -159,7 +162,7 @@ export const brainRoutes = new Hono()
|
|
|
159
162
|
|
|
160
163
|
.get("/brain/stats", async (c) => {
|
|
161
164
|
try {
|
|
162
|
-
const stats = await
|
|
165
|
+
const stats = await brain.stats();
|
|
163
166
|
return c.json(stats);
|
|
164
167
|
} catch (err) {
|
|
165
168
|
return c.json(
|
|
@@ -171,7 +174,7 @@ export const brainRoutes = new Hono()
|
|
|
171
174
|
|
|
172
175
|
.get("/brain/list", async (c) => {
|
|
173
176
|
try {
|
|
174
|
-
const results = await
|
|
177
|
+
const results = await brain.list({
|
|
175
178
|
type: c.req.query("type"),
|
|
176
179
|
tag: c.req.query("tag"),
|
|
177
180
|
status: c.req.query("status"),
|
|
@@ -190,9 +193,6 @@ export const brainRoutes = new Hono()
|
|
|
190
193
|
.post("/brain/sync", async (c) => {
|
|
191
194
|
// Stream sync output via SSE so the client sees live progress.
|
|
192
195
|
// brain sync spawns a nested Claude Code process that can take minutes.
|
|
193
|
-
const BRAIN_PATH =
|
|
194
|
-
process.env.BRAIN_PATH ||
|
|
195
|
-
`${process.env.HOME || "/root"}/brain`;
|
|
196
196
|
|
|
197
197
|
return streamSSE(c, async (stream) => {
|
|
198
198
|
const send = (data: {
|
|
@@ -212,17 +212,17 @@ export const brainRoutes = new Hono()
|
|
|
212
212
|
|
|
213
213
|
// Merge stderr into stdout — brain sync spawns a nested Claude Code
|
|
214
214
|
// process whose output goes to stderr. The command comes from
|
|
215
|
-
//
|
|
215
|
+
// brain.cliCommand() (packaged bin or legacy vendored script), passed to
|
|
216
216
|
// bash as positional args rather than interpolated into the script, so a
|
|
217
217
|
// BRAIN_PATH containing spaces or shell metacharacters stays inert.
|
|
218
|
-
const [cliBin, ...cliArgs] =
|
|
218
|
+
const [cliBin, ...cliArgs] = brain.cliCommand();
|
|
219
219
|
const proc = Bun.spawn(
|
|
220
220
|
["bash", "-c", '"$0" "$@" 2>&1', cliBin!, ...cliArgs, "sync"],
|
|
221
221
|
{
|
|
222
|
-
cwd:
|
|
222
|
+
cwd: brainPath,
|
|
223
223
|
stdout: "pipe",
|
|
224
224
|
stderr: "pipe",
|
|
225
|
-
env: {
|
|
225
|
+
env: subprocessEnv({ NO_COLOR: "1" }),
|
|
226
226
|
}
|
|
227
227
|
);
|
|
228
228
|
|
|
@@ -254,8 +254,8 @@ export const brainRoutes = new Hono()
|
|
|
254
254
|
// Rebuild voice keyterms cache after a successful sync.
|
|
255
255
|
if (exitCode === 0) {
|
|
256
256
|
try {
|
|
257
|
-
const cache = buildKeyterms();
|
|
258
|
-
writeCache(cache);
|
|
257
|
+
const cache = buildKeyterms(keyterms);
|
|
258
|
+
writeCache(keyterms, cache);
|
|
259
259
|
await send({
|
|
260
260
|
type: "progress",
|
|
261
261
|
text: `[voice] Rebuilt keyterms cache (${cache.count} terms)`,
|
|
@@ -290,7 +290,7 @@ export const brainRoutes = new Hono()
|
|
|
290
290
|
return c.json({ error: "Field 'content' is required" }, 400);
|
|
291
291
|
}
|
|
292
292
|
try {
|
|
293
|
-
await
|
|
293
|
+
await brain.add(body.content, {
|
|
294
294
|
type: body.type,
|
|
295
295
|
title: body.title,
|
|
296
296
|
tags: body.tags,
|
|
@@ -303,3 +303,4 @@ export const brainRoutes = new Hono()
|
|
|
303
303
|
);
|
|
304
304
|
}
|
|
305
305
|
});
|
|
306
|
+
}
|
package/src/routes/files.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
TooLargeError,
|
|
11
11
|
} from "../files/walker.js";
|
|
12
12
|
|
|
13
|
-
let wikilinkCache: { generatedAt: number; map: Record<string, string> } | null = null;
|
|
14
13
|
const WIKILINK_TTL_MS = 30_000;
|
|
15
14
|
|
|
16
15
|
function errorResponse(err: unknown): { body: { error: string; size?: number }; status: 400 | 404 | 413 | 500 } {
|
|
@@ -27,11 +26,14 @@ function errorResponse(err: unknown): { body: { error: string; size?: number };
|
|
|
27
26
|
return { body: { error: err instanceof Error ? err.message : "internal_error" }, status: 500 };
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export
|
|
29
|
+
export function createFilesRoutes(deps: { brainRoot: string }): Hono {
|
|
30
|
+
const { brainRoot } = deps;
|
|
31
|
+
let wikilinkCache: { generatedAt: number; map: Record<string, string> } | null = null;
|
|
32
|
+
return new Hono()
|
|
31
33
|
.get("/files/tree", async (c) => {
|
|
32
34
|
const path = c.req.query("path") ?? "";
|
|
33
35
|
try {
|
|
34
|
-
const entries = await listDirectory(path);
|
|
36
|
+
const entries = await listDirectory(path, brainRoot);
|
|
35
37
|
return c.json({ path, entries });
|
|
36
38
|
} catch (err) {
|
|
37
39
|
const { body, status } = errorResponse(err);
|
|
@@ -45,7 +47,7 @@ export const filesRoutes = new Hono()
|
|
|
45
47
|
const raw = c.req.query("raw") === "1";
|
|
46
48
|
try {
|
|
47
49
|
if (raw) {
|
|
48
|
-
const { abs, mime, size } = await resolveForRaw(path);
|
|
50
|
+
const { abs, mime, size } = await resolveForRaw(path, brainRoot);
|
|
49
51
|
const file = Bun.file(abs);
|
|
50
52
|
return new Response(file, {
|
|
51
53
|
status: 200,
|
|
@@ -59,7 +61,7 @@ export const filesRoutes = new Hono()
|
|
|
59
61
|
},
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
|
-
const result = await readFileContent(path);
|
|
64
|
+
const result = await readFileContent(path, brainRoot);
|
|
63
65
|
return c.json({ path, ...result });
|
|
64
66
|
} catch (err) {
|
|
65
67
|
const { body, status } = errorResponse(err);
|
|
@@ -71,7 +73,7 @@ export const filesRoutes = new Hono()
|
|
|
71
73
|
const path = c.req.query("path");
|
|
72
74
|
if (!path) return c.json({ error: "missing_path" }, 400);
|
|
73
75
|
try {
|
|
74
|
-
const result = await resolveAncestors(path);
|
|
76
|
+
const result = await resolveAncestors(path, brainRoot);
|
|
75
77
|
return c.json({ path, ...result });
|
|
76
78
|
} catch (err) {
|
|
77
79
|
const { body, status } = errorResponse(err);
|
|
@@ -84,7 +86,7 @@ export const filesRoutes = new Hono()
|
|
|
84
86
|
const now = Date.now();
|
|
85
87
|
try {
|
|
86
88
|
if (refresh || !wikilinkCache || now - wikilinkCache.generatedAt > WIKILINK_TTL_MS) {
|
|
87
|
-
const map = await buildWikilinkMap();
|
|
89
|
+
const map = await buildWikilinkMap(brainRoot);
|
|
88
90
|
wikilinkCache = { generatedAt: now, map };
|
|
89
91
|
}
|
|
90
92
|
return c.json({
|
|
@@ -97,3 +99,4 @@ export const filesRoutes = new Hono()
|
|
|
97
99
|
return c.json(body, status);
|
|
98
100
|
}
|
|
99
101
|
});
|
|
102
|
+
}
|