@schlessera/brain-ui-server 0.4.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.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/agent/backend.d.ts +30 -0
  4. package/dist/agent/backend.d.ts.map +1 -0
  5. package/dist/agent/backend.js +226 -0
  6. package/dist/agent/backend.js.map +1 -0
  7. package/dist/app.d.ts +27 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +116 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/brain/client.d.ts +43 -0
  12. package/dist/brain/client.d.ts.map +1 -0
  13. package/dist/brain/client.js +154 -0
  14. package/dist/brain/client.js.map +1 -0
  15. package/dist/brain/types.d.ts +41 -0
  16. package/dist/brain/types.d.ts.map +1 -0
  17. package/dist/brain/types.js +2 -0
  18. package/dist/brain/types.js.map +1 -0
  19. package/dist/cron/scheduler.d.ts +9 -0
  20. package/dist/cron/scheduler.d.ts.map +1 -0
  21. package/dist/cron/scheduler.js +58 -0
  22. package/dist/cron/scheduler.js.map +1 -0
  23. package/dist/db/client.d.ts +10 -0
  24. package/dist/db/client.d.ts.map +1 -0
  25. package/dist/db/client.js +73 -0
  26. package/dist/db/client.js.map +1 -0
  27. package/dist/files/walker.d.ts +63 -0
  28. package/dist/files/walker.d.ts.map +1 -0
  29. package/dist/files/walker.js +416 -0
  30. package/dist/files/walker.js.map +1 -0
  31. package/dist/index.d.ts +18 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +25 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +59 -0
  36. package/dist/middleware/auth.d.ts.map +1 -0
  37. package/dist/middleware/auth.js +246 -0
  38. package/dist/middleware/auth.js.map +1 -0
  39. package/dist/middleware/passkeys.d.ts +36 -0
  40. package/dist/middleware/passkeys.d.ts.map +1 -0
  41. package/dist/middleware/passkeys.js +427 -0
  42. package/dist/middleware/passkeys.js.map +1 -0
  43. package/dist/middleware/tailscale.d.ts +31 -0
  44. package/dist/middleware/tailscale.d.ts.map +1 -0
  45. package/dist/middleware/tailscale.js +64 -0
  46. package/dist/middleware/tailscale.js.map +1 -0
  47. package/dist/render/template.d.ts +7 -0
  48. package/dist/render/template.d.ts.map +1 -0
  49. package/dist/render/template.js +112 -0
  50. package/dist/render/template.js.map +1 -0
  51. package/dist/routes/brain.d.ts +155 -0
  52. package/dist/routes/brain.d.ts.map +1 -0
  53. package/dist/routes/brain.js +250 -0
  54. package/dist/routes/brain.js.map +1 -0
  55. package/dist/routes/files.d.ts +89 -0
  56. package/dist/routes/files.d.ts.map +1 -0
  57. package/dist/routes/files.js +91 -0
  58. package/dist/routes/files.js.map +1 -0
  59. package/dist/routes/health.d.ts +37 -0
  60. package/dist/routes/health.d.ts.map +1 -0
  61. package/dist/routes/health.js +27 -0
  62. package/dist/routes/health.js.map +1 -0
  63. package/dist/routes/providers.d.ts +33 -0
  64. package/dist/routes/providers.d.ts.map +1 -0
  65. package/dist/routes/providers.js +9 -0
  66. package/dist/routes/providers.js.map +1 -0
  67. package/dist/routes/render.d.ts +25 -0
  68. package/dist/routes/render.d.ts.map +1 -0
  69. package/dist/routes/render.js +47 -0
  70. package/dist/routes/render.js.map +1 -0
  71. package/dist/routes/sessions.d.ts +79 -0
  72. package/dist/routes/sessions.d.ts.map +1 -0
  73. package/dist/routes/sessions.js +37 -0
  74. package/dist/routes/sessions.js.map +1 -0
  75. package/dist/routes/voice.d.ts +92 -0
  76. package/dist/routes/voice.d.ts.map +1 -0
  77. package/dist/routes/voice.js +64 -0
  78. package/dist/routes/voice.js.map +1 -0
  79. package/dist/voice/deepgram-token.d.ts +5 -0
  80. package/dist/voice/deepgram-token.d.ts.map +1 -0
  81. package/dist/voice/deepgram-token.js +63 -0
  82. package/dist/voice/deepgram-token.js.map +1 -0
  83. package/dist/voice/keyterm-builder.d.ts +18 -0
  84. package/dist/voice/keyterm-builder.d.ts.map +1 -0
  85. package/dist/voice/keyterm-builder.js +391 -0
  86. package/dist/voice/keyterm-builder.js.map +1 -0
  87. package/dist/voice/speech-providers.d.ts +25 -0
  88. package/dist/voice/speech-providers.d.ts.map +1 -0
  89. package/dist/voice/speech-providers.js +104 -0
  90. package/dist/voice/speech-providers.js.map +1 -0
  91. package/dist/ws/attachments.d.ts +10 -0
  92. package/dist/ws/attachments.d.ts.map +1 -0
  93. package/dist/ws/attachments.js +63 -0
  94. package/dist/ws/attachments.js.map +1 -0
  95. package/dist/ws/bridge.d.ts +7 -0
  96. package/dist/ws/bridge.d.ts.map +1 -0
  97. package/dist/ws/bridge.js +86 -0
  98. package/dist/ws/bridge.js.map +1 -0
  99. package/dist/ws/clients.d.ts +20 -0
  100. package/dist/ws/clients.d.ts.map +1 -0
  101. package/dist/ws/clients.js +48 -0
  102. package/dist/ws/clients.js.map +1 -0
  103. package/dist/ws/connection.d.ts +8 -0
  104. package/dist/ws/connection.d.ts.map +1 -0
  105. package/dist/ws/connection.js +116 -0
  106. package/dist/ws/connection.js.map +1 -0
  107. package/dist/ws/dispatch.d.ts +5 -0
  108. package/dist/ws/dispatch.d.ts.map +1 -0
  109. package/dist/ws/dispatch.js +139 -0
  110. package/dist/ws/dispatch.js.map +1 -0
  111. package/dist/ws/frames.d.ts +17 -0
  112. package/dist/ws/frames.d.ts.map +1 -0
  113. package/dist/ws/frames.js +37 -0
  114. package/dist/ws/frames.js.map +1 -0
  115. package/dist/ws/handler.d.ts +34 -0
  116. package/dist/ws/handler.d.ts.map +1 -0
  117. package/dist/ws/handler.js +33 -0
  118. package/dist/ws/handler.js.map +1 -0
  119. package/dist/ws/history.d.ts +17 -0
  120. package/dist/ws/history.d.ts.map +1 -0
  121. package/dist/ws/history.js +45 -0
  122. package/dist/ws/history.js.map +1 -0
  123. package/dist/ws/host.d.ts +40 -0
  124. package/dist/ws/host.d.ts.map +1 -0
  125. package/dist/ws/host.js +49 -0
  126. package/dist/ws/host.js.map +1 -0
  127. package/dist/ws/routing.d.ts +12 -0
  128. package/dist/ws/routing.d.ts.map +1 -0
  129. package/dist/ws/routing.js +31 -0
  130. package/dist/ws/routing.js.map +1 -0
  131. package/dist/ws/run-session.d.ts +17 -0
  132. package/dist/ws/run-session.d.ts.map +1 -0
  133. package/dist/ws/run-session.js +160 -0
  134. package/dist/ws/run-session.js.map +1 -0
  135. package/dist/ws/session-catalog.d.ts +25 -0
  136. package/dist/ws/session-catalog.d.ts.map +1 -0
  137. package/dist/ws/session-catalog.js +45 -0
  138. package/dist/ws/session-catalog.js.map +1 -0
  139. package/dist/ws/shrink.d.ts +34 -0
  140. package/dist/ws/shrink.d.ts.map +1 -0
  141. package/dist/ws/shrink.js +91 -0
  142. package/dist/ws/shrink.js.map +1 -0
  143. package/dist/ws/turns.d.ts +77 -0
  144. package/dist/ws/turns.d.ts.map +1 -0
  145. package/dist/ws/turns.js +89 -0
  146. package/dist/ws/turns.js.map +1 -0
  147. package/migrations/001_initial.sql +29 -0
  148. package/migrations/002_session_provider.sql +1 -0
  149. package/migrations/003_session_backend.sql +5 -0
  150. package/migrations/004_drop_gemini_backend.sql +20 -0
  151. package/migrations/005_passkey_credentials.sql +18 -0
  152. package/package.json +72 -0
  153. package/src/agent/backend.ts +308 -0
  154. package/src/app.ts +159 -0
  155. package/src/brain/client.ts +186 -0
  156. package/src/brain/types.ts +44 -0
  157. package/src/cron/scheduler.ts +80 -0
  158. package/src/db/client.ts +85 -0
  159. package/src/files/walker.ts +418 -0
  160. package/src/index.ts +39 -0
  161. package/src/middleware/auth.ts +321 -0
  162. package/src/middleware/passkeys.ts +510 -0
  163. package/src/middleware/tailscale.ts +81 -0
  164. package/src/render/template.ts +124 -0
  165. package/src/routes/brain.ts +305 -0
  166. package/src/routes/files.ts +99 -0
  167. package/src/routes/health.ts +29 -0
  168. package/src/routes/providers.ts +9 -0
  169. package/src/routes/render.ts +68 -0
  170. package/src/routes/sessions.ts +49 -0
  171. package/src/routes/voice.ts +80 -0
  172. package/src/voice/deepgram-token.ts +80 -0
  173. package/src/voice/keyterm-builder.ts +426 -0
  174. package/src/voice/speech-providers.ts +111 -0
  175. package/src/ws/attachments.ts +86 -0
  176. package/src/ws/bridge.ts +139 -0
  177. package/src/ws/clients.ts +57 -0
  178. package/src/ws/connection.ts +132 -0
  179. package/src/ws/dispatch.ts +162 -0
  180. package/src/ws/frames.ts +42 -0
  181. package/src/ws/handler.ts +50 -0
  182. package/src/ws/history.ts +51 -0
  183. package/src/ws/host.ts +69 -0
  184. package/src/ws/routing.ts +43 -0
  185. package/src/ws/run-session.ts +192 -0
  186. package/src/ws/session-catalog.ts +97 -0
  187. package/src/ws/shrink.ts +95 -0
  188. package/src/ws/turns.ts +151 -0
@@ -0,0 +1,427 @@
1
+ import { Hono } from "hono";
2
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse as realVerifyAuthentication, verifyRegistrationResponse as realVerifyRegistration, } from "@simplewebauthn/server";
3
+ import { issueSessionCookie, consumeLoginToken, LOGIN_RATE_LIMIT, GLOBAL_LOGIN_RATE_LIMIT, } from "./auth.js";
4
+ import { clientIp } from "./tailscale.js";
5
+ import { getDb } from "../db/client.js";
6
+ /**
7
+ * WebAuthn passkeys as an extension of `password` mode: the password bootstraps
8
+ * the first registration (and stays as recovery), passkeys are the day-to-day
9
+ * login. A successful assertion mints the same session cookie as password login
10
+ * (issueSessionCookie), so authGuard / isWsAuthorized / TTL are untouched.
11
+ *
12
+ * Self-contained on purpose — this module (plus its auth.ts imports) is slated
13
+ * to move wholesale into @schlessera/ui-server.
14
+ *
15
+ * Credentials are RP-scoped: a credential registered on localhost does not
16
+ * exist for the production domain and vice versa. Rows carry rp_id so all
17
+ * environments share the table.
18
+ */
19
+ const CHALLENGE_TTL_MS = 120_000; // > the 60s ceremony timeout
20
+ // Pending challenges are unauthenticated state; cap and evict oldest-first so
21
+ // login-options spam can't grow memory. Losing one costs a retried ceremony.
22
+ const MAX_PENDING_CHALLENGES = 100;
23
+ const OPTIONS_RATE_LIMIT = 10; // login-options per IP per minute (own bucket)
24
+ // Branding/identity are config-injected with the historical values as
25
+ // defaults, so existing deployments keep their credentials.
26
+ function rpName() {
27
+ return process.env.WEBAUTHN_RP_NAME || "Brain UI";
28
+ }
29
+ // The single "user" every passkey belongs to. Stable across registrations so
30
+ // an authenticator overwrites its existing entry instead of stacking
31
+ // duplicates for the same site.
32
+ //
33
+ // WARNING: the user handle is part of the WebAuthn wire contract — it is
34
+ // burned into every resident credential at registration. Changing
35
+ // WEBAUTHN_USER_ID on a deployment that already has passkeys orphans them
36
+ // (authenticators will present a handle the server no longer recognizes as
37
+ // the same user and re-registration stacks a second entry). Set it before the
38
+ // first passkey is registered and never change it.
39
+ function userName() {
40
+ return process.env.WEBAUTHN_USER_NAME || "owner";
41
+ }
42
+ /** WebAuthn caps `user.id` at 64 bytes; browsers reject anything longer. */
43
+ const MAX_USER_ID_BYTES = 64;
44
+ function userId() {
45
+ return new TextEncoder().encode(process.env.WEBAUTHN_USER_ID || "brain-ui-owner");
46
+ }
47
+ /**
48
+ * Refuse to boot on a WebAuthn user handle the browser would reject. Called
49
+ * from createApp(): an oversized handle otherwise produces registration
50
+ * options that look fine server-side and fail silently in every browser.
51
+ */
52
+ export function assertPasskeyConfig() {
53
+ const bytes = userId().byteLength;
54
+ if (bytes > MAX_USER_ID_BYTES) {
55
+ throw new Error(`WEBAUTHN_USER_ID is ${bytes} bytes; WebAuthn allows at most ${MAX_USER_ID_BYTES}. ` +
56
+ "Pick a shorter stable identifier (it is burned into every credential).");
57
+ }
58
+ }
59
+ // ES256 + RS256 only: every mainstream authenticator supports ES256, and it
60
+ // keeps Ed25519 (whose WebCrypto verify path on Bun is unproven) out of the
61
+ // database. Widen once Bun's Ed25519 verify is validated.
62
+ const SUPPORTED_ALGORITHM_IDS = [-7, -257];
63
+ // --- challenge store (in-memory: single-process server; move to a shared
64
+ // store if this ever runs multi-process) ---
65
+ const pendingChallenges = new Map();
66
+ function putChallenge(challenge, type, now) {
67
+ for (const [key, entry] of pendingChallenges) {
68
+ if (entry.expiresAt <= now)
69
+ pendingChallenges.delete(key);
70
+ }
71
+ while (pendingChallenges.size >= MAX_PENDING_CHALLENGES) {
72
+ const oldest = pendingChallenges.keys().next().value;
73
+ if (oldest === undefined)
74
+ break;
75
+ pendingChallenges.delete(oldest);
76
+ }
77
+ pendingChallenges.set(challenge, { type, expiresAt: now + CHALLENGE_TTL_MS });
78
+ }
79
+ /** Single-use: deleted before verification, so a failed verify still burns it. */
80
+ function consumeChallenge(challenge, type, now) {
81
+ const entry = pendingChallenges.get(challenge);
82
+ if (!entry)
83
+ return false;
84
+ pendingChallenges.delete(challenge);
85
+ return entry.type === type && entry.expiresAt > now;
86
+ }
87
+ /** The challenge the browser actually signed, from clientDataJSON. */
88
+ function challengeFromResponse(response) {
89
+ try {
90
+ const json = JSON.parse(Buffer.from(response.response.clientDataJSON, "base64url").toString("utf8"));
91
+ return typeof json.challenge === "string" ? json.challenge : null;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ // --- RP / origin resolution ---
98
+ function envList(name) {
99
+ return (process.env[name] ?? "")
100
+ .split(",")
101
+ .map((s) => s.trim())
102
+ .filter(Boolean);
103
+ }
104
+ function isLoopbackHostname(hostname) {
105
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
106
+ }
107
+ /**
108
+ * Derive the relying party from the request instead of demanding new env:
109
+ * same-origin deployments, split topology (ALLOWED_ORIGINS), and dev all work
110
+ * unconfigured. WEBAUTHN_ORIGINS / WEBAUTHN_RP_ID remain as overrides for
111
+ * proxies that rewrite Host.
112
+ */
113
+ function resolveRp(c) {
114
+ const origin = c.req.header("origin");
115
+ if (!origin)
116
+ return null;
117
+ let url;
118
+ try {
119
+ url = new URL(origin);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ const allowed = envList("WEBAUTHN_ORIGINS").includes(origin) ||
125
+ envList("ALLOWED_ORIGINS").includes(origin) ||
126
+ url.host === c.req.header("host") ||
127
+ // Dev escape hatch: the vite proxy sets changeOrigin, so the server sees
128
+ // Host localhost:3000 but Origin http://localhost:5173. Opt-in via an
129
+ // explicit flag — NOT NODE_ENV, whose absence must never widen the RP.
130
+ (process.env.BRAIN_UI_ALLOW_LOOPBACK_ORIGIN === "1" &&
131
+ isLoopbackHostname(url.hostname));
132
+ if (!allowed)
133
+ return null;
134
+ return { rpID: process.env.WEBAUTHN_RP_ID ?? url.hostname, origin };
135
+ }
136
+ /** rpID for endpoints where same-origin GETs legitimately omit Origin. */
137
+ function rpIdForRead(c) {
138
+ if (process.env.WEBAUTHN_RP_ID)
139
+ return process.env.WEBAUTHN_RP_ID;
140
+ const origin = c.req.header("origin");
141
+ if (origin) {
142
+ try {
143
+ return new URL(origin).hostname;
144
+ }
145
+ catch {
146
+ // fall through to Host
147
+ }
148
+ }
149
+ const host = c.req.header("host") ?? "localhost";
150
+ return host.replace(/:\d+$/, "");
151
+ }
152
+ /** Wire shape shared with the client (shared/protocol.ts PasskeySummary). */
153
+ function toSummary(row) {
154
+ return {
155
+ id: row.id,
156
+ label: row.label,
157
+ rpId: row.rp_id,
158
+ createdAt: row.created_at,
159
+ lastUsedAt: row.last_used_at,
160
+ backedUp: row.backed_up === 1,
161
+ deviceType: row.device_type,
162
+ transports: row.transports ? JSON.parse(row.transports) : null,
163
+ aaguid: row.aaguid,
164
+ };
165
+ }
166
+ function credentialsForRp(rpID) {
167
+ return getDb()
168
+ .query("SELECT * FROM passkey_credentials WHERE rp_id = ?")
169
+ .all(rpID);
170
+ }
171
+ function credentialById(id) {
172
+ return getDb()
173
+ .query("SELECT * FROM passkey_credentials WHERE id = ?")
174
+ .get(id);
175
+ }
176
+ function toWebAuthnCredential(row) {
177
+ return {
178
+ id: row.id,
179
+ publicKey: new Uint8Array(row.public_key),
180
+ counter: row.counter,
181
+ transports: row.transports
182
+ ? JSON.parse(row.transports)
183
+ : undefined,
184
+ };
185
+ }
186
+ function sanitizeLabel(label) {
187
+ return typeof label === "string" ? label.trim().slice(0, 64) : "";
188
+ }
189
+ function notEnabled(c) {
190
+ return c.json({ error: "Passkey login is not enabled" }, 400);
191
+ }
192
+ /**
193
+ * Once a passkey exists for the request's RP, password login is disabled —
194
+ * the password becomes registration-bootstrap and break-glass recovery only
195
+ * (set BRAIN_UI_ALLOW_PASSWORD=1 to re-enable it, e.g. after losing every
196
+ * authenticator). Scoped per RP so e.g. localhost dev keeps password login
197
+ * until it has its own credential. Host-derived rpID is fine here: a forged
198
+ * Host can at most re-enable password auth, which still requires the password.
199
+ */
200
+ export function passwordLoginDisabled(c) {
201
+ if (process.env.BRAIN_UI_ALLOW_PASSWORD === "1")
202
+ return false;
203
+ const row = getDb()
204
+ .query("SELECT COUNT(*) AS n FROM passkey_credentials WHERE rp_id = ?")
205
+ .get(rpIdForRead(c));
206
+ return row.n > 0;
207
+ }
208
+ /**
209
+ * Unauthenticated passkey routes. Mount BEFORE the auth guard, next to
210
+ * authRoutes.
211
+ */
212
+ export function passkeyPublicRoutes(mode, deps = {}) {
213
+ const verifyAuthentication = deps.verifyAuthenticationResponse ?? realVerifyAuthentication;
214
+ const now = deps.now ?? Date.now;
215
+ const app = new Hono();
216
+ // Capability hint for the login screen: which methods can it offer?
217
+ app.get("/auth/methods", (c) => {
218
+ c.header("Cache-Control", "no-store");
219
+ if (mode !== "password") {
220
+ return c.json({ password: false, passkey: false });
221
+ }
222
+ const row = getDb()
223
+ .query("SELECT COUNT(*) AS n FROM passkey_credentials WHERE rp_id = ?")
224
+ .get(rpIdForRead(c));
225
+ return c.json({ password: !passwordLoginDisabled(c), passkey: row.n > 0 });
226
+ });
227
+ app.post("/auth/passkey/login-options", async (c) => {
228
+ if (mode !== "password")
229
+ return notEnabled(c);
230
+ const ip = clientIp(c, process.env.TRUST_PROXY === "1") || "unknown";
231
+ // Own bucket: conditional-UI mounts fire this on every login-screen load
232
+ // and must not starve real verify attempts of their shared budget.
233
+ if (!consumeLoginToken(`pk-opt:${ip}`, OPTIONS_RATE_LIMIT)) {
234
+ return c.json({ error: "Too many attempts. Try again in a minute." }, 429);
235
+ }
236
+ const rp = resolveRp(c);
237
+ if (!rp)
238
+ return c.json({ error: "Origin not allowed" }, 400);
239
+ // Empty allowCredentials + discoverable credentials = usernameless login.
240
+ const options = await generateAuthenticationOptions({
241
+ rpID: rp.rpID,
242
+ allowCredentials: [],
243
+ userVerification: "required",
244
+ });
245
+ putChallenge(options.challenge, "authentication", now());
246
+ return c.json(options);
247
+ });
248
+ app.post("/auth/passkey/login-verify", async (c) => {
249
+ if (mode !== "password")
250
+ return notEnabled(c);
251
+ const ip = clientIp(c, process.env.TRUST_PROXY === "1") || "unknown";
252
+ // Same buckets as password login: one combined online-guess budget.
253
+ const perIpOk = consumeLoginToken(`ip:${ip}`, LOGIN_RATE_LIMIT);
254
+ const globalOk = consumeLoginToken("global", GLOBAL_LOGIN_RATE_LIMIT);
255
+ if (!perIpOk || !globalOk) {
256
+ return c.json({ error: "Too many attempts. Try again in a minute." }, 429);
257
+ }
258
+ const rp = resolveRp(c);
259
+ if (!rp)
260
+ return c.json({ error: "Origin not allowed" }, 400);
261
+ let response;
262
+ try {
263
+ response = await c.req.json();
264
+ }
265
+ catch {
266
+ return c.json({ error: "Invalid request body" }, 400);
267
+ }
268
+ const fail = () => c.json({ error: "Passkey verification failed" }, 401);
269
+ const row = typeof response?.id === "string" ? credentialById(response.id) : null;
270
+ if (!row || row.rp_id !== rp.rpID)
271
+ return fail();
272
+ const challenge = challengeFromResponse(response);
273
+ if (!challenge || !consumeChallenge(challenge, "authentication", now())) {
274
+ return fail();
275
+ }
276
+ try {
277
+ const result = await verifyAuthentication({
278
+ response,
279
+ expectedChallenge: challenge,
280
+ expectedOrigin: rp.origin,
281
+ expectedRPID: rp.rpID,
282
+ credential: toWebAuthnCredential(row),
283
+ });
284
+ if (!result.verified)
285
+ return fail();
286
+ getDb()
287
+ .prepare("UPDATE passkey_credentials SET counter = ?, last_used_at = ? WHERE id = ?")
288
+ .run(result.authenticationInfo.newCounter, now(), row.id);
289
+ }
290
+ catch (err) {
291
+ const message = err instanceof Error ? err.message : String(err);
292
+ // The library throws on a counter regression — possible cloned
293
+ // credential. Cloud passkeys legitimately sit at 0, so warn, don't
294
+ // revoke.
295
+ if (/counter/i.test(message)) {
296
+ console.warn(`[passkeys] counter regression for credential ${row.id} — possible cloned credential`);
297
+ }
298
+ else {
299
+ console.warn(`[passkeys] authentication failed: ${message}`);
300
+ }
301
+ return fail();
302
+ }
303
+ await issueSessionCookie(c);
304
+ return c.json({ ok: true });
305
+ });
306
+ return app;
307
+ }
308
+ /**
309
+ * Session-gated passkey routes. Mount AFTER the auth guard — gating comes from
310
+ * mount position, not per-route checks.
311
+ */
312
+ export function passkeyManagementRoutes(mode, deps = {}) {
313
+ const verifyRegistration = deps.verifyRegistrationResponse ?? realVerifyRegistration;
314
+ const now = deps.now ?? Date.now;
315
+ const app = new Hono();
316
+ app.post("/auth/passkey/register-options", async (c) => {
317
+ if (mode !== "password")
318
+ return notEnabled(c);
319
+ const rp = resolveRp(c);
320
+ if (!rp)
321
+ return c.json({ error: "Origin not allowed" }, 400);
322
+ const options = await generateRegistrationOptions({
323
+ rpName: rpName(),
324
+ rpID: rp.rpID,
325
+ userName: userName(),
326
+ userID: userId(),
327
+ userDisplayName: rpName(),
328
+ attestationType: "none",
329
+ excludeCredentials: credentialsForRp(rp.rpID).map((row) => ({
330
+ id: row.id,
331
+ transports: toWebAuthnCredential(row).transports,
332
+ })),
333
+ authenticatorSelection: {
334
+ residentKey: "required",
335
+ userVerification: "required",
336
+ },
337
+ supportedAlgorithmIDs: SUPPORTED_ALGORITHM_IDS,
338
+ });
339
+ putChallenge(options.challenge, "registration", now());
340
+ return c.json(options);
341
+ });
342
+ app.post("/auth/passkey/register-verify", async (c) => {
343
+ if (mode !== "password")
344
+ return notEnabled(c);
345
+ const rp = resolveRp(c);
346
+ if (!rp)
347
+ return c.json({ error: "Origin not allowed" }, 400);
348
+ let body;
349
+ try {
350
+ body = await c.req.json();
351
+ }
352
+ catch {
353
+ return c.json({ error: "Invalid request body" }, 400);
354
+ }
355
+ if (!body.response)
356
+ return c.json({ error: "Invalid request body" }, 400);
357
+ const challenge = challengeFromResponse(body.response);
358
+ if (!challenge || !consumeChallenge(challenge, "registration", now())) {
359
+ return c.json({ error: "Passkey registration failed" }, 400);
360
+ }
361
+ try {
362
+ const result = await verifyRegistration({
363
+ response: body.response,
364
+ expectedChallenge: challenge,
365
+ expectedOrigin: rp.origin,
366
+ expectedRPID: rp.rpID,
367
+ });
368
+ if (!result.verified || !result.registrationInfo) {
369
+ return c.json({ error: "Passkey registration failed" }, 400);
370
+ }
371
+ const { credential, aaguid, credentialDeviceType, credentialBackedUp } = result.registrationInfo;
372
+ const createdAt = now();
373
+ getDb()
374
+ .prepare(`INSERT INTO passkey_credentials
375
+ (id, public_key, counter, transports, rp_id, aaguid, device_type,
376
+ backed_up, label, created_at)
377
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
378
+ .run(credential.id, credential.publicKey, credential.counter, credential.transports ? JSON.stringify(credential.transports) : null, rp.rpID, aaguid || null, credentialDeviceType, credentialBackedUp ? 1 : 0, sanitizeLabel(body.label), createdAt);
379
+ const row = credentialById(credential.id);
380
+ return c.json({ ok: true, credential: row ? toSummary(row) : null });
381
+ }
382
+ catch (err) {
383
+ const message = err instanceof Error ? err.message : String(err);
384
+ console.warn(`[passkeys] registration failed: ${message}`);
385
+ return c.json({ error: "Passkey registration failed" }, 400);
386
+ }
387
+ });
388
+ // All rows, not rpID-filtered: the prod panel must be able to show and
389
+ // delete stale localhost/dev credentials. The client badges foreign RPs.
390
+ app.get("/auth/passkey/list", (c) => {
391
+ if (mode !== "password")
392
+ return notEnabled(c);
393
+ const rows = getDb()
394
+ .query("SELECT * FROM passkey_credentials ORDER BY created_at DESC")
395
+ .all();
396
+ return c.json({ credentials: rows.map(toSummary) });
397
+ });
398
+ app.put("/auth/passkey/:id", async (c) => {
399
+ if (mode !== "password")
400
+ return notEnabled(c);
401
+ let body;
402
+ try {
403
+ body = await c.req.json();
404
+ }
405
+ catch {
406
+ return c.json({ error: "Invalid request body" }, 400);
407
+ }
408
+ const result = getDb()
409
+ .prepare("UPDATE passkey_credentials SET label = ? WHERE id = ?")
410
+ .run(sanitizeLabel(body.label), c.req.param("id"));
411
+ if (result.changes === 0)
412
+ return c.json({ error: "Unknown passkey" }, 404);
413
+ return c.json({ ok: true });
414
+ });
415
+ app.delete("/auth/passkey/:id", (c) => {
416
+ if (mode !== "password")
417
+ return notEnabled(c);
418
+ const result = getDb()
419
+ .prepare("DELETE FROM passkey_credentials WHERE id = ?")
420
+ .run(c.req.param("id"));
421
+ if (result.changes === 0)
422
+ return c.json({ error: "Unknown passkey" }, 404);
423
+ return c.json({ ok: true });
424
+ });
425
+ return app;
426
+ }
427
+ //# sourceMappingURL=passkeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkeys.js","sourceRoot":"","sources":["../../src/middleware/passkeys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ /**
3
+ * Restrict access to Tailscale VPN clients (CGNAT range 100.64.0.0/10) or
4
+ * localhost.
5
+ *
6
+ * SECURITY: header trust is OFF by default. An exposed port lets anyone spoof
7
+ * `x-forwarded-for`, so the real socket address is used unless `TRUST_PROXY=1`
8
+ * — set that ONLY when a trusted reverse proxy in front of brain-ui sets the
9
+ * forwarding header.
10
+ */
11
+ export interface TailscaleGuardOptions {
12
+ /** Trust x-forwarded-for / x-real-ip. Defaults to `TRUST_PROXY === "1"`. */
13
+ trustProxy?: boolean;
14
+ }
15
+ export declare function tailscaleGuard(options?: TailscaleGuardOptions): MiddlewareHandler;
16
+ /** True when the request's client IP is a Tailscale or loopback address. */
17
+ export declare function isTailscaleAllowed(c: Context, trustProxy: boolean): boolean;
18
+ /**
19
+ * The client IP. With `trustProxy`, read the forwarding header a trusted proxy
20
+ * set; otherwise use the real socket address (unspoofable). Returns "" when the
21
+ * source is unavailable (e.g. Hono's `app.request()` has no socket) — callers
22
+ * treat "" as not-allowed.
23
+ *
24
+ * X-Forwarded-For is parsed RIGHT-TO-LEFT: each proxy appends the address it saw,
25
+ * so the rightmost entries are the trusted hops we control and the leftmost is
26
+ * client-controllable. `TRUST_PROXY_HOPS` (default 1) is how many proxies front
27
+ * this app; the client IP is the entry just before them. Taking the leftmost
28
+ * entry (the old behaviour) let a client spoof its address by pre-seeding XFF.
29
+ */
30
+ export declare function clientIp(c: Context, trustProxy: boolean): string;
31
+ //# sourceMappingURL=tailscale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailscale.d.ts","sourceRoot":"","sources":["../../src/middleware/tailscale.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,iBAAiB,CASnB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,OAAO,CAG3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAqBhE"}
@@ -0,0 +1,64 @@
1
+ import { getConnInfo } from "hono/bun";
2
+ export function tailscaleGuard(options = {}) {
3
+ const trustProxy = options.trustProxy ?? process.env.TRUST_PROXY === "1";
4
+ return async (c, next) => {
5
+ if (isTailscaleAllowed(c, trustProxy)) {
6
+ await next();
7
+ }
8
+ else {
9
+ return c.json({ error: "VPN access required" }, 403);
10
+ }
11
+ };
12
+ }
13
+ /** True when the request's client IP is a Tailscale or loopback address. */
14
+ export function isTailscaleAllowed(c, trustProxy) {
15
+ const ip = clientIp(c, trustProxy);
16
+ return isTailscaleIp(ip) || isLocalIp(ip);
17
+ }
18
+ /**
19
+ * The client IP. With `trustProxy`, read the forwarding header a trusted proxy
20
+ * set; otherwise use the real socket address (unspoofable). Returns "" when the
21
+ * source is unavailable (e.g. Hono's `app.request()` has no socket) — callers
22
+ * treat "" as not-allowed.
23
+ *
24
+ * X-Forwarded-For is parsed RIGHT-TO-LEFT: each proxy appends the address it saw,
25
+ * so the rightmost entries are the trusted hops we control and the leftmost is
26
+ * client-controllable. `TRUST_PROXY_HOPS` (default 1) is how many proxies front
27
+ * this app; the client IP is the entry just before them. Taking the leftmost
28
+ * entry (the old behaviour) let a client spoof its address by pre-seeding XFF.
29
+ */
30
+ export function clientIp(c, trustProxy) {
31
+ if (trustProxy) {
32
+ const forwarded = c.req.header("x-forwarded-for");
33
+ if (forwarded) {
34
+ const parts = forwarded
35
+ .split(",")
36
+ .map((part) => part.trim())
37
+ .filter(Boolean);
38
+ if (parts.length > 0) {
39
+ const hops = Math.max(1, Number(process.env.TRUST_PROXY_HOPS) || 1);
40
+ const ip = parts[Math.max(0, parts.length - hops)];
41
+ if (ip)
42
+ return ip;
43
+ }
44
+ }
45
+ return c.req.header("x-real-ip") ?? "";
46
+ }
47
+ try {
48
+ return getConnInfo(c).remote.address ?? "";
49
+ }
50
+ catch {
51
+ return "";
52
+ }
53
+ }
54
+ function isTailscaleIp(ip) {
55
+ // Tailscale CGNAT range: 100.64.0.0/10 (100.64.0.0 - 100.127.255.255)
56
+ if (!ip.startsWith("100."))
57
+ return false;
58
+ const second = parseInt(ip.split(".")[1], 10);
59
+ return second >= 64 && second <= 127;
60
+ }
61
+ function isLocalIp(ip) {
62
+ return ip === "127.0.0.1" || ip === "::1" || ip === "localhost";
63
+ }
64
+ //# sourceMappingURL=tailscale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailscale.js","sourceRoot":"","sources":["../../src/middleware/tailscale.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAgBvC,MAAM,UAAU,cAAc,CAC5B,UAAiC,EAAE;IAEnC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,GAAG,CAAC;IACzE,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,IAAI,kBAAkB,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,GAAG,CAAC,CAAC;QACvD,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,kBAAkB,CAAC,CAAU,EAAE,UAAmB;IAChE,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACnC,OAAO,aAAa,CAAC,EAAE,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CAAC,CAAU,EAAE,UAAmB;IACtD,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAClD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,SAAS;iBACpB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBAC1B,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;gBACpE,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;gBACnD,IAAI,EAAE;oBAAE,OAAO,EAAE,CAAC;YACpB,CAAC;QACH,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;IACD,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,EAAU;IAC/B,sEAAsE;IACtE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9C,OAAO,MAAM,IAAI,EAAE,IAAI,MAAM,IAAI,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,SAAS,CAAC,EAAU;IAC3B,OAAO,EAAE,KAAK,WAAW,IAAI,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,WAAW,CAAC;AAClE,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { RenderContentType } from "@schlessera/brain-ui-sdk/protocol";
2
+ export declare function buildHtmlDocument(opts: {
3
+ content: string;
4
+ contentType: RenderContentType;
5
+ title?: string;
6
+ }): string;
7
+ //# sourceMappingURL=template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/render/template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAqG3E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAiBT"}