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.
- package/README.md +1 -1
- package/client/assets/{JsonEditor-CU5Q6RZZ.js → JsonEditor-ZvNJuqs_.js} +1 -1
- package/client/assets/{index-hiEYYDcg.js → index-CkmW0yWc.js} +396 -396
- package/client/assets/{marp-WnOHAJUy.js → marp-OXXg-yyV.js} +1 -1
- package/client/assets/vue-i18n-BbRMUmOW.js +3 -0
- package/client/index.html +2 -2
- package/package.json +11 -11
- package/server/agent/index.ts +5 -3
- package/server/agent/prompt.ts +6 -3
- package/server/agent/promptWorkspacePaths.ts +12 -0
- package/server/api/auth/bearerAuth.ts +10 -24
- package/server/api/auth/viewToken.ts +5 -12
- package/server/api/routes/files.ts +34 -17
- package/server/build/mcp-server.mjs +604 -607
- package/server/events/pub-sub/handshakeAuth.ts +9 -0
- package/server/events/pub-sub/index.ts +47 -14
- package/server/index.ts +8 -23
- package/server/plugins/dev-watcher.ts +1 -1
- package/server/prompts/system/system.md +1 -2
- package/server/utils/launcher/preflight.d.mts +8 -5
- package/server/utils/launcher/preflight.mjs +34 -8
- package/server/workspace/journal/index.ts +3 -0
- package/server/workspace/journal/memoryExtractor.ts +6 -0
- package/server/workspace/memory/legacy-fold.ts +34 -0
- package/server/workspace/memory/legacy-migration-gate.ts +27 -0
- package/server/workspace/memory/llm-classifier.ts +4 -22
- package/server/workspace/memory/migrate.ts +52 -21
- package/server/workspace/memory/run.ts +32 -28
- package/server/workspace/memory/seed-topic-layout-run.ts +50 -0
- package/server/workspace/memory/seed-topic-layout.ts +34 -0
- package/server/workspace/memory/startup.ts +29 -0
- package/server/workspace/memory/topic-migrate.ts +78 -9
- package/server/workspace/memory/topic-run.ts +14 -9
- package/server/workspace/memory/topic-swap.ts +4 -6
- package/server/workspace/workspace.ts +4 -7
- package/src/App.vue +5 -0
- package/src/components/AuthTokenStaleBanner.vue +27 -0
- package/src/components/BackendOfflineBanner.vue +13 -24
- package/src/components/RemoteHostOfflineBanner.vue +12 -21
- package/src/components/StatusBanner.vue +65 -0
- package/src/composables/usePubSub.ts +32 -0
- package/src/config/systemFileDescriptors.ts +1 -1
- package/src/lang/de.ts +10 -4
- package/src/lang/en.ts +10 -4
- package/src/lang/es.ts +10 -4
- package/src/lang/fr.ts +10 -4
- package/src/lang/ja.ts +11 -4
- package/src/lang/ko.ts +11 -4
- package/src/lang/pt-BR.ts +10 -4
- package/src/lang/zh.ts +11 -4
- package/src/main.ts +1 -1
- package/src/utils/api.ts +45 -22
- package/src/utils/auth/authToken.ts +14 -0
- package/src/utils/auth/authTokenHtml.ts +19 -0
- package/src/utils/auth/authTokenRecovery.ts +73 -0
- package/src/utils/dom/authTokenMeta.ts +3 -1
- package/client/assets/vue-i18n-CdSW1CeF.js +0 -3
- package/server/system/logs/aaa +0 -737
- package/server/system/logs/bb +0 -446
- 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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
1258
|
-
// (no sandbox CSP) because
|
|
1259
|
-
//
|
|
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
|
|