@schlessera/brain-ui-server 0.30.1 → 0.32.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 +46 -5
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +49 -25
- package/dist/app.js.map +1 -1
- package/dist/config/env.d.ts +6 -5
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +13 -7
- package/dist/config/env.js.map +1 -1
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +70 -10
- package/dist/files/walker.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +25 -5
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +255 -47
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/body-limit.d.ts +15 -0
- package/dist/middleware/body-limit.d.ts.map +1 -0
- package/dist/middleware/body-limit.js +41 -0
- package/dist/middleware/body-limit.js.map +1 -0
- package/dist/middleware/origin.d.ts +20 -12
- package/dist/middleware/origin.d.ts.map +1 -1
- package/dist/middleware/origin.js +71 -25
- package/dist/middleware/origin.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +5 -1
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +67 -26
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +7 -2
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.js +2 -2
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +22 -10
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/pi-auth.d.ts.map +1 -1
- package/dist/routes/pi-auth.js +12 -4
- package/dist/routes/pi-auth.js.map +1 -1
- package/dist/routes/push.d.ts.map +1 -1
- package/dist/routes/push.js +12 -4
- package/dist/routes/push.js.map +1 -1
- package/dist/routes/render.d.ts +6 -0
- package/dist/routes/render.d.ts.map +1 -1
- package/dist/routes/render.js +8 -11
- package/dist/routes/render.js.map +1 -1
- package/dist/routes/share.d.ts +2 -0
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +30 -31
- package/dist/routes/share.js.map +1 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +22 -8
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/web-search.d.ts.map +1 -1
- package/dist/routes/web-search.js +7 -2
- package/dist/routes/web-search.js.map +1 -1
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +2 -1
- package/dist/share/staging.js.map +1 -1
- package/dist/ws/clients.d.ts +13 -4
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +41 -5
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +10 -2
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/host.d.ts +5 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +18 -1
- package/dist/ws/host.js.map +1 -1
- package/package.json +3 -3
- package/src/app.ts +55 -23
- package/src/config/env.ts +18 -7
- package/src/files/walker.ts +66 -10
- package/src/index.ts +8 -0
- package/src/middleware/auth.ts +311 -48
- package/src/middleware/body-limit.ts +47 -0
- package/src/middleware/origin.ts +92 -24
- package/src/middleware/passkeys.ts +80 -29
- package/src/routes/brain.ts +7 -3
- package/src/routes/files.ts +2 -2
- package/src/routes/models.ts +17 -10
- package/src/routes/pi-auth.ts +10 -4
- package/src/routes/push.ts +10 -4
- package/src/routes/render.ts +14 -10
- package/src/routes/share.ts +35 -34
- package/src/routes/skills.ts +18 -8
- package/src/routes/web-search.ts +6 -2
- package/src/share/staging.ts +2 -1
- package/src/ws/clients.ts +45 -7
- package/src/ws/connection.ts +11 -2
- package/src/ws/host.ts +23 -1
package/src/config/env.ts
CHANGED
|
@@ -19,7 +19,10 @@ import type { BillingMode } from "@schlessera/brain-ui-sdk/protocol";
|
|
|
19
19
|
|
|
20
20
|
import { SEVERITIES, type Severity } from "../observability/types.js";
|
|
21
21
|
import { envFlag } from "./env-core.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
filterSubprocessEnv,
|
|
24
|
+
WEB_SEARCH_PROVIDERS,
|
|
25
|
+
} from "@schlessera/brain-ui-sdk/server";
|
|
23
26
|
|
|
24
27
|
// --- descriptor -------------------------------------------------------------
|
|
25
28
|
|
|
@@ -126,6 +129,12 @@ export const ENV_VARS: readonly EnvVarDescriptor[] = [
|
|
|
126
129
|
default: "the shipped pattern set",
|
|
127
130
|
required: false,
|
|
128
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: "BRAIN_UI_WS_MAX_CONNECTIONS",
|
|
134
|
+
description: "Maximum number of WebSocket connections accepted by one server process.",
|
|
135
|
+
default: "32",
|
|
136
|
+
required: false,
|
|
137
|
+
},
|
|
129
138
|
{
|
|
130
139
|
name: "BRAIN_UI_WS_RATE",
|
|
131
140
|
description:
|
|
@@ -471,6 +480,8 @@ export interface ServerConfig {
|
|
|
471
480
|
logLevel: Severity;
|
|
472
481
|
/** Inbound WebSocket frame metering, per connection. */
|
|
473
482
|
wsRate: { ratePerSecond: number; burst: number };
|
|
483
|
+
/** Maximum WebSocket connections accepted by one server process. */
|
|
484
|
+
wsMaxConnections: number;
|
|
474
485
|
auth: AuthConfig;
|
|
475
486
|
webauthn: WebAuthnConfig;
|
|
476
487
|
agent: AgentConfig;
|
|
@@ -633,6 +644,7 @@ export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig
|
|
|
633
644
|
ambientBilling: resolveAmbientBillingMode(env),
|
|
634
645
|
},
|
|
635
646
|
logLevel: parseSeverity(env.BRAIN_UI_LOG_LEVEL),
|
|
647
|
+
wsMaxConnections: positiveNumber(env.BRAIN_UI_WS_MAX_CONNECTIONS, 32),
|
|
636
648
|
wsRate: {
|
|
637
649
|
ratePerSecond: positiveNumber(env.BRAIN_UI_WS_RATE, 20),
|
|
638
650
|
burst: positiveNumber(env.BRAIN_UI_WS_BURST, 60),
|
|
@@ -651,14 +663,13 @@ export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig
|
|
|
651
663
|
}
|
|
652
664
|
|
|
653
665
|
/**
|
|
654
|
-
* The parent environment for spawned subprocesses (brain CLI,
|
|
655
|
-
* overrides.
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* chokepoint.
|
|
666
|
+
* The filtered parent environment for spawned subprocesses (brain CLI,
|
|
667
|
+
* whatsup), plus explicit overrides. The shared descriptor strips server-only
|
|
668
|
+
* material while retaining all known subprocess capabilities and unknown
|
|
669
|
+
* variables. This reads `process.env`, so it lives behind this chokepoint.
|
|
659
670
|
*/
|
|
660
671
|
export function subprocessEnv(extra: Record<string, string> = {}): EnvRecord {
|
|
661
|
-
return { ...process.env, ...extra };
|
|
672
|
+
return { ...filterSubprocessEnv(process.env), ...extra };
|
|
662
673
|
}
|
|
663
674
|
|
|
664
675
|
/**
|
package/src/files/walker.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { readdir, readFile, stat, realpath } from "node:fs/promises";
|
|
2
|
-
import { join, resolve, sep, posix, dirname } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { lstat, readdir, readFile, stat, realpath } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve, sep, posix, dirname } from "node:path";
|
|
4
3
|
import ignore, { type Ignore } from "ignore";
|
|
5
4
|
import type { FileEntry, FileContentKind } from "@schlessera/brain-ui-sdk/protocol";
|
|
6
5
|
import { FILE_SIZE_CAP_BYTES } from "@schlessera/brain-ui-sdk/protocol";
|
|
@@ -25,6 +24,15 @@ const HARD_EXCLUDE_FILE_NAMES = new Set(
|
|
|
25
24
|
].map((n) => n.toLowerCase())
|
|
26
25
|
);
|
|
27
26
|
|
|
27
|
+
const UNRESOLVABLE_PATH_CODES = new Set([
|
|
28
|
+
"ENOENT",
|
|
29
|
+
"ENOTDIR",
|
|
30
|
+
"ENAMETOOLONG",
|
|
31
|
+
"ELOOP",
|
|
32
|
+
"EACCES",
|
|
33
|
+
"EPERM",
|
|
34
|
+
]);
|
|
35
|
+
|
|
28
36
|
export class PathEscapeError extends Error {
|
|
29
37
|
constructor(rel: string) {
|
|
30
38
|
super(`path_escape: ${rel}`);
|
|
@@ -52,8 +60,14 @@ export class TooLargeError extends Error {
|
|
|
52
60
|
*/
|
|
53
61
|
export async function safeResolve(rel: string, root: string): Promise<string> {
|
|
54
62
|
if (typeof rel !== "string") throw new PathEscapeError(String(rel));
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const requestSegments = rel.split("/");
|
|
64
|
+
// Reject absolute paths, null bytes, and Windows path syntax before resolving.
|
|
65
|
+
if (
|
|
66
|
+
rel.startsWith("/") ||
|
|
67
|
+
rel.startsWith("\\") ||
|
|
68
|
+
rel.includes("\0") ||
|
|
69
|
+
requestSegments.some((segment) => segment.includes(":") || segment.includes("\\"))
|
|
70
|
+
) {
|
|
57
71
|
throw new PathEscapeError(rel);
|
|
58
72
|
}
|
|
59
73
|
// Normalize separators; reject ".." segments anywhere
|
|
@@ -61,14 +75,49 @@ export async function safeResolve(rel: string, root: string): Promise<string> {
|
|
|
61
75
|
for (const seg of normalized.split("/")) {
|
|
62
76
|
if (seg === "..") throw new PathEscapeError(rel);
|
|
63
77
|
}
|
|
64
|
-
const
|
|
78
|
+
const lexicalRoot = resolve(root);
|
|
79
|
+
let absRoot: string;
|
|
80
|
+
try {
|
|
81
|
+
absRoot = await realpath(lexicalRoot);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
84
|
+
absRoot = lexicalRoot;
|
|
85
|
+
}
|
|
65
86
|
const abs = resolve(absRoot, normalized);
|
|
66
87
|
if (abs !== absRoot && !abs.startsWith(absRoot + sep)) {
|
|
67
88
|
throw new PathEscapeError(rel);
|
|
68
89
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
// Canonicalize every existing component. Stopping at the first genuinely
|
|
91
|
+
// missing component permits callers to create a new path, while lstat
|
|
92
|
+
// distinguishes that case from a dangling symlink, which must fail closed.
|
|
93
|
+
let candidate = absRoot;
|
|
94
|
+
const candidates = [candidate];
|
|
95
|
+
for (const segment of relative(absRoot, abs).split(sep)) {
|
|
96
|
+
if (!segment || segment === ".") continue;
|
|
97
|
+
candidate = join(candidate, segment);
|
|
98
|
+
candidates.push(candidate);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const existing of candidates) {
|
|
102
|
+
try {
|
|
103
|
+
await lstat(existing);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
106
|
+
if (code && UNRESOLVABLE_PATH_CODES.has(code)) break;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let real: string;
|
|
111
|
+
try {
|
|
112
|
+
real = await realpath(existing);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
115
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
116
|
+
throw new PathEscapeError(rel);
|
|
117
|
+
}
|
|
118
|
+
if (code && UNRESOLVABLE_PATH_CODES.has(code)) break;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
72
121
|
if (real !== absRoot && !real.startsWith(absRoot + sep)) {
|
|
73
122
|
throw new PathEscapeError(rel);
|
|
74
123
|
}
|
|
@@ -373,7 +422,14 @@ export async function buildWikilinkMap(root: string): Promise<Record<string, str
|
|
|
373
422
|
}
|
|
374
423
|
|
|
375
424
|
if (childStat.isDirectory()) {
|
|
376
|
-
|
|
425
|
+
try {
|
|
426
|
+
await walk(relChild);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
// Directory names come from readdir rather than an external request.
|
|
429
|
+
// Keep request-facing validation strict, but skip a discovered path
|
|
430
|
+
// that safeResolve rejects instead of discarding the whole map.
|
|
431
|
+
if (!(err instanceof PathEscapeError)) throw err;
|
|
432
|
+
}
|
|
377
433
|
continue;
|
|
378
434
|
}
|
|
379
435
|
const lower = d.name.toLowerCase();
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,9 @@ export {
|
|
|
40
40
|
type ModelDiscoveryState,
|
|
41
41
|
} from "./agent/backend.js";
|
|
42
42
|
export {
|
|
43
|
+
authGuard,
|
|
44
|
+
bumpSessionsEpoch,
|
|
45
|
+
isWsAuthorized,
|
|
43
46
|
resolveAuthMode,
|
|
44
47
|
type AuthMode,
|
|
45
48
|
type AuthRuntime,
|
|
@@ -107,6 +110,11 @@ export {
|
|
|
107
110
|
// intake route also sweeps opportunistically on every share.
|
|
108
111
|
export { pruneShareStaging, shareStagingRoot } from "./share/staging.js";
|
|
109
112
|
|
|
113
|
+
// Skill archive transport sizing for deployment shells. The route keeps the
|
|
114
|
+
// application-level check; consumers use this value only to avoid setting a
|
|
115
|
+
// smaller process-level request ceiling.
|
|
116
|
+
export { MAX_ARCHIVE_BYTES } from "./skills/install.js";
|
|
117
|
+
|
|
110
118
|
// Observability: the producing side is the OpenTelemetry API, the consuming
|
|
111
119
|
// side is ours. Swap the consumer to change where a deployment reports; a test
|
|
112
120
|
// swaps in the recording one and asserts on what the server actually said.
|