@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.
Files changed (56) hide show
  1. package/README.md +43 -15
  2. package/config.toml +7 -27
  3. package/dist/index.html +21 -5
  4. package/dist/js/app.INIQQEGD.js +5 -0
  5. package/dist/js/chunk.3CLGCUHW.js +1 -0
  6. package/dist/js/{chunk.CT5WBNGZ.js → chunk.7WADDFJZ.js} +50 -49
  7. package/dist/js/chunk.AOTG3PL7.js +20 -0
  8. package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
  9. package/dist/js/viewer.RHZMFYWJ.js +1 -0
  10. package/dist/login.html +2 -2
  11. package/dist/share-viewer.html +6 -6
  12. package/dist/{styles.00etlpgs.css → styles.01aj0l37.css} +186 -4
  13. package/dist/sw.js +6 -6
  14. package/lib/agent-key.js +6 -0
  15. package/lib/attachment-dispatch.js +60 -31
  16. package/lib/attachment-interceptor.js +7 -7
  17. package/lib/attachment-labels.js +1 -1
  18. package/lib/attachments.js +69 -7
  19. package/lib/auth-middleware.js +11 -4
  20. package/lib/auth.js +2 -2
  21. package/lib/bridge.js +209 -90
  22. package/lib/client-registry.js +12 -12
  23. package/lib/config.js +2 -31
  24. package/lib/event-handler.js +166 -85
  25. package/lib/files/limits.js +15 -0
  26. package/lib/files/paths.js +155 -0
  27. package/lib/files/routes.js +232 -0
  28. package/lib/home-path.js +35 -0
  29. package/lib/http-status.js +1 -0
  30. package/lib/mcp/capability.js +74 -0
  31. package/lib/mcp/server.js +148 -0
  32. package/lib/mcp/task-history.js +245 -0
  33. package/lib/mcp/task-host.js +253 -0
  34. package/lib/mcp/tools.js +168 -0
  35. package/lib/mode-bucket.js +1 -1
  36. package/lib/push-service.js +33 -35
  37. package/lib/routes.js +1022 -475
  38. package/lib/server.js +84 -34
  39. package/lib/share/routes.js +97 -85
  40. package/lib/shared/task-reference.js +20 -0
  41. package/lib/sse-manager.js +8 -8
  42. package/lib/store.js +992 -284
  43. package/lib/task-collaboration.js +15 -0
  44. package/lib/task-manager.js +1409 -0
  45. package/lib/task-path.js +131 -0
  46. package/lib/{session-state.js → task-state.js} +90 -38
  47. package/lib/task-tree-lock.js +74 -0
  48. package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
  49. package/lib/tokens.js +1 -1
  50. package/lib/types.js +2 -2
  51. package/package.json +8 -1
  52. package/dist/js/app.XBFXH37R.js +0 -2
  53. package/dist/js/chunk.UMQMOGWO.js +0 -1
  54. package/dist/js/viewer.CVWXSKJM.js +0 -1
  55. package/lib/session-manager.js +0 -613
  56. package/lib/title-service.js +0 -95
@@ -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
- try {
74
- const decoder = new TextDecoder("utf-8", { fatal: true });
75
- decoder.decode(buf);
76
- return true;
77
- }
78
- catch {
79
- return false;
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 —
@@ -1,5 +1,6 @@
1
- // All whitelisted paths must be GETs that read non-sensitive data, or static
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\/sessions\/[A-Za-z0-9_-]+\/events\/stream$/.test(p),
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\/sessions\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+$/.test(p),
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/sessions/:id/attachments/:file` URL inside a JSON-serialized
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\/sessions\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9._-]+(?:\?(?:exp=\d+&sig=[a-f0-9]+|sig=[a-f0-9]+&exp=\d+))?/g;
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];