@lelouchhe/webagent 0.8.0 → 0.10.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 +43 -15
- package/config.toml +7 -27
- package/dist/index.html +21 -5
- package/dist/js/app.INIQQEGD.js +5 -0
- package/dist/js/chunk.3CLGCUHW.js +1 -0
- package/dist/js/{chunk.CT5WBNGZ.js → chunk.7WADDFJZ.js} +50 -49
- package/dist/js/chunk.AOTG3PL7.js +20 -0
- package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
- package/dist/js/viewer.RHZMFYWJ.js +1 -0
- package/dist/login.html +2 -2
- package/dist/share-viewer.html +6 -6
- package/dist/{styles.00etlpgs.css → styles.01aj0l37.css} +186 -4
- package/dist/sw.js +6 -6
- package/lib/agent-key.js +6 -0
- package/lib/attachment-dispatch.js +60 -31
- package/lib/attachment-interceptor.js +7 -7
- package/lib/attachment-labels.js +1 -1
- package/lib/attachments.js +69 -7
- package/lib/auth-middleware.js +11 -4
- package/lib/auth.js +2 -2
- package/lib/bridge.js +209 -90
- package/lib/client-registry.js +12 -12
- package/lib/config.js +2 -31
- package/lib/event-handler.js +166 -85
- package/lib/files/limits.js +15 -0
- package/lib/files/paths.js +155 -0
- package/lib/files/routes.js +232 -0
- package/lib/home-path.js +35 -0
- package/lib/http-status.js +1 -0
- package/lib/mcp/capability.js +74 -0
- package/lib/mcp/server.js +148 -0
- package/lib/mcp/task-history.js +245 -0
- package/lib/mcp/task-host.js +253 -0
- package/lib/mcp/tools.js +168 -0
- package/lib/mode-bucket.js +1 -1
- package/lib/push-service.js +33 -35
- package/lib/routes.js +1022 -475
- package/lib/server.js +84 -34
- package/lib/share/routes.js +97 -85
- package/lib/shared/task-reference.js +20 -0
- package/lib/sse-manager.js +8 -8
- package/lib/store.js +992 -284
- package/lib/task-collaboration.js +15 -0
- package/lib/task-manager.js +1409 -0
- package/lib/task-path.js +131 -0
- package/lib/{session-state.js → task-state.js} +90 -38
- package/lib/task-tree-lock.js +74 -0
- package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
- package/lib/tokens.js +1 -1
- package/lib/types.js +2 -2
- package/package.json +8 -1
- package/dist/js/app.XBFXH37R.js +0 -2
- package/dist/js/chunk.UMQMOGWO.js +0 -1
- package/dist/js/viewer.CVWXSKJM.js +0 -1
- package/lib/session-manager.js +0 -613
- package/lib/title-service.js +0 -95
package/lib/attachments.js
CHANGED
|
@@ -16,6 +16,11 @@ const MIME_TO_EXT = {
|
|
|
16
16
|
"image/jpeg": "jpg",
|
|
17
17
|
"image/gif": "gif",
|
|
18
18
|
"image/webp": "webp",
|
|
19
|
+
"image/heic": "heic",
|
|
20
|
+
"image/heif": "heif",
|
|
21
|
+
"image/avif": "avif",
|
|
22
|
+
"image/bmp": "bmp",
|
|
23
|
+
"image/tiff": "tif",
|
|
19
24
|
"image/svg+xml": "svg",
|
|
20
25
|
"application/pdf": "pdf",
|
|
21
26
|
"text/plain": "txt",
|
|
@@ -70,14 +75,51 @@ function looksLikeUtf8Text(buf) {
|
|
|
70
75
|
if (byte === 0)
|
|
71
76
|
return false;
|
|
72
77
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
return decodesAsUtf8ToleratingTailTruncation(buf);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Maximum bytes one UTF-8 code point can occupy. A TRAILING code point that
|
|
82
|
+
* was cut off by the sampling window contributes at most one byte less than
|
|
83
|
+
* a full character (a complete 4-byte character would decode), so backing up
|
|
84
|
+
* `MAX_UTF8_CHAR_BYTES - 1` bytes always reaches the preceding boundary.
|
|
85
|
+
*/
|
|
86
|
+
const MAX_UTF8_CHAR_BYTES = 4;
|
|
87
|
+
/**
|
|
88
|
+
* Whether `buf` decodes cleanly as UTF-8, allowing its FINAL multi-byte
|
|
89
|
+
* character to be cut off at the buffer end.
|
|
90
|
+
*
|
|
91
|
+
* Why the tolerance: sniffMime only receives the first 4096 bytes of a
|
|
92
|
+
* larger file (the viewer's `SNIFF_BYTES` window in src/files/routes.ts and
|
|
93
|
+
* the upload-pipeline head). Content dense in multi-byte characters —
|
|
94
|
+
* Chinese, Japanese, emoji, ... — regularly leaves that window slicing
|
|
95
|
+
* clean through a character. A strict whole-buffer fatal decode then
|
|
96
|
+
* misclassifies perfectly valid UTF-8 text as binary, so the viewer
|
|
97
|
+
* force-downloads it instead of previewing it (real-world regression:
|
|
98
|
+
* ~100 of the markdown notes under ~/mine/space downloaded instead of
|
|
99
|
+
* rendering, even though every one of them was valid UTF-8).
|
|
100
|
+
*
|
|
101
|
+
* This relaxes ONLY the tail: a truncated trailing code point occupies at
|
|
102
|
+
* most 3 bytes of the buffer, so backing up at most 3 bytes reaches a
|
|
103
|
+
* character boundary — and the loop never trims the whole buffer, because an
|
|
104
|
+
* empty prefix would trivially "decode" and would turn short garbage buffers
|
|
105
|
+
* into text. NUL bytes and genuinely invalid sequences elsewhere fail every
|
|
106
|
+
* prefix attempt and still fall through to application/octet-stream — the
|
|
107
|
+
* "binary stays binary" security posture of the sniff is unchanged.
|
|
108
|
+
*/
|
|
109
|
+
function decodesAsUtf8ToleratingTailTruncation(buf) {
|
|
110
|
+
for (let end = buf.length; end >= Math.max(1, buf.length - (MAX_UTF8_CHAR_BYTES - 1)); end--) {
|
|
111
|
+
try {
|
|
112
|
+
// Fresh decoder per attempt: reuse-after-throw is not part of the
|
|
113
|
+
// TextDecoder contract across implementations.
|
|
114
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buf.subarray(0, end));
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Cut deeper into the tail. Only a trailing truncated character can
|
|
119
|
+
// make a later attempt succeed — real corruption fails all of them.
|
|
120
|
+
}
|
|
80
121
|
}
|
|
122
|
+
return false;
|
|
81
123
|
}
|
|
82
124
|
/**
|
|
83
125
|
* Mimes we are willing to render inline in <img>. Everything else is forced
|
|
@@ -93,6 +135,26 @@ const INLINE_MIMES = new Set([
|
|
|
93
135
|
export function isInlineMime(mime) {
|
|
94
136
|
return INLINE_MIMES.has(mime.toLowerCase());
|
|
95
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Image mimes the upstream model accepts as inline ACP image blocks.
|
|
140
|
+
*
|
|
141
|
+
* This is deliberately a SEPARATE set from INLINE_MIMES even though the two
|
|
142
|
+
* currently hold the same values: INLINE_MIMES is a display/XSS policy (which
|
|
143
|
+
* mimes may be served `Content-Disposition: inline` in the browser), while
|
|
144
|
+
* UPSTREAM_IMAGE_MIMES is an upstream compatibility policy (which image
|
|
145
|
+
* containers the model provider accepts on the wire). They answer different
|
|
146
|
+
* questions and will diverge — e.g. upstream support for HEIC would change
|
|
147
|
+
* only this set. Never reference one from the other's check.
|
|
148
|
+
*/
|
|
149
|
+
const UPSTREAM_IMAGE_MIMES = new Set([
|
|
150
|
+
"image/png",
|
|
151
|
+
"image/jpeg",
|
|
152
|
+
"image/gif",
|
|
153
|
+
"image/webp",
|
|
154
|
+
]);
|
|
155
|
+
export function isUpstreamImageMime(mime) {
|
|
156
|
+
return UPSTREAM_IMAGE_MIMES.has(mime.toLowerCase());
|
|
157
|
+
}
|
|
96
158
|
/**
|
|
97
159
|
* Classify an upload as "image" (sent as ACP image block, inlined as base64)
|
|
98
160
|
* or "file" (sent as ACP resource_link). Decision is mime-prefix-based —
|
package/lib/auth-middleware.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// All whitelisted paths must be GETs that read non-sensitive data,
|
|
2
|
-
// assets needed before the user can present a token
|
|
1
|
+
// All whitelisted paths must be GETs that either read non-sensitive data,
|
|
2
|
+
// serve static assets needed before the user can present a token, or enforce
|
|
3
|
+
// independent ticket/HMAC authentication inside their route handler.
|
|
3
4
|
const WHITELIST = [
|
|
4
5
|
// Public probes
|
|
5
6
|
{ method: "GET", test: (p) => p === "/api/v1/version" },
|
|
@@ -26,15 +27,21 @@ const WHITELIST = [
|
|
|
26
27
|
{ method: "GET", test: (p) => p === "/api/v1/events/stream" },
|
|
27
28
|
{
|
|
28
29
|
method: "GET",
|
|
29
|
-
test: (p) => /^\/api\/v1\/
|
|
30
|
+
test: (p) => /^\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/events\/stream$/.test(p),
|
|
30
31
|
},
|
|
31
32
|
// Image GETs — authenticated via HMAC sig+exp query string (an <img>
|
|
32
33
|
// tag cannot send Authorization headers). The image route handler does
|
|
33
34
|
// its own verification before serving bytes.
|
|
34
35
|
{
|
|
35
36
|
method: "GET",
|
|
36
|
-
test: (p) => /^\/api\/v1\/
|
|
37
|
+
test: (p) => /^\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+$/.test(p),
|
|
37
38
|
},
|
|
39
|
+
// File viewer content — read-only bytes for one confirmed path,
|
|
40
|
+
// authenticated via HMAC sig+exp query string for the same reason as
|
|
41
|
+
// image GETs above (media tags / downloads cannot send Authorization
|
|
42
|
+
// headers). The content handler verifies the signature before serving:
|
|
43
|
+
// without a valid sig+exp it returns 401, never bytes.
|
|
44
|
+
{ method: "GET", test: (p) => p === "/api/v1/files/content" },
|
|
38
45
|
// --- Share viewer (public read-only snapshots) ---
|
|
39
46
|
// Viewer HTML shell + image proxy + viewer-namespaced static assets
|
|
40
47
|
// (CSS/JS) all live under /s/* — see src/share/routes.ts. The auth gate
|
package/lib/auth.js
CHANGED
|
@@ -75,12 +75,12 @@ export function verifyAttachmentSig(path, expRaw, sigHex, secret) {
|
|
|
75
75
|
return timingSafeEqual(actual, expected);
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* Rewrite every `/api/v1/
|
|
78
|
+
* Rewrite every `/api/v1/tasks/:id/attachments/:file` URL inside a JSON-serialized
|
|
79
79
|
* payload to carry a fresh `?exp=&sig=`. Applied at egress (history GET, SSE
|
|
80
80
|
* push) so a 1h-old stored URL is re-signed on the way out — the user can
|
|
81
81
|
* reload history days later and images still resolve.
|
|
82
82
|
*/
|
|
83
|
-
const ATTACHMENT_URL_RE = /\/api\/v1\/
|
|
83
|
+
const ATTACHMENT_URL_RE = /\/api\/v1\/tasks\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+(?:\?(?:exp=\d+&sig=[a-f0-9]+|sig=[a-f0-9]+&exp=\d+))?/g;
|
|
84
84
|
export function reSignAttachmentUrlsInJson(json, secret, ttlSeconds = 3600) {
|
|
85
85
|
return json.replace(ATTACHMENT_URL_RE, (match) => {
|
|
86
86
|
const basePath = match.split("?")[0];
|