mulmoclaude 2.3.0 → 2.4.1

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 (60) hide show
  1. package/README.md +1 -1
  2. package/client/assets/{JsonEditor-CU5Q6RZZ.js → JsonEditor-ZvNJuqs_.js} +1 -1
  3. package/client/assets/{index-hiEYYDcg.js → index-CkmW0yWc.js} +396 -396
  4. package/client/assets/{marp-WnOHAJUy.js → marp-OXXg-yyV.js} +1 -1
  5. package/client/assets/vue-i18n-BbRMUmOW.js +3 -0
  6. package/client/index.html +2 -2
  7. package/package.json +11 -11
  8. package/server/agent/index.ts +5 -3
  9. package/server/agent/prompt.ts +6 -3
  10. package/server/agent/promptWorkspacePaths.ts +12 -0
  11. package/server/api/auth/bearerAuth.ts +10 -24
  12. package/server/api/auth/viewToken.ts +5 -12
  13. package/server/api/routes/files.ts +34 -17
  14. package/server/build/mcp-server.mjs +604 -607
  15. package/server/events/pub-sub/handshakeAuth.ts +9 -0
  16. package/server/events/pub-sub/index.ts +47 -14
  17. package/server/index.ts +8 -23
  18. package/server/plugins/dev-watcher.ts +1 -1
  19. package/server/prompts/system/system.md +1 -2
  20. package/server/utils/launcher/preflight.d.mts +8 -5
  21. package/server/utils/launcher/preflight.mjs +34 -8
  22. package/server/workspace/journal/index.ts +3 -0
  23. package/server/workspace/journal/memoryExtractor.ts +6 -0
  24. package/server/workspace/memory/legacy-fold.ts +34 -0
  25. package/server/workspace/memory/legacy-migration-gate.ts +27 -0
  26. package/server/workspace/memory/llm-classifier.ts +4 -22
  27. package/server/workspace/memory/migrate.ts +52 -21
  28. package/server/workspace/memory/run.ts +32 -28
  29. package/server/workspace/memory/seed-topic-layout-run.ts +50 -0
  30. package/server/workspace/memory/seed-topic-layout.ts +34 -0
  31. package/server/workspace/memory/startup.ts +29 -0
  32. package/server/workspace/memory/topic-migrate.ts +78 -9
  33. package/server/workspace/memory/topic-run.ts +14 -9
  34. package/server/workspace/memory/topic-swap.ts +4 -6
  35. package/server/workspace/workspace.ts +4 -7
  36. package/src/App.vue +5 -0
  37. package/src/components/AuthTokenStaleBanner.vue +27 -0
  38. package/src/components/BackendOfflineBanner.vue +13 -24
  39. package/src/components/RemoteHostOfflineBanner.vue +12 -21
  40. package/src/components/StatusBanner.vue +65 -0
  41. package/src/composables/usePubSub.ts +32 -0
  42. package/src/config/systemFileDescriptors.ts +1 -1
  43. package/src/lang/de.ts +10 -4
  44. package/src/lang/en.ts +10 -4
  45. package/src/lang/es.ts +10 -4
  46. package/src/lang/fr.ts +10 -4
  47. package/src/lang/ja.ts +11 -4
  48. package/src/lang/ko.ts +11 -4
  49. package/src/lang/pt-BR.ts +10 -4
  50. package/src/lang/zh.ts +11 -4
  51. package/src/main.ts +1 -1
  52. package/src/utils/api.ts +45 -22
  53. package/src/utils/auth/authToken.ts +14 -0
  54. package/src/utils/auth/authTokenHtml.ts +19 -0
  55. package/src/utils/auth/authTokenRecovery.ts +73 -0
  56. package/src/utils/dom/authTokenMeta.ts +3 -1
  57. package/client/assets/vue-i18n-CdSW1CeF.js +0 -3
  58. package/server/system/logs/aaa +0 -737
  59. package/server/system/logs/bb +0 -446
  60. package/server/workspace/billing-migration.ts +0 -69
@@ -20,9 +20,10 @@
20
20
  // sends `Origin: null`), so the unguessable scoped token is what stands in
21
21
  // for both. See the exemptions in `server/index.ts`.
22
22
 
23
- import { createHmac, timingSafeEqual } from "node:crypto";
23
+ import { createHmac } from "node:crypto";
24
24
  import type { Request, Response, NextFunction } from "express";
25
25
  import { getCurrentToken } from "./token.js";
26
+ import { tokenEquals, UNAUTHORIZED_MESSAGE } from "@mulmoclaude/common/token-guard";
26
27
  import { unauthorized } from "../../utils/httpError.js";
27
28
  import { ONE_HOUR_MS } from "../../utils/time.js";
28
29
  import { isRecord, isUnknownArray } from "../../utils/types.js";
@@ -86,15 +87,7 @@ export function verifyViewToken(token: string, nowMs: number = Date.now()): View
86
87
  const payloadB64 = token.slice(0, dot);
87
88
  const providedSig = token.slice(dot + 1);
88
89
  const expectedSig = signPayload(payloadB64, key);
89
- // Compare BYTE lengths (not string lengths) before timingSafeEqual — it
90
- // throws a RangeError on a buffer-length mismatch, and a malformed signature
91
- // with the same character count but multi-byte chars would otherwise crash
92
- // the request (500) instead of failing closed. The lengths are non-secret
93
- // (fixed-size HMAC), so the early-out leaks nothing useful.
94
- const providedBuf = Buffer.from(providedSig);
95
- const expectedBuf = Buffer.from(expectedSig);
96
- if (providedBuf.length !== expectedBuf.length) return null;
97
- if (!timingSafeEqual(providedBuf, expectedBuf)) return null;
90
+ if (!tokenEquals(providedSig, expectedSig)) return null;
98
91
  const payload = decodePayload(payloadB64);
99
92
  if (payload === null || nowMs >= payload.exp) return null;
100
93
  return payload;
@@ -129,12 +122,12 @@ export function requireViewToken(action: ViewCapability) {
129
122
  return function requireViewTokenMiddleware(req: Request, res: Response, next: NextFunction): void {
130
123
  const header = req.headers.authorization;
131
124
  if (typeof header !== "string" || !header.startsWith(BEARER_PREFIX)) {
132
- unauthorized(res, "unauthorized");
125
+ unauthorized(res, UNAUTHORIZED_MESSAGE);
133
126
  return;
134
127
  }
135
128
  const payload = verifyViewToken(header.slice(BEARER_PREFIX.length));
136
129
  if (!payload || payload.slug !== req.params.slug || !payload.caps.includes(action)) {
137
- unauthorized(res, "unauthorized");
130
+ unauthorized(res, UNAUTHORIZED_MESSAGE);
138
131
  return;
139
132
  }
140
133
  next();
@@ -219,7 +219,9 @@ export function isWritableTextFile(absPath: string): boolean {
219
219
  return !BLOCKED_UPLOAD_EXTENSIONS.has(path.extname(absPath).toLowerCase());
220
220
  }
221
221
 
222
- const MIME_BY_EXT: Record<string, string> = {
222
+ // Exported so the header tests can prove every MIME this table emits is
223
+ // classified as sandboxed or exempt; a new entry fails them until placed.
224
+ export const MIME_BY_EXT: Record<string, string> = {
223
225
  ".png": "image/png",
224
226
  ".jpg": "image/jpeg",
225
227
  ".jpeg": "image/jpeg",
@@ -453,23 +455,38 @@ export const RAW_SECURITY_HEADERS: Readonly<Record<string, string>> = {
453
455
  "X-Content-Type-Options": "nosniff",
454
456
  };
455
457
 
456
- // PDF responses skip `Content-Security-Policy: sandbox`. Issue
457
- // #1299: WebKit refuses to render `sandbox`-opaque PDFs and forces
458
- // a download, breaking the Files preview iframe on Safari. The
459
- // PDF viewer (PDFium on Chromium, the WebKit PDF renderer, pdf.js
460
- // on Firefox) runs embedded AcroJS inside its own sandbox; the
461
- // response-level CSP was never the layer enforcing PDF script
462
- // isolation. `nosniff` is kept so the response can't be
463
- // re-interpreted as HTML.
464
- export const RAW_SECURITY_HEADERS_PDF: Readonly<Record<string, string>> = {
458
+ // Two kinds of response skip `Content-Security-Policy: sandbox`,
459
+ // each because a browser cannot show the file under it at all:
460
+ // - PDF (#1299): WebKit refuses to render a `sandbox`-opaque PDF and
461
+ // forces a download, breaking the Files preview iframe on Safari.
462
+ // The PDF viewer (PDFium on Chromium, the WebKit PDF renderer,
463
+ // pdf.js on Firefox) runs embedded AcroJS inside its own sandbox;
464
+ // the response-level CSP was never the layer enforcing PDF script
465
+ // isolation.
466
+ // - audio / video (#3437): opened as a top-level document, Chrome's
467
+ // media page re-requests the file in CORS mode from the sandbox's
468
+ // `null` origin, the request is blocked, and the player has
469
+ // nothing to play.
470
+ // Neither can run script in the document that shows it: a media
471
+ // file is decoded, not parsed as markup. `nosniff` is kept so the
472
+ // response can't be re-interpreted as HTML.
473
+ export const RAW_SECURITY_HEADERS_NO_SANDBOX: Readonly<Record<string, string>> = {
465
474
  "X-Content-Type-Options": "nosniff",
466
475
  };
467
476
 
468
- /** Pick the header set for a given MIME. PDF is the only special
469
- * case today — every other MIME (`image/*`, `text/*`,
470
- * `application/octet-stream`, …) keeps the sandbox CSP. */
477
+ const SANDBOX_EXEMPT_MIME_PREFIXES = ["audio/", "video/"] as const;
478
+
479
+ /** True for the MIMEs a browser can only show without the sandbox
480
+ * CSP: `application/pdf` and the `audio/*` / `video/*` the route's
481
+ * MIME table emits. Every other MIME (`image/*`, `text/*`,
482
+ * `application/octet-stream`, …) must stay sandboxed — an SVG
483
+ * served without it is stored XSS against the app origin. */
484
+ export function isSandboxExemptMime(mime: string): boolean {
485
+ return mime === "application/pdf" || SANDBOX_EXEMPT_MIME_PREFIXES.some((prefix) => mime.startsWith(prefix));
486
+ }
487
+
471
488
  export function rawSecurityHeadersForMime(mime: string): Readonly<Record<string, string>> {
472
- return mime === "application/pdf" ? RAW_SECURITY_HEADERS_PDF : RAW_SECURITY_HEADERS;
489
+ return isSandboxExemptMime(mime) ? RAW_SECURITY_HEADERS_NO_SANDBOX : RAW_SECURITY_HEADERS;
473
490
  }
474
491
 
475
492
  function applyRawSecurityHeaders(res: Response, mime: string): void {
@@ -1254,9 +1271,9 @@ router.get(API_ROUTES.files.raw, (req: Request<object, unknown, unknown, PathQue
1254
1271
  res.setHeader("Content-Type", mime);
1255
1272
  // Sandbox the response so an `.svg` / `.html` with embedded
1256
1273
  // JavaScript can't escape into the localhost:3001 origin via
1257
- // direct navigation or <iframe>. PDFs get a narrower header set
1258
- // (no sandbox CSP) because Safari/WebKit refuses to render
1259
- // sandbox-opaque PDFs (#1299). See plans/done/
1274
+ // direct navigation or <iframe>. PDFs (#1299) and audio / video
1275
+ // (#3437) get a narrower header set (no sandbox CSP) because a
1276
+ // browser cannot show them under it. See plans/done/
1260
1277
  // fix-files-raw-csp-sandbox.md for the full threat model.
1261
1278
  applyRawSecurityHeaders(res, mime);
1262
1279