tinywebui 0.1.3 → 0.1.5
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 +20 -13
- package/bin/tinywebui.js +69 -63
- package/package.json +25 -8
- package/public/app.js +40 -14
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -49
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +83 -20
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -138
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -451
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +98 -66
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -100
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +428 -392
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- package/src/set-password.js +0 -43
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/http.js
CHANGED
|
@@ -1,61 +1,64 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { dirname, join, normalize } from 'node:path';
|
|
4
|
-
|
|
5
4
|
const PUBLIC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
|
6
5
|
const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml' };
|
|
7
|
-
|
|
8
6
|
export function json(res, code, payload) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const body = JSON.stringify(payload);
|
|
8
|
+
res.writeHead(code, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) });
|
|
9
|
+
res.end(body);
|
|
12
10
|
}
|
|
13
|
-
|
|
14
11
|
/** An error that carries its own HTTP status to the handler's catch. */
|
|
15
12
|
export class HttpError extends Error {
|
|
16
|
-
|
|
13
|
+
status;
|
|
14
|
+
constructor(status, message) { super(message); this.status = status; }
|
|
17
15
|
}
|
|
18
|
-
|
|
19
16
|
// Body caps. Most requests are a few KB; uploads and turns with images carry
|
|
20
17
|
// base64 (4/3 of the file), so they get room for their own per-file limits.
|
|
21
18
|
export const BODY_LIMIT = 1024 * 1024;
|
|
22
|
-
export const UPLOAD_LIMIT = 8 * 1024 * 1024;
|
|
23
|
-
export const TURN_LIMIT = 60 * 1024 * 1024;
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
export const UPLOAD_LIMIT = 8 * 1024 * 1024; // one 5 MB file, base64
|
|
20
|
+
export const TURN_LIMIT = 60 * 1024 * 1024; // up to 8 images of 5 MB, base64
|
|
21
|
+
/** True when a body field is one of the allowed values, which also tells TypeScript which. */
|
|
22
|
+
export const oneOf = (value, allowed) => typeof value === 'string' && allowed.includes(value);
|
|
23
|
+
/** Reads a JSON object body, refusing more than `limit` bytes (413), bad JSON or anything but an object (400). */
|
|
26
24
|
export async function readJson(req, limit = BODY_LIMIT) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
const declared = Number(req.headers['content-length']);
|
|
26
|
+
if (declared > limit)
|
|
27
|
+
throw new HttpError(413, `request body exceeds ${limit} bytes`);
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let size = 0;
|
|
30
|
+
for await (const c of req) {
|
|
31
|
+
size += c.length;
|
|
32
|
+
if (size > limit)
|
|
33
|
+
throw new HttpError(413, `request body exceeds ${limit} bytes`);
|
|
34
|
+
chunks.push(c);
|
|
35
|
+
}
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new HttpError(400, 'request body is not valid JSON');
|
|
42
|
+
}
|
|
43
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
44
|
+
throw new HttpError(400, 'request body must be a JSON object');
|
|
45
|
+
return body;
|
|
41
46
|
}
|
|
42
|
-
|
|
43
47
|
// Sent on every response. The page loads only its own scripts; the one
|
|
44
48
|
// outside origin is the Fall Fairy theme's Google Fonts.
|
|
45
49
|
export const SECURITY_HEADERS = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
'content-security-policy': [
|
|
51
|
+
"default-src 'self'", "script-src 'self'", "connect-src 'self'",
|
|
52
|
+
"style-src 'self' https://fonts.googleapis.com",
|
|
53
|
+
"font-src 'self' data: https://fonts.gstatic.com",
|
|
54
|
+
"img-src 'self' data: blob:", "object-src 'none'", "base-uri 'none'",
|
|
55
|
+
"frame-ancestors 'none'", "form-action 'self'"
|
|
56
|
+
].join('; '),
|
|
57
|
+
'x-content-type-options': 'nosniff',
|
|
58
|
+
'x-frame-options': 'DENY',
|
|
59
|
+
'referrer-policy': 'no-referrer',
|
|
60
|
+
'cross-origin-opener-policy': 'same-origin'
|
|
57
61
|
};
|
|
58
|
-
|
|
59
62
|
/**
|
|
60
63
|
* Cross-site request forgery guard for anything that changes state. Browsers
|
|
61
64
|
* send Origin on cross-origin POSTs (and Sec-Fetch-Site on all modern ones),
|
|
@@ -63,33 +66,62 @@ export const SECURITY_HEADERS = {
|
|
|
63
66
|
* gateway's included -- or poke a tier-1 instance on localhost.
|
|
64
67
|
*/
|
|
65
68
|
export function crossSite(req, cfg) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS')
|
|
70
|
+
return false;
|
|
71
|
+
const origin = req.headers.origin;
|
|
72
|
+
if (origin && origin !== 'null') {
|
|
73
|
+
let host;
|
|
74
|
+
try {
|
|
75
|
+
host = new URL(origin).host;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (host === req.headers.host)
|
|
81
|
+
return false;
|
|
82
|
+
return !(cfg.allowedOrigins || []).includes(origin);
|
|
83
|
+
}
|
|
84
|
+
if (origin === 'null')
|
|
85
|
+
return true;
|
|
86
|
+
return req.headers['sec-fetch-site'] === 'cross-site';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* DNS-rebinding guard. With no login, a hostile site can point its own name at
|
|
90
|
+
* 127.0.0.1 and pass the same-origin check above (Origin and Host both name it),
|
|
91
|
+
* so a no-login install only answers to loopback names and allowedHosts.
|
|
92
|
+
* Logged-in modes are safe: the attacker's origin has no session cookie.
|
|
93
|
+
*/
|
|
94
|
+
export function badHost(req, cfg) {
|
|
95
|
+
if (cfg.authMode !== 'local')
|
|
96
|
+
return false;
|
|
97
|
+
let hostname;
|
|
98
|
+
try {
|
|
99
|
+
hostname = new URL(`http://${req.headers.host}`).hostname.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '127.0.0.1' || hostname === '[::1]')
|
|
105
|
+
return false;
|
|
106
|
+
return !(cfg.allowedHosts || []).some((h) => h.toLowerCase() === hostname);
|
|
76
107
|
}
|
|
77
|
-
|
|
78
108
|
export async function serveStatic(req, res) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
// Resolve the index BEFORE normalising: on Windows normalize('/') returns a
|
|
110
|
+
// lone backslash, so a check for '/' after it never matches and the root
|
|
111
|
+
// request lands on the directory itself.
|
|
112
|
+
const pathname = (req.url ?? '/').split('?')[0];
|
|
113
|
+
const rel = normalize(pathname === '/' ? 'index.html' : pathname)
|
|
114
|
+
.replace(/^(\.\.[/\\])+/, '');
|
|
115
|
+
const file = join(PUBLIC_DIR, rel);
|
|
116
|
+
if (!file.startsWith(PUBLIC_DIR))
|
|
117
|
+
return json(res, 403, { error: 'forbidden' });
|
|
118
|
+
try {
|
|
119
|
+
const data = await readFile(file);
|
|
120
|
+
const ext = file.slice(file.lastIndexOf('.'));
|
|
121
|
+
res.writeHead(200, { 'content-type': TYPES[ext] || 'application/octet-stream' });
|
|
122
|
+
res.end(data);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
json(res, 404, { error: 'not found' });
|
|
126
|
+
}
|
|
95
127
|
}
|
package/src/log.js
CHANGED
|
@@ -7,18 +7,17 @@
|
|
|
7
7
|
* filtered by level.
|
|
8
8
|
*/
|
|
9
9
|
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: Infinity };
|
|
10
|
-
|
|
11
10
|
const threshold = () => LEVELS[String(process.env.TINYWEBUI_LOG_LEVEL || '').toLowerCase()] ?? LEVELS.info;
|
|
12
|
-
|
|
13
11
|
export function logger(area) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const line = (level, write) => (message) => {
|
|
13
|
+
if (LEVELS[level] < threshold())
|
|
14
|
+
return;
|
|
15
|
+
write(`[tinywebui:${area}] ${level === 'info' ? '' : `${level}: `}${message}`);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
debug: line('debug', console.log),
|
|
19
|
+
info: line('info', console.log),
|
|
20
|
+
warn: line('warn', console.error),
|
|
21
|
+
error: line('error', console.error)
|
|
22
|
+
};
|
|
24
23
|
}
|