@1agh/maude 0.55.0 → 0.57.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/apps/studio/acp/bridge.ts +385 -26
- package/apps/studio/acp/index.ts +498 -102
- package/apps/studio/acp/running.ts +97 -0
- package/apps/studio/acp/transcript.ts +64 -0
- package/apps/studio/acp/write-scope.ts +459 -0
- package/apps/studio/bin/_smart-frames.mjs +187 -20
- package/apps/studio/bin/_smart-frames.test.mjs +59 -4
- package/apps/studio/bin/_transcribe.mjs +40 -3
- package/apps/studio/bin/smoke.sh +7 -1
- package/apps/studio/build.ts +28 -1
- package/apps/studio/client/app.jsx +78 -33
- package/apps/studio/client/panels/ChatPanel.jsx +19 -1
- package/apps/studio/client/panels/CloudBar.jsx +104 -6
- package/apps/studio/client/panels/GitPanel.jsx +38 -25
- package/apps/studio/client/panels/PermissionPrompt.jsx +146 -6
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +145 -6
- package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
- package/apps/studio/client/panels/acp-runtime.js +96 -5
- package/apps/studio/client/styles/3-shell-maude.css +11 -2
- package/apps/studio/client/styles/4-components.css +72 -0
- package/apps/studio/client/styles/6-acp-chat.css +51 -0
- package/apps/studio/cloud/endpoints.ts +56 -3
- package/apps/studio/collab/persistence.ts +29 -2
- package/apps/studio/config.schema.json +3 -3
- package/apps/studio/context.ts +41 -0
- package/apps/studio/dist/client.bundle.js +1330 -1330
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/generation/gemma-models.ts +312 -14
- package/apps/studio/generation/prefs.ts +7 -2
- package/apps/studio/generation/runtime-probe.ts +50 -0
- package/apps/studio/generation/whisper-models.ts +124 -0
- package/apps/studio/hmr-broadcast.ts +67 -0
- package/apps/studio/http.ts +247 -111
- package/apps/studio/input-router.tsx +55 -2
- package/apps/studio/server.ts +22 -9
- package/apps/studio/sync/autocommit.ts +61 -2
- package/apps/studio/sync/cell-pairing.ts +174 -0
- package/apps/studio/sync/codec.ts +11 -5
- package/apps/studio/sync/connection-state.ts +116 -6
- package/apps/studio/sync/index.ts +288 -26
- package/apps/studio/sync/limits.ts +49 -0
- package/apps/studio/sync/loopback.ts +21 -0
- package/apps/studio/sync/presentation.ts +273 -0
- package/apps/studio/sync/projection.ts +47 -12
- package/apps/studio/sync/remote-docs.ts +191 -0
- package/apps/studio/sync/status.ts +4 -1
- package/apps/studio/sync/supervisor.ts +178 -0
- package/apps/studio/test/acp-activity-endpoint.test.ts +183 -0
- package/apps/studio/test/acp-branch-guard.test.ts +123 -0
- package/apps/studio/test/acp-bridge-lifetime.test.ts +369 -0
- package/apps/studio/test/acp-caps-bridge.test.ts +5 -0
- package/apps/studio/test/acp-commands.test.ts +5 -0
- package/apps/studio/test/acp-elicitation-bridge.test.ts +5 -0
- package/apps/studio/test/acp-permission-prompt.test.ts +175 -1
- package/apps/studio/test/acp-permission.test.ts +18 -2
- package/apps/studio/test/acp-session-allowed-tools.test.ts +62 -14
- package/apps/studio/test/acp-usage-bridge.test.ts +5 -0
- package/apps/studio/test/acp-write-gate.test.ts +323 -0
- package/apps/studio/test/acp-write-scope.test.ts +533 -0
- package/apps/studio/test/bundle-smoke.test.ts +35 -18
- package/apps/studio/test/canvas-origin-gate.test.ts +7 -0
- package/apps/studio/test/cloud-connect-note.test.ts +135 -0
- package/apps/studio/test/cloud-endpoints.test.ts +102 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +5 -2
- package/apps/studio/test/csrf-write-guard.test.ts +19 -2
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +45 -0
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +118 -0
- package/apps/studio/test/gemma-models.test.ts +245 -0
- package/apps/studio/test/hmr-broadcast.test.ts +57 -1
- package/apps/studio/test/input-router.test.ts +95 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +639 -0
- package/apps/studio/test/sync-autocommit.test.ts +47 -0
- package/apps/studio/test/sync-connect-honesty.test.ts +114 -0
- package/apps/studio/test/sync-connection-state.test.ts +45 -1
- package/apps/studio/test/sync-presentation.test.ts +185 -0
- package/apps/studio/test/sync-remote-docs.test.ts +171 -0
- package/apps/studio/test/sync-supervisor.test.ts +212 -0
- package/apps/studio/test/trusted-request-host.test.ts +66 -0
- package/apps/studio/test/whisper-setup.test.ts +97 -0
- package/apps/studio/whats-new.json +45 -0
- package/apps/studio/ws.ts +9 -1
- package/cli/commands/kg.mjs +9 -2
- package/package.json +8 -8
- package/plugins/design/dependencies.json +21 -3
package/apps/studio/http.ts
CHANGED
|
@@ -17,7 +17,14 @@ import {
|
|
|
17
17
|
startSignin,
|
|
18
18
|
} from './acp/login-state.ts';
|
|
19
19
|
import { probeAcpAvailabilityAuthed } from './acp/probe.ts';
|
|
20
|
-
import {
|
|
20
|
+
import { activitySnapshot, runningChats } from './acp/running.ts';
|
|
21
|
+
import {
|
|
22
|
+
chatTranscriptSeq,
|
|
23
|
+
deleteChat,
|
|
24
|
+
listChats,
|
|
25
|
+
readChatMessages,
|
|
26
|
+
writeChatMeta,
|
|
27
|
+
} from './acp/transcript.ts';
|
|
21
28
|
import { type Api, ASSET_MAX_BYTES, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
22
29
|
import { ImportAssetError, importSvg, SVG_MAX_BYTES } from './bin/_import-asset.mjs';
|
|
23
30
|
import { ImportBrandError, importBrand } from './bin/_import-brand.mjs';
|
|
@@ -46,8 +53,15 @@ import { localizeGenAsset } from './generation/download.ts';
|
|
|
46
53
|
import {
|
|
47
54
|
downloadGemmaModel,
|
|
48
55
|
ffmpegAvailable,
|
|
56
|
+
getOllamaModel,
|
|
49
57
|
listGemmaModels,
|
|
58
|
+
listScoutModels,
|
|
59
|
+
mlxSetup,
|
|
50
60
|
mlxVlmAvailable,
|
|
61
|
+
OLLAMA_RECOMMENDED_MODEL,
|
|
62
|
+
ollamaSetupOptions,
|
|
63
|
+
ollamaStatus,
|
|
64
|
+
pullOllamaModel,
|
|
51
65
|
} from './generation/gemma-models.ts';
|
|
52
66
|
import { type GenerationJobQueue, GenerationQueueFullError } from './generation/jobs.ts';
|
|
53
67
|
import {
|
|
@@ -77,6 +91,8 @@ import {
|
|
|
77
91
|
getWhisperModel,
|
|
78
92
|
listWhisperModels,
|
|
79
93
|
removeWhisperModel,
|
|
94
|
+
resolveAutoEngine,
|
|
95
|
+
whisperSetup,
|
|
80
96
|
} from './generation/whisper-models.ts';
|
|
81
97
|
import { createGitEndpoints } from './git/endpoints.ts';
|
|
82
98
|
import { gitShowFile } from './git/service.ts';
|
|
@@ -289,12 +305,53 @@ export function sameOriginWrite(req: Request): boolean {
|
|
|
289
305
|
const origin = req.headers.get('origin');
|
|
290
306
|
if (!origin) return true; // non-browser / same-origin omitting Origin → allow
|
|
291
307
|
try {
|
|
292
|
-
|
|
308
|
+
// HOSTS, not full origins — the same reason isOwnShellOrigin (studio-proxy)
|
|
309
|
+
// compares host-only for the collab WS upgrade. In a cell, TLS terminates at
|
|
310
|
+
// the proxy, so the browser's `https://<host>` Origin reaches a studio that
|
|
311
|
+
// serves plaintext and computes `req.url` as `http://<host>`; a full-origin
|
|
312
|
+
// compare then rejects every legitimate shell write on the SCHEME alone
|
|
313
|
+
// (403 "cross-origin write rejected" — the second half of the inspector-edits
|
|
314
|
+
// RCA, found when the loopback fix uncovered it). Host+port still pins the
|
|
315
|
+
// origin: the port rides in `host`, and scheme was never the CSRF
|
|
316
|
+
// discriminator here — locally everything is http, and a real cross-site page
|
|
317
|
+
// has a different host.
|
|
318
|
+
return new URL(origin).host === new URL(req.url).host;
|
|
293
319
|
} catch {
|
|
294
320
|
return false;
|
|
295
321
|
}
|
|
296
322
|
}
|
|
297
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The DNS-rebind guard, mode-aware — Cloud Phase 27, the inspector-edits RCA
|
|
326
|
+
* (`issue-cloud-inspector-edits-refused-by-loopback-guard`).
|
|
327
|
+
*
|
|
328
|
+
* `isLoopbackHost(req.headers.get('host'))` is right on a laptop: a malicious
|
|
329
|
+
* page that resolves `evil.com` → 127.0.0.1 makes the browser send a FOREIGN
|
|
330
|
+
* Host to the locally-bound dev server, and the guard rejects it. In a CELL that
|
|
331
|
+
* same check can NEVER pass — the hub's authenticating proxy rewrites Host to the
|
|
332
|
+
* project's public name (D4, `upstreamHeaders`) on purpose — so every shell edit
|
|
333
|
+
* (Inspector CSS, artboard ops, insert/delete/reorder, edit-attr/text) 403'd
|
|
334
|
+
* with "local request required (DNS-rebinding guard)". Annotations and comments
|
|
335
|
+
* were spared only because their handlers carry no such guard; that split of
|
|
336
|
+
* which routes worked was exactly the fingerprint of this bug.
|
|
337
|
+
*
|
|
338
|
+
* The cell doesn't NEED the Host check: the proxy already terminated the session,
|
|
339
|
+
* authorized it against the one role table (studio-manifest's `decide()`), and
|
|
340
|
+
* injects `x-maude-role` after stripping every inbound `x-maude-*` — so the
|
|
341
|
+
* header's PRESENCE is unforgeable proof the request came through the hub. Same
|
|
342
|
+
* `WORKSPACE ? x-maude-role` vouch the collab-WS upgrade already trusts
|
|
343
|
+
* (server.ts, RCA issue-cloud-live-collaboration-dead). Outside workspace mode
|
|
344
|
+
* this is the loopback check, verbatim.
|
|
345
|
+
*
|
|
346
|
+
* Safe against secret leakage by construction: a cell PRUNES every secret-bearing
|
|
347
|
+
* route family out of its table (`pruneForWorkspace` + the boot-assert), so the
|
|
348
|
+
* vouched path can only ever reach the project-editing surfaces that survive.
|
|
349
|
+
*/
|
|
350
|
+
export function isTrustedRequestHost(req: Request): boolean {
|
|
351
|
+
if (isLoopbackHost(req.headers.get('host'))) return true;
|
|
352
|
+
return isWorkspaceMode() && !!req.headers.get('x-maude-role');
|
|
353
|
+
}
|
|
354
|
+
|
|
298
355
|
/**
|
|
299
356
|
* CSRF guard for a key-bearing / side-effecting GET (Task 2.5 audio-search —
|
|
300
357
|
* ethical-hacker F1). A cross-site page can issue a `no-cors` GET to
|
|
@@ -1137,7 +1194,7 @@ export function createHttp(
|
|
|
1137
1194
|
// but newly worth closing given what this diff makes it able to reveal.
|
|
1138
1195
|
'/_api/acp/status': async (req: Request) => {
|
|
1139
1196
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1140
|
-
if (!
|
|
1197
|
+
if (!isTrustedRequestHost(req))
|
|
1141
1198
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1142
1199
|
return Response.json(await probeAcpAvailabilityAuthed(), {
|
|
1143
1200
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1155,7 +1212,7 @@ export function createHttp(
|
|
|
1155
1212
|
// ChatPanel not-connected explainer + the Help-menu modal read this.
|
|
1156
1213
|
'/_api/preflight': async (req: Request) => {
|
|
1157
1214
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1158
|
-
if (!
|
|
1215
|
+
if (!isTrustedRequestHost(req))
|
|
1159
1216
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1160
1217
|
return Response.json(await probeReadiness(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1161
1218
|
},
|
|
@@ -1169,7 +1226,7 @@ export function createHttp(
|
|
|
1169
1226
|
// (SetupChecklist.jsx) polls this.
|
|
1170
1227
|
'/_api/setup-readiness': async (req: Request) => {
|
|
1171
1228
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1172
|
-
if (!
|
|
1229
|
+
if (!isTrustedRequestHost(req))
|
|
1173
1230
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1174
1231
|
return Response.json(await probeSetupReadiness(ctx), {
|
|
1175
1232
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1185,7 +1242,7 @@ export function createHttp(
|
|
|
1185
1242
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1186
1243
|
if (!sameOriginWrite(req))
|
|
1187
1244
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1188
|
-
if (!
|
|
1245
|
+
if (!isTrustedRequestHost(req))
|
|
1189
1246
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1190
1247
|
// Fire-and-forget — the install can run past Bun's default 10s idle
|
|
1191
1248
|
// timeout on a slow network, so this returns as soon as the spawn is
|
|
@@ -1206,7 +1263,7 @@ export function createHttp(
|
|
|
1206
1263
|
// two routes, which used the wrong guard.
|
|
1207
1264
|
'/_api/claude/install-status': (req: Request) => {
|
|
1208
1265
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1209
|
-
if (!
|
|
1266
|
+
if (!isTrustedRequestHost(req))
|
|
1210
1267
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1211
1268
|
return Response.json(getInstallState(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1212
1269
|
},
|
|
@@ -1223,7 +1280,7 @@ export function createHttp(
|
|
|
1223
1280
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1224
1281
|
if (!sameOriginWrite(req))
|
|
1225
1282
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1226
|
-
if (!
|
|
1283
|
+
if (!isTrustedRequestHost(req))
|
|
1227
1284
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1228
1285
|
const result = startSignin();
|
|
1229
1286
|
return Response.json(result, {
|
|
@@ -1235,7 +1292,7 @@ export function createHttp(
|
|
|
1235
1292
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1236
1293
|
if (!sameOriginWrite(req))
|
|
1237
1294
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1238
|
-
if (!
|
|
1295
|
+
if (!isTrustedRequestHost(req))
|
|
1239
1296
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1240
1297
|
cancelSignin();
|
|
1241
1298
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -1245,7 +1302,7 @@ export function createHttp(
|
|
|
1245
1302
|
// Narrowed fields only (never email/orgId/orgName — see login-state.ts).
|
|
1246
1303
|
'/_api/claude/signin-status': async (req: Request) => {
|
|
1247
1304
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1248
|
-
if (!
|
|
1305
|
+
if (!isTrustedRequestHost(req))
|
|
1249
1306
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1250
1307
|
const status = await getClaudeAuthStatus();
|
|
1251
1308
|
return Response.json(
|
|
@@ -1263,12 +1320,46 @@ export function createHttp(
|
|
|
1263
1320
|
// chat-open` driver omits Origin (allowed); a browser drive-by can't forge it.
|
|
1264
1321
|
if (!sameOriginWrite(req))
|
|
1265
1322
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1266
|
-
if (!
|
|
1323
|
+
if (!isTrustedRequestHost(req))
|
|
1267
1324
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1268
1325
|
ctx.bus.emit('acp-focus', {});
|
|
1269
1326
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
1270
1327
|
},
|
|
1271
1328
|
|
|
1329
|
+
// Addendum Task 9 — "is a chat mid-turn right now?", asked by
|
|
1330
|
+
// RepoBranchSwitcher.jsx before any of its three `window.location.reload()`
|
|
1331
|
+
// call sites. A `git checkout` moves the worktree under a running agent
|
|
1332
|
+
// (read on `draft-a`, written back after the checkout to `main` — a silent
|
|
1333
|
+
// cross-branch clobber that `_history/`, being per-canvas-slug with no
|
|
1334
|
+
// branch awareness, cannot undo), so this gates a confirm rather than
|
|
1335
|
+
// reloading silently. MAIN-ORIGIN ONLY, read-only, no chat CONTENT — just
|
|
1336
|
+
// how many are busy, so it says nothing the canvas origin could want.
|
|
1337
|
+
'/_api/acp/running': (req: Request) => {
|
|
1338
|
+
if (!isTrustedRequestHost(req))
|
|
1339
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1340
|
+
const running = runningChats();
|
|
1341
|
+
return Response.json(
|
|
1342
|
+
{ running: running.length, chats: running },
|
|
1343
|
+
{ headers: { 'Cache-Control': 'no-store' } }
|
|
1344
|
+
);
|
|
1345
|
+
},
|
|
1346
|
+
|
|
1347
|
+
// feature-acp-turn-notifications Task 3 — the native shell's poller reads
|
|
1348
|
+
// this to decide whether to fire an OS notification for a NON-visible
|
|
1349
|
+
// project (Decision C). A sibling of `/_api/acp/running` above rather than
|
|
1350
|
+
// an extension of it — that route stays exactly as-is because the reaper's
|
|
1351
|
+
// `has_running_chat` (sidecar.rs) already depends on its `{running,chats}`
|
|
1352
|
+
// shape and has no reason to grow one. MAIN-ORIGIN ONLY, same gate, and —
|
|
1353
|
+
// load-bearing (Decision D) — NEVER a chat title, message text, or any
|
|
1354
|
+
// transcript content: just chat ids (opaque, already exposed by `running`
|
|
1355
|
+
// above) and a three-value state. Anyone adding a field here should ask
|
|
1356
|
+
// what it would look like rendered on a lock screen.
|
|
1357
|
+
'/_api/acp/activity': (req: Request) => {
|
|
1358
|
+
if (!isTrustedRequestHost(req))
|
|
1359
|
+
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1360
|
+
return Response.json(activitySnapshot(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1361
|
+
},
|
|
1362
|
+
|
|
1272
1363
|
// Phase 31 — repo-level chat list + history (for the chat switcher +
|
|
1273
1364
|
// hydration). MAIN-ORIGIN ONLY. Read-only; ids are sanitized before disk.
|
|
1274
1365
|
'/_api/acp/chats': () =>
|
|
@@ -1289,7 +1380,7 @@ export function createHttp(
|
|
|
1289
1380
|
if (req.method === 'PATCH') {
|
|
1290
1381
|
if (!sameOriginWrite(req))
|
|
1291
1382
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1292
|
-
if (!
|
|
1383
|
+
if (!isTrustedRequestHost(req))
|
|
1293
1384
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1294
1385
|
if (!id) return new Response('missing id', { status: 400 });
|
|
1295
1386
|
let body: unknown;
|
|
@@ -1315,8 +1406,16 @@ export function createHttp(
|
|
|
1315
1406
|
return Response.json(meta, { headers: { 'Cache-Control': 'no-store' } });
|
|
1316
1407
|
}
|
|
1317
1408
|
if (!id) return Response.json([], { headers: { 'Cache-Control': 'no-store' } });
|
|
1409
|
+
// Addendum Task 8 — the re-attach seam. Read the sequence marker BEFORE
|
|
1410
|
+
// the messages: a line appended between the two reads then shows up in
|
|
1411
|
+
// neither, and the client's `attach` will replay it. Reading it after
|
|
1412
|
+
// would risk the opposite (a line counted but not returned), which the
|
|
1413
|
+
// client would silently drop as "already hydrated" — a hole in the feed.
|
|
1414
|
+
// Shipped as a header rather than a body field so the response SHAPE
|
|
1415
|
+
// stays the plain message array every existing consumer expects.
|
|
1416
|
+
const seq = chatTranscriptSeq(ctx.paths.designRoot, id);
|
|
1318
1417
|
return Response.json(readChatMessages(ctx.paths.designRoot, id), {
|
|
1319
|
-
headers: { 'Cache-Control': 'no-store' },
|
|
1418
|
+
headers: { 'Cache-Control': 'no-store', 'X-Maude-Chat-Seq': String(seq) },
|
|
1320
1419
|
});
|
|
1321
1420
|
},
|
|
1322
1421
|
|
|
@@ -1350,7 +1449,7 @@ export function createHttp(
|
|
|
1350
1449
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1351
1450
|
if (!sameOriginWrite(req))
|
|
1352
1451
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1353
|
-
if (!
|
|
1452
|
+
if (!isTrustedRequestHost(req))
|
|
1354
1453
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1355
1454
|
const declared = Number(req.headers.get('content-length') || '0');
|
|
1356
1455
|
if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
|
|
@@ -1457,7 +1556,7 @@ export function createHttp(
|
|
|
1457
1556
|
// /_api/acp/status (a drive-by page must not read logs via DNS rebinding).
|
|
1458
1557
|
'/_api/debug-bundle': (req: Request) => {
|
|
1459
1558
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1460
|
-
if (!
|
|
1559
|
+
if (!isTrustedRequestHost(req))
|
|
1461
1560
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1462
1561
|
return Response.json(
|
|
1463
1562
|
buildDebugBundle({
|
|
@@ -1490,7 +1589,7 @@ export function createHttp(
|
|
|
1490
1589
|
'/_api/shell-shot': async (req: Request) => {
|
|
1491
1590
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1492
1591
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1493
|
-
if (!
|
|
1592
|
+
if (!isTrustedRequestHost(req))
|
|
1494
1593
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1495
1594
|
// Target the very server handling this request — no _server.json read, so
|
|
1496
1595
|
// a stale/absent state file can't point the capture at another instance.
|
|
@@ -1560,7 +1659,7 @@ export function createHttp(
|
|
|
1560
1659
|
'/_api/report': async (req: Request) => {
|
|
1561
1660
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1562
1661
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1563
|
-
if (!
|
|
1662
|
+
if (!isTrustedRequestHost(req))
|
|
1564
1663
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1565
1664
|
const endpoint = process.env.MAUDE_REPORT_URL || 'https://cloud.maude.sh/report';
|
|
1566
1665
|
try {
|
|
@@ -1593,7 +1692,7 @@ export function createHttp(
|
|
|
1593
1692
|
'/_api/report-fallback': async (req: Request) => {
|
|
1594
1693
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1595
1694
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1596
|
-
if (!
|
|
1695
|
+
if (!isTrustedRequestHost(req))
|
|
1597
1696
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1598
1697
|
const body = await readJson<{ report?: unknown; screenshots?: unknown }>(req);
|
|
1599
1698
|
if (!body || typeof body.report !== 'object' || body.report === null) {
|
|
@@ -1694,7 +1793,7 @@ export function createHttp(
|
|
|
1694
1793
|
// the photo-store cap stack (sha8 validate + containment + validatePhotoEdit
|
|
1695
1794
|
// + size cap) is the load-bearing mitigation. loopback-Host gates DNS-
|
|
1696
1795
|
// rebinding on every method.
|
|
1697
|
-
if (!
|
|
1796
|
+
if (!isTrustedRequestHost(req))
|
|
1698
1797
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1699
1798
|
const asset = new URL(req.url).searchParams.get('asset');
|
|
1700
1799
|
if (req.method === 'GET') {
|
|
@@ -1732,7 +1831,7 @@ export function createHttp(
|
|
|
1732
1831
|
// DNS-rebinding on every method; the footage-store cap stack (sha8/slug
|
|
1733
1832
|
// validate + containment + validate{FootageAnalysis,Edl} + size cap) is the
|
|
1734
1833
|
// load-bearing mitigation for the caller-derived write path.
|
|
1735
|
-
if (!
|
|
1834
|
+
if (!isTrustedRequestHost(req))
|
|
1736
1835
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1737
1836
|
const params = new URL(req.url).searchParams;
|
|
1738
1837
|
const asset = params.get('asset');
|
|
@@ -1775,7 +1874,7 @@ export function createHttp(
|
|
|
1775
1874
|
// a rebound page shares this origin; the dev-server always binds 127.0.0.1,
|
|
1776
1875
|
// so a real browser (Host: localhost:<port>) passes and only a rebound
|
|
1777
1876
|
// foreign hostname 403s. (No sameOriginWrite here — GET.)
|
|
1778
|
-
if (!
|
|
1877
|
+
if (!isTrustedRequestHost(req))
|
|
1779
1878
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1780
1879
|
const file = new URL(req.url).searchParams.get('file');
|
|
1781
1880
|
const r = await api.loadCanvasSource(file);
|
|
@@ -1840,7 +1939,7 @@ export function createHttp(
|
|
|
1840
1939
|
// slash-command driver omits Origin (→ allowed); a browser drive-by can't.
|
|
1841
1940
|
if (!sameOriginWrite(req))
|
|
1842
1941
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1843
|
-
if (!
|
|
1942
|
+
if (!isTrustedRequestHost(req))
|
|
1844
1943
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1845
1944
|
const body = await readJson<{ file?: string; author?: string }>(req);
|
|
1846
1945
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1860,7 +1959,7 @@ export function createHttp(
|
|
|
1860
1959
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1861
1960
|
if (!sameOriginWrite(req))
|
|
1862
1961
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1863
|
-
if (!
|
|
1962
|
+
if (!isTrustedRequestHost(req))
|
|
1864
1963
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1865
1964
|
const body = await readJson<{ file?: string }>(req);
|
|
1866
1965
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1876,7 +1975,7 @@ export function createHttp(
|
|
|
1876
1975
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1877
1976
|
if (!sameOriginWrite(req))
|
|
1878
1977
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1879
|
-
if (!
|
|
1978
|
+
if (!isTrustedRequestHost(req))
|
|
1880
1979
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1881
1980
|
const body = await readJson<{ file?: string }>(req);
|
|
1882
1981
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1937,7 +2036,7 @@ export function createHttp(
|
|
|
1937
2036
|
if (req.method === 'DELETE' || req.method === 'POST') {
|
|
1938
2037
|
if (!sameOriginWrite(req))
|
|
1939
2038
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1940
|
-
if (!
|
|
2039
|
+
if (!isTrustedRequestHost(req))
|
|
1941
2040
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1942
2041
|
}
|
|
1943
2042
|
if (req.method === 'DELETE') {
|
|
@@ -2000,7 +2099,7 @@ export function createHttp(
|
|
|
2000
2099
|
'/_api/fs-move': async (req: Request) => {
|
|
2001
2100
|
if (!sameOriginWrite(req))
|
|
2002
2101
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2003
|
-
if (!
|
|
2102
|
+
if (!isTrustedRequestHost(req))
|
|
2004
2103
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2005
2104
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2006
2105
|
const body = await readJson<{ file?: unknown; toDir?: unknown }>(req, 4 * 1024);
|
|
@@ -2031,7 +2130,7 @@ export function createHttp(
|
|
|
2031
2130
|
'/_api/fs-mkdir': async (req: Request) => {
|
|
2032
2131
|
if (!sameOriginWrite(req))
|
|
2033
2132
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2034
|
-
if (!
|
|
2133
|
+
if (!isTrustedRequestHost(req))
|
|
2035
2134
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2036
2135
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2037
2136
|
const body = await readJson<{ parent?: unknown; name?: unknown }>(req, 4 * 1024);
|
|
@@ -2092,7 +2191,7 @@ export function createHttp(
|
|
|
2092
2191
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2093
2192
|
if (!sameOriginWrite(req))
|
|
2094
2193
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2095
|
-
if (!
|
|
2194
|
+
if (!isTrustedRequestHost(req))
|
|
2096
2195
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2097
2196
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2098
2197
|
return gitJson(await gitApi.createBranch(body));
|
|
@@ -2102,7 +2201,7 @@ export function createHttp(
|
|
|
2102
2201
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2103
2202
|
if (!sameOriginWrite(req))
|
|
2104
2203
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2105
|
-
if (!
|
|
2204
|
+
if (!isTrustedRequestHost(req))
|
|
2106
2205
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2107
2206
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2108
2207
|
return gitJson(await gitApi.checkout(body));
|
|
@@ -2114,7 +2213,7 @@ export function createHttp(
|
|
|
2114
2213
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2115
2214
|
if (!sameOriginWrite(req))
|
|
2116
2215
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2117
|
-
if (!
|
|
2216
|
+
if (!isTrustedRequestHost(req))
|
|
2118
2217
|
return new Response('adding a draft requires a local request', { status: 403 });
|
|
2119
2218
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2120
2219
|
return gitJson(await gitApi.fold(body));
|
|
@@ -2126,7 +2225,7 @@ export function createHttp(
|
|
|
2126
2225
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2127
2226
|
if (!sameOriginWrite(req))
|
|
2128
2227
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2129
|
-
if (!
|
|
2228
|
+
if (!isTrustedRequestHost(req))
|
|
2130
2229
|
return new Response('refresh requires a local request', { status: 403 });
|
|
2131
2230
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2132
2231
|
return gitJson(await gitApi.fetchRemote(body));
|
|
@@ -2136,7 +2235,7 @@ export function createHttp(
|
|
|
2136
2235
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2137
2236
|
if (!sameOriginWrite(req))
|
|
2138
2237
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2139
|
-
if (!
|
|
2238
|
+
if (!isTrustedRequestHost(req))
|
|
2140
2239
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2141
2240
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2142
2241
|
return gitJson(await gitApi.commit(body));
|
|
@@ -2146,7 +2245,7 @@ export function createHttp(
|
|
|
2146
2245
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2147
2246
|
if (!sameOriginWrite(req))
|
|
2148
2247
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2149
|
-
if (!
|
|
2248
|
+
if (!isTrustedRequestHost(req))
|
|
2150
2249
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2151
2250
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2152
2251
|
return gitJson(await gitApi.discard(body));
|
|
@@ -2156,11 +2255,11 @@ export function createHttp(
|
|
|
2156
2255
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2157
2256
|
if (!sameOriginWrite(req))
|
|
2158
2257
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2159
|
-
if (!
|
|
2258
|
+
if (!isTrustedRequestHost(req))
|
|
2160
2259
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2161
2260
|
// Token-bearing: refuse anything not from a loopback Host (the server binds
|
|
2162
2261
|
// 127.0.0.1, so this is belt-and-suspenders against a forwarded/rebound Host).
|
|
2163
|
-
if (!
|
|
2262
|
+
if (!isTrustedRequestHost(req))
|
|
2164
2263
|
return new Response('publish requires a local request', { status: 403 });
|
|
2165
2264
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2166
2265
|
return gitJson(await gitApi.push(body));
|
|
@@ -2170,7 +2269,7 @@ export function createHttp(
|
|
|
2170
2269
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2171
2270
|
if (!sameOriginWrite(req))
|
|
2172
2271
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2173
|
-
if (!
|
|
2272
|
+
if (!isTrustedRequestHost(req))
|
|
2174
2273
|
return new Response('get latest requires a local request', { status: 403 });
|
|
2175
2274
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2176
2275
|
return gitJson(await gitApi.pull(body));
|
|
@@ -2184,7 +2283,7 @@ export function createHttp(
|
|
|
2184
2283
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2185
2284
|
if (!sameOriginWrite(req))
|
|
2186
2285
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2187
|
-
if (!
|
|
2286
|
+
if (!isTrustedRequestHost(req))
|
|
2188
2287
|
return new Response('resolve requires a local request', { status: 403 });
|
|
2189
2288
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2190
2289
|
return gitJson(await gitApi.resolve(body));
|
|
@@ -2205,7 +2304,7 @@ export function createHttp(
|
|
|
2205
2304
|
// whose 401 DELETES the stored credential — a confused deputy worth
|
|
2206
2305
|
// closing (validate 2026-07-30, defender F6).
|
|
2207
2306
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2208
|
-
if (!
|
|
2307
|
+
if (!isTrustedRequestHost(req))
|
|
2209
2308
|
return new Response('local request required', { status: 403 });
|
|
2210
2309
|
return gitJson(cloudApi.status());
|
|
2211
2310
|
},
|
|
@@ -2213,7 +2312,7 @@ export function createHttp(
|
|
|
2213
2312
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2214
2313
|
if (!sameOriginWrite(req))
|
|
2215
2314
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2216
|
-
if (!
|
|
2315
|
+
if (!isTrustedRequestHost(req))
|
|
2217
2316
|
return new Response('local request required', { status: 403 });
|
|
2218
2317
|
return gitJson(await cloudApi.signinStart());
|
|
2219
2318
|
},
|
|
@@ -2221,7 +2320,7 @@ export function createHttp(
|
|
|
2221
2320
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2222
2321
|
if (!sameOriginWrite(req))
|
|
2223
2322
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2224
|
-
if (!
|
|
2323
|
+
if (!isTrustedRequestHost(req))
|
|
2225
2324
|
return new Response('local request required', { status: 403 });
|
|
2226
2325
|
const body = (await req.json().catch(() => ({}))) as { deviceCode?: string };
|
|
2227
2326
|
return gitJson(await cloudApi.signinPoll(String(body.deviceCode ?? '')));
|
|
@@ -2230,14 +2329,14 @@ export function createHttp(
|
|
|
2230
2329
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2231
2330
|
if (!sameOriginWrite(req))
|
|
2232
2331
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2233
|
-
if (!
|
|
2332
|
+
if (!isTrustedRequestHost(req))
|
|
2234
2333
|
return new Response('local request required', { status: 403 });
|
|
2235
2334
|
return gitJson(cloudApi.signout());
|
|
2236
2335
|
},
|
|
2237
2336
|
'/_api/cloud/projects': async (req: Request) => {
|
|
2238
2337
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2239
2338
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2240
|
-
if (!
|
|
2339
|
+
if (!isTrustedRequestHost(req))
|
|
2241
2340
|
return new Response('local request required', { status: 403 });
|
|
2242
2341
|
return gitJson(await cloudApi.projects());
|
|
2243
2342
|
},
|
|
@@ -2245,7 +2344,7 @@ export function createHttp(
|
|
|
2245
2344
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2246
2345
|
if (!sameOriginWrite(req))
|
|
2247
2346
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2248
|
-
if (!
|
|
2347
|
+
if (!isTrustedRequestHost(req))
|
|
2249
2348
|
return new Response('local request required', { status: 403 });
|
|
2250
2349
|
const body = (await req.json().catch(() => ({}))) as { project?: string };
|
|
2251
2350
|
return gitJson(await cloudApi.attach(String(body.project ?? '')));
|
|
@@ -2257,7 +2356,7 @@ export function createHttp(
|
|
|
2257
2356
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2258
2357
|
if (!sameOriginWrite(req))
|
|
2259
2358
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2260
|
-
if (!
|
|
2359
|
+
if (!isTrustedRequestHost(req))
|
|
2261
2360
|
return new Response('local request required', { status: 403 });
|
|
2262
2361
|
const body = (await req.json().catch(() => ({}))) as { code?: string; project?: string };
|
|
2263
2362
|
return gitJson(
|
|
@@ -2270,14 +2369,14 @@ export function createHttp(
|
|
|
2270
2369
|
|
|
2271
2370
|
'/_api/github/identity': async (req: Request) => {
|
|
2272
2371
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2273
|
-
if (!
|
|
2372
|
+
if (!isTrustedRequestHost(req))
|
|
2274
2373
|
return new Response('local request required', { status: 403 });
|
|
2275
2374
|
return gitJson(await githubApi.identity());
|
|
2276
2375
|
},
|
|
2277
2376
|
|
|
2278
2377
|
'/_api/github/repos': async (req: Request) => {
|
|
2279
2378
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2280
|
-
if (!
|
|
2379
|
+
if (!isTrustedRequestHost(req))
|
|
2281
2380
|
return new Response('local request required', { status: 403 });
|
|
2282
2381
|
return gitJson(await githubApi.repos());
|
|
2283
2382
|
},
|
|
@@ -2286,7 +2385,7 @@ export function createHttp(
|
|
|
2286
2385
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2287
2386
|
if (!sameOriginWrite(req))
|
|
2288
2387
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2289
|
-
if (!
|
|
2388
|
+
if (!isTrustedRequestHost(req))
|
|
2290
2389
|
return new Response('local request required', { status: 403 });
|
|
2291
2390
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2292
2391
|
return gitJson(await githubApi.createRepo(body));
|
|
@@ -2296,7 +2395,7 @@ export function createHttp(
|
|
|
2296
2395
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2297
2396
|
if (!sameOriginWrite(req))
|
|
2298
2397
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2299
|
-
if (!
|
|
2398
|
+
if (!isTrustedRequestHost(req))
|
|
2300
2399
|
return new Response('local request required', { status: 403 });
|
|
2301
2400
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2302
2401
|
return gitJson(await githubApi.invite(body));
|
|
@@ -2306,7 +2405,7 @@ export function createHttp(
|
|
|
2306
2405
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2307
2406
|
if (!sameOriginWrite(req))
|
|
2308
2407
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2309
|
-
if (!
|
|
2408
|
+
if (!isTrustedRequestHost(req))
|
|
2310
2409
|
return new Response('local request required', { status: 403 });
|
|
2311
2410
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2312
2411
|
return gitJson(await githubApi.clone(body));
|
|
@@ -2316,7 +2415,7 @@ export function createHttp(
|
|
|
2316
2415
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2317
2416
|
if (!sameOriginWrite(req))
|
|
2318
2417
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2319
|
-
if (!
|
|
2418
|
+
if (!isTrustedRequestHost(req))
|
|
2320
2419
|
return new Response('local request required', { status: 403 });
|
|
2321
2420
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2322
2421
|
return gitJson(await githubApi.createProject(body));
|
|
@@ -2331,7 +2430,7 @@ export function createHttp(
|
|
|
2331
2430
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2332
2431
|
if (!sameOriginWrite(req))
|
|
2333
2432
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2334
|
-
if (!
|
|
2433
|
+
if (!isTrustedRequestHost(req))
|
|
2335
2434
|
return new Response('local request required', { status: 403 });
|
|
2336
2435
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2337
2436
|
return gitJson(await githubApi.createLocalProject(body));
|
|
@@ -2344,7 +2443,7 @@ export function createHttp(
|
|
|
2344
2443
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2345
2444
|
if (!sameOriginWrite(req))
|
|
2346
2445
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2347
|
-
if (!
|
|
2446
|
+
if (!isTrustedRequestHost(req))
|
|
2348
2447
|
return new Response('local request required', { status: 403 });
|
|
2349
2448
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2350
2449
|
return gitJson(await githubApi.initDesign(body));
|
|
@@ -2359,7 +2458,7 @@ export function createHttp(
|
|
|
2359
2458
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2360
2459
|
if (!sameOriginWrite(req))
|
|
2361
2460
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2362
|
-
if (!
|
|
2461
|
+
if (!isTrustedRequestHost(req))
|
|
2363
2462
|
return new Response('local request required', { status: 403 });
|
|
2364
2463
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2365
2464
|
return gitJson(await linkHub(body));
|
|
@@ -2374,7 +2473,7 @@ export function createHttp(
|
|
|
2374
2473
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2375
2474
|
if (!sameOriginWrite(req))
|
|
2376
2475
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2377
|
-
if (!
|
|
2476
|
+
if (!isTrustedRequestHost(req))
|
|
2378
2477
|
return new Response('local request required', { status: 403 });
|
|
2379
2478
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2380
2479
|
const result = await signInToWorkspace((body ?? {}) as Record<string, unknown>);
|
|
@@ -2390,7 +2489,7 @@ export function createHttp(
|
|
|
2390
2489
|
// for, and every route the canvas can reach is one more thing to reason about.
|
|
2391
2490
|
'/_api/workspace/disclosure': async (req: Request) => {
|
|
2392
2491
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2393
|
-
if (!
|
|
2492
|
+
if (!isTrustedRequestHost(req))
|
|
2394
2493
|
return new Response('local request required', { status: 403 });
|
|
2395
2494
|
const url = new URL(req.url);
|
|
2396
2495
|
const linked = ctx.cfg.linkedHub?.url ?? null;
|
|
@@ -2420,7 +2519,7 @@ export function createHttp(
|
|
|
2420
2519
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2421
2520
|
if (!sameOriginWrite(req))
|
|
2422
2521
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2423
|
-
if (!
|
|
2522
|
+
if (!isTrustedRequestHost(req))
|
|
2424
2523
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2425
2524
|
const body = await readJson<{
|
|
2426
2525
|
canvas?: unknown;
|
|
@@ -2453,7 +2552,7 @@ export function createHttp(
|
|
|
2453
2552
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2454
2553
|
if (!sameOriginWrite(req))
|
|
2455
2554
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2456
|
-
if (!
|
|
2555
|
+
if (!isTrustedRequestHost(req))
|
|
2457
2556
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2458
2557
|
const body = await readJson<{
|
|
2459
2558
|
canvas?: unknown;
|
|
@@ -2486,7 +2585,7 @@ export function createHttp(
|
|
|
2486
2585
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2487
2586
|
if (!sameOriginWrite(req))
|
|
2488
2587
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2489
|
-
if (!
|
|
2588
|
+
if (!isTrustedRequestHost(req))
|
|
2490
2589
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2491
2590
|
const body = await readJson<{
|
|
2492
2591
|
canvas?: unknown;
|
|
@@ -2522,7 +2621,7 @@ export function createHttp(
|
|
|
2522
2621
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2523
2622
|
if (!sameOriginWrite(req))
|
|
2524
2623
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2525
|
-
if (!
|
|
2624
|
+
if (!isTrustedRequestHost(req))
|
|
2526
2625
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2527
2626
|
const body = await readJson<{
|
|
2528
2627
|
canvas?: unknown;
|
|
@@ -2562,7 +2661,7 @@ export function createHttp(
|
|
|
2562
2661
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2563
2662
|
if (!sameOriginWrite(req))
|
|
2564
2663
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2565
|
-
if (!
|
|
2664
|
+
if (!isTrustedRequestHost(req))
|
|
2566
2665
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2567
2666
|
const body = await readJson<{
|
|
2568
2667
|
canvas?: unknown;
|
|
@@ -2598,7 +2697,7 @@ export function createHttp(
|
|
|
2598
2697
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2599
2698
|
if (!sameOriginWrite(req))
|
|
2600
2699
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2601
|
-
if (!
|
|
2700
|
+
if (!isTrustedRequestHost(req))
|
|
2602
2701
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2603
2702
|
const body = await readJson<{
|
|
2604
2703
|
canvas?: unknown;
|
|
@@ -2643,7 +2742,7 @@ export function createHttp(
|
|
|
2643
2742
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2644
2743
|
if (!sameOriginWrite(req))
|
|
2645
2744
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2646
|
-
if (!
|
|
2745
|
+
if (!isTrustedRequestHost(req))
|
|
2647
2746
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2648
2747
|
const body = await readJson<{
|
|
2649
2748
|
canvas?: unknown;
|
|
@@ -2673,7 +2772,7 @@ export function createHttp(
|
|
|
2673
2772
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2674
2773
|
if (!sameOriginWrite(req))
|
|
2675
2774
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2676
|
-
if (!
|
|
2775
|
+
if (!isTrustedRequestHost(req))
|
|
2677
2776
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2678
2777
|
const body = await readJson<{
|
|
2679
2778
|
canvas?: unknown;
|
|
@@ -2710,7 +2809,7 @@ export function createHttp(
|
|
|
2710
2809
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2711
2810
|
if (!sameOriginWrite(req))
|
|
2712
2811
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2713
|
-
if (!
|
|
2812
|
+
if (!isTrustedRequestHost(req))
|
|
2714
2813
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2715
2814
|
const body = await readJson<{
|
|
2716
2815
|
canvas?: unknown;
|
|
@@ -2743,7 +2842,7 @@ export function createHttp(
|
|
|
2743
2842
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2744
2843
|
if (!sameOriginWrite(req))
|
|
2745
2844
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2746
|
-
if (!
|
|
2845
|
+
if (!isTrustedRequestHost(req))
|
|
2747
2846
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2748
2847
|
const body = await readJson<{
|
|
2749
2848
|
canvas?: unknown;
|
|
@@ -2774,7 +2873,7 @@ export function createHttp(
|
|
|
2774
2873
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2775
2874
|
if (!sameOriginWrite(req))
|
|
2776
2875
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2777
|
-
if (!
|
|
2876
|
+
if (!isTrustedRequestHost(req))
|
|
2778
2877
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2779
2878
|
const body = await readJson<{
|
|
2780
2879
|
canvas?: unknown;
|
|
@@ -2808,7 +2907,7 @@ export function createHttp(
|
|
|
2808
2907
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2809
2908
|
// DNS-rebinding guard — this GET echoes project source structure; loopback-
|
|
2810
2909
|
// only Host (real browser passes, rebound hostname 403s). No sameOriginWrite (GET).
|
|
2811
|
-
if (!
|
|
2910
|
+
if (!isTrustedRequestHost(req))
|
|
2812
2911
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2813
2912
|
const u = new URL(req.url);
|
|
2814
2913
|
const result = await api.compClips({
|
|
@@ -2834,7 +2933,7 @@ export function createHttp(
|
|
|
2834
2933
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2835
2934
|
if (!sameOriginWrite(req))
|
|
2836
2935
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2837
|
-
if (!
|
|
2936
|
+
if (!isTrustedRequestHost(req))
|
|
2838
2937
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2839
2938
|
const body = await readJson<{ canvas?: unknown; seq?: unknown; dir?: unknown }>(
|
|
2840
2939
|
req,
|
|
@@ -2865,7 +2964,7 @@ export function createHttp(
|
|
|
2865
2964
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2866
2965
|
if (!sameOriginWrite(req))
|
|
2867
2966
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2868
|
-
if (!
|
|
2967
|
+
if (!isTrustedRequestHost(req))
|
|
2869
2968
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2870
2969
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2871
2970
|
req,
|
|
@@ -2897,7 +2996,7 @@ export function createHttp(
|
|
|
2897
2996
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2898
2997
|
if (!sameOriginWrite(req))
|
|
2899
2998
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2900
|
-
if (!
|
|
2999
|
+
if (!isTrustedRequestHost(req))
|
|
2901
3000
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2902
3001
|
const body = await readJson<{
|
|
2903
3002
|
canvas?: unknown;
|
|
@@ -2932,7 +3031,7 @@ export function createHttp(
|
|
|
2932
3031
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2933
3032
|
if (!sameOriginWrite(req))
|
|
2934
3033
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2935
|
-
if (!
|
|
3034
|
+
if (!isTrustedRequestHost(req))
|
|
2936
3035
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2937
3036
|
const body = await readJson<{
|
|
2938
3037
|
canvas?: unknown;
|
|
@@ -2966,7 +3065,7 @@ export function createHttp(
|
|
|
2966
3065
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2967
3066
|
if (!sameOriginWrite(req))
|
|
2968
3067
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2969
|
-
if (!
|
|
3068
|
+
if (!isTrustedRequestHost(req))
|
|
2970
3069
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2971
3070
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2972
3071
|
req,
|
|
@@ -2993,7 +3092,7 @@ export function createHttp(
|
|
|
2993
3092
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2994
3093
|
if (!sameOriginWrite(req))
|
|
2995
3094
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2996
|
-
if (!
|
|
3095
|
+
if (!isTrustedRequestHost(req))
|
|
2997
3096
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2998
3097
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2999
3098
|
req,
|
|
@@ -3022,7 +3121,7 @@ export function createHttp(
|
|
|
3022
3121
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3023
3122
|
if (!sameOriginWrite(req))
|
|
3024
3123
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3025
|
-
if (!
|
|
3124
|
+
if (!isTrustedRequestHost(req))
|
|
3026
3125
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3027
3126
|
const body = await readJson<{
|
|
3028
3127
|
canvas?: unknown;
|
|
@@ -3056,7 +3155,7 @@ export function createHttp(
|
|
|
3056
3155
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3057
3156
|
if (!sameOriginWrite(req))
|
|
3058
3157
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3059
|
-
if (!
|
|
3158
|
+
if (!isTrustedRequestHost(req))
|
|
3060
3159
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3061
3160
|
const body = await readJson<{
|
|
3062
3161
|
canvas?: unknown;
|
|
@@ -3085,7 +3184,7 @@ export function createHttp(
|
|
|
3085
3184
|
// + startCanvasServer routes (dual-allowlist, DDR-054). Loopback-Host gated
|
|
3086
3185
|
// (DNS-rebinding); read-only, so no sameOriginWrite (GET).
|
|
3087
3186
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3088
|
-
if (!
|
|
3187
|
+
if (!isTrustedRequestHost(req))
|
|
3089
3188
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3090
3189
|
const r = await api.listAssets();
|
|
3091
3190
|
return Response.json(
|
|
@@ -3103,7 +3202,7 @@ export function createHttp(
|
|
|
3103
3202
|
// routes (dual-allowlist, DDR-054). Loopback-Host gated (DNS-rebinding);
|
|
3104
3203
|
// read-only, so no sameOriginWrite (GET).
|
|
3105
3204
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3106
|
-
if (!
|
|
3205
|
+
if (!isTrustedRequestHost(req))
|
|
3107
3206
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3108
3207
|
const r = await api.listStickers();
|
|
3109
3208
|
return Response.json(
|
|
@@ -3120,7 +3219,7 @@ export function createHttp(
|
|
|
3120
3219
|
// from CANVAS_SAFE_API + startCanvasServer routes (dual-allowlist, DDR-054);
|
|
3121
3220
|
// loopback-Host gated (DNS-rebinding); no sameOriginWrite (GET).
|
|
3122
3221
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3123
|
-
if (!
|
|
3222
|
+
if (!isTrustedRequestHost(req))
|
|
3124
3223
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3125
3224
|
const url = new URL(req.url);
|
|
3126
3225
|
const result = await api.editScopeOp({
|
|
@@ -3144,7 +3243,7 @@ export function createHttp(
|
|
|
3144
3243
|
// CANVAS_SAFE_API + startCanvasServer routes (DDR-054); loopback-Host
|
|
3145
3244
|
// gated; no sameOriginWrite (GET).
|
|
3146
3245
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3147
|
-
if (!
|
|
3246
|
+
if (!isTrustedRequestHost(req))
|
|
3148
3247
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3149
3248
|
const url = new URL(req.url);
|
|
3150
3249
|
const result = await api.componentMapOp({
|
|
@@ -3168,7 +3267,7 @@ export function createHttp(
|
|
|
3168
3267
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3169
3268
|
if (!sameOriginWrite(req))
|
|
3170
3269
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3171
|
-
if (!
|
|
3270
|
+
if (!isTrustedRequestHost(req))
|
|
3172
3271
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3173
3272
|
const body = await readJson<{
|
|
3174
3273
|
canvas?: unknown;
|
|
@@ -3200,7 +3299,7 @@ export function createHttp(
|
|
|
3200
3299
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3201
3300
|
if (!sameOriginWrite(req))
|
|
3202
3301
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3203
|
-
if (!
|
|
3302
|
+
if (!isTrustedRequestHost(req))
|
|
3204
3303
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3205
3304
|
const body = await readJson<{
|
|
3206
3305
|
canvas?: unknown;
|
|
@@ -3231,7 +3330,7 @@ export function createHttp(
|
|
|
3231
3330
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3232
3331
|
if (!sameOriginWrite(req))
|
|
3233
3332
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3234
|
-
if (!
|
|
3333
|
+
if (!isTrustedRequestHost(req))
|
|
3235
3334
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3236
3335
|
const body = await readJson<{
|
|
3237
3336
|
canvas?: unknown;
|
|
@@ -3265,7 +3364,7 @@ export function createHttp(
|
|
|
3265
3364
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3266
3365
|
if (!sameOriginWrite(req))
|
|
3267
3366
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3268
|
-
if (!
|
|
3367
|
+
if (!isTrustedRequestHost(req))
|
|
3269
3368
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3270
3369
|
const body = await readJson<{
|
|
3271
3370
|
canvas?: unknown;
|
|
@@ -3295,7 +3394,7 @@ export function createHttp(
|
|
|
3295
3394
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3296
3395
|
if (!sameOriginWrite(req))
|
|
3297
3396
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3298
|
-
if (!
|
|
3397
|
+
if (!isTrustedRequestHost(req))
|
|
3299
3398
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3300
3399
|
const body = await readJson<{
|
|
3301
3400
|
canvas?: unknown;
|
|
@@ -3326,7 +3425,7 @@ export function createHttp(
|
|
|
3326
3425
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3327
3426
|
if (!sameOriginWrite(req))
|
|
3328
3427
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3329
|
-
if (!
|
|
3428
|
+
if (!isTrustedRequestHost(req))
|
|
3330
3429
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3331
3430
|
const body = await readJson<{
|
|
3332
3431
|
canvas?: unknown;
|
|
@@ -3355,7 +3454,7 @@ export function createHttp(
|
|
|
3355
3454
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3356
3455
|
if (!sameOriginWrite(req))
|
|
3357
3456
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3358
|
-
if (!
|
|
3457
|
+
if (!isTrustedRequestHost(req))
|
|
3359
3458
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3360
3459
|
const body = await readJson<{ canvas?: unknown; artboardId?: unknown }>(req, 8 * 1024);
|
|
3361
3460
|
if (!body) return new Response('body required', { status: 400 });
|
|
@@ -3423,7 +3522,7 @@ export function createHttp(
|
|
|
3423
3522
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3424
3523
|
if (!sameOriginWrite(req))
|
|
3425
3524
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3426
|
-
if (!
|
|
3525
|
+
if (!isTrustedRequestHost(req))
|
|
3427
3526
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3428
3527
|
const kind = (req.headers.get('x-import-kind') || 'svg').toLowerCase();
|
|
3429
3528
|
if (kind !== 'svg') {
|
|
@@ -3490,7 +3589,7 @@ export function createHttp(
|
|
|
3490
3589
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3491
3590
|
if (!sameOriginWrite(req))
|
|
3492
3591
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3493
|
-
if (!
|
|
3592
|
+
if (!isTrustedRequestHost(req))
|
|
3494
3593
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3495
3594
|
let body: { assetPath?: unknown };
|
|
3496
3595
|
try {
|
|
@@ -3564,7 +3663,7 @@ export function createHttp(
|
|
|
3564
3663
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3565
3664
|
if (!sameOriginWrite(req))
|
|
3566
3665
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3567
|
-
if (!
|
|
3666
|
+
if (!isTrustedRequestHost(req))
|
|
3568
3667
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3569
3668
|
const body = await readJson<{
|
|
3570
3669
|
format?: unknown;
|
|
@@ -3608,7 +3707,7 @@ export function createHttp(
|
|
|
3608
3707
|
// /_api/export today — DDR-060). loopback-Host gated on every method;
|
|
3609
3708
|
// the mutating POST is additionally sameOriginWrite CSRF gated (same
|
|
3610
3709
|
// /flow:done security fan-out fix as /_api/export above).
|
|
3611
|
-
if (!
|
|
3710
|
+
if (!isTrustedRequestHost(req))
|
|
3612
3711
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3613
3712
|
if (req.method === 'GET') {
|
|
3614
3713
|
return Response.json(
|
|
@@ -3656,7 +3755,7 @@ export function createHttp(
|
|
|
3656
3755
|
// unguessable job-scoped UUID plus this guard closes the DNS-rebinding
|
|
3657
3756
|
// angle the /flow:done security fan-out checked for).
|
|
3658
3757
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3659
|
-
if (!
|
|
3758
|
+
if (!isTrustedRequestHost(req))
|
|
3660
3759
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3661
3760
|
const id = new URL(req.url).searchParams.get('id');
|
|
3662
3761
|
if (!id) return new Response('id query param required', { status: 400 });
|
|
@@ -3684,7 +3783,7 @@ export function createHttp(
|
|
|
3684
3783
|
// loopback-Host gated on every method; the mutating POST is additionally
|
|
3685
3784
|
// sameOriginWrite CSRF-gated. See DDR-16x + the canvas-origin-gate test.
|
|
3686
3785
|
'/_api/generate-jobs': async (req: Request) => {
|
|
3687
|
-
if (!
|
|
3786
|
+
if (!isTrustedRequestHost(req))
|
|
3688
3787
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3689
3788
|
if (req.method === 'GET') {
|
|
3690
3789
|
return Response.json(
|
|
@@ -3803,7 +3902,7 @@ export function createHttp(
|
|
|
3803
3902
|
// keyUrl) + presence-only `configured` flags. NO secret ever crosses here.
|
|
3804
3903
|
// MAIN-ORIGIN ONLY, loopback-gated.
|
|
3805
3904
|
'/_api/generate/providers': async (req: Request) => {
|
|
3806
|
-
if (!
|
|
3905
|
+
if (!isTrustedRequestHost(req))
|
|
3807
3906
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3808
3907
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3809
3908
|
const configured = new Set(configuredProviders());
|
|
@@ -3820,7 +3919,7 @@ export function createHttp(
|
|
|
3820
3919
|
// ({configured:[...]}) — a key value is NEVER echoed back (mirrors
|
|
3821
3920
|
// github_is_signed_in). MAIN-ORIGIN ONLY, loopback + sameOriginWrite gated.
|
|
3822
3921
|
'/_api/generate/keys': async (req: Request) => {
|
|
3823
|
-
if (!
|
|
3922
|
+
if (!isTrustedRequestHost(req))
|
|
3824
3923
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3825
3924
|
if (req.method === 'GET') {
|
|
3826
3925
|
return Response.json(
|
|
@@ -3864,7 +3963,7 @@ export function createHttp(
|
|
|
3864
3963
|
// ONLY, loopback + sameOriginWrite gated. NEVER touches a key — that's the
|
|
3865
3964
|
// separate /_api/generate/keys route.
|
|
3866
3965
|
'/_api/generate/prefs': async (req: Request) => {
|
|
3867
|
-
if (!
|
|
3966
|
+
if (!isTrustedRequestHost(req))
|
|
3868
3967
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3869
3968
|
if (req.method === 'GET') {
|
|
3870
3969
|
return Response.json(
|
|
@@ -3897,7 +3996,7 @@ export function createHttp(
|
|
|
3897
3996
|
}
|
|
3898
3997
|
const provider = body?.transcriptionProvider;
|
|
3899
3998
|
if (!isTranscriptionProvider(provider))
|
|
3900
|
-
return new Response('transcriptionProvider must be whisper|elevenlabs|groq', {
|
|
3999
|
+
return new Response('transcriptionProvider must be auto|whisper|elevenlabs|groq', {
|
|
3901
4000
|
status: 400,
|
|
3902
4001
|
});
|
|
3903
4002
|
await writeTranscriptionProvider(ctx.paths.repoRoot, provider);
|
|
@@ -3919,7 +4018,7 @@ export function createHttp(
|
|
|
3919
4018
|
// prefs; POST merge-patches only the provided keys. MAIN-ORIGIN ONLY, loopback
|
|
3920
4019
|
// + sameOriginWrite gated (never reachable from the untrusted canvas origin).
|
|
3921
4020
|
'/_api/ui-prefs': async (req: Request) => {
|
|
3922
|
-
if (!
|
|
4021
|
+
if (!isTrustedRequestHost(req))
|
|
3923
4022
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3924
4023
|
if (req.method === 'GET') {
|
|
3925
4024
|
return Response.json(readUiPrefs(), { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -3976,7 +4075,7 @@ export function createHttp(
|
|
|
3976
4075
|
// already paid), returning ranked reuse candidates. MAIN-ORIGIN ONLY,
|
|
3977
4076
|
// loopback-gated (it resolves the provider key server-side).
|
|
3978
4077
|
'/_api/generate/audio-search': async (req: Request) => {
|
|
3979
|
-
if (!
|
|
4078
|
+
if (!isTrustedRequestHost(req))
|
|
3980
4079
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3981
4080
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3982
4081
|
// F1 (ethical-hacker) — this GET resolves the user's key + fans out to the
|
|
@@ -4024,7 +4123,7 @@ export function createHttp(
|
|
|
4024
4123
|
// reused track is itself searchable. MAIN-ORIGIN ONLY, loopback +
|
|
4025
4124
|
// sameOriginWrite gated.
|
|
4026
4125
|
'/_api/generate/audio-reuse': async (req: Request) => {
|
|
4027
|
-
if (!
|
|
4126
|
+
if (!isTrustedRequestHost(req))
|
|
4028
4127
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4029
4128
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4030
4129
|
if (!sameOriginWrite(req))
|
|
@@ -4064,11 +4163,21 @@ export function createHttp(
|
|
|
4064
4163
|
// `maude design transcribe --provider whisper` auto-resolves it. MAIN-ORIGIN
|
|
4065
4164
|
// ONLY, loopback + sameOriginWrite gated.
|
|
4066
4165
|
'/_api/generate/whisper-model': async (req: Request) => {
|
|
4067
|
-
if (!
|
|
4166
|
+
if (!isTrustedRequestHost(req))
|
|
4068
4167
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4069
4168
|
if (req.method === 'GET') {
|
|
4169
|
+
// `setup` tells the card whether the ENGINE is actually installed and
|
|
4170
|
+
// how to get it on THIS machine (no `brew install` without Homebrew);
|
|
4171
|
+
// `auto` reports what the automatic engine choice currently resolves
|
|
4172
|
+
// to, so the card can state it out loud rather than switch silently.
|
|
4173
|
+
const setup = whisperSetup();
|
|
4070
4174
|
return Response.json(
|
|
4071
|
-
{
|
|
4175
|
+
{
|
|
4176
|
+
models: listWhisperModels(),
|
|
4177
|
+
downloading: whisperDownload,
|
|
4178
|
+
setup,
|
|
4179
|
+
auto: resolveAutoEngine(configuredProviders(), setup.installed),
|
|
4180
|
+
},
|
|
4072
4181
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4073
4182
|
);
|
|
4074
4183
|
}
|
|
@@ -4127,7 +4236,7 @@ export function createHttp(
|
|
|
4127
4236
|
// which tier will run; POST {id} downloads one via huggingface_hub (gated on
|
|
4128
4237
|
// mlx-vlm being installed — the model is useless without it). MAIN-ORIGIN ONLY.
|
|
4129
4238
|
'/_api/generate/keyframe-model': async (req: Request) => {
|
|
4130
|
-
if (!
|
|
4239
|
+
if (!isTrustedRequestHost(req))
|
|
4131
4240
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4132
4241
|
if (req.method === 'GET') {
|
|
4133
4242
|
// The GET builds availability by spawning subprocesses (import mlx_vlm is
|
|
@@ -4137,12 +4246,28 @@ export function createHttp(
|
|
|
4137
4246
|
// are also TTL-cached in gemma-models.ts as defence-in-depth.
|
|
4138
4247
|
if (!sameOriginRead(req))
|
|
4139
4248
|
return new Response('cross-origin read rejected', { status: 403 });
|
|
4249
|
+
const ollama = await ollamaStatus();
|
|
4140
4250
|
return Response.json(
|
|
4141
4251
|
{
|
|
4142
|
-
models
|
|
4252
|
+
// Both runtimes' models in one list, each tagged with the runtime
|
|
4253
|
+
// it needs — the card enables the button for whichever runtime this
|
|
4254
|
+
// machine actually has, instead of a dead "Needs mlx-vlm".
|
|
4255
|
+
models: listScoutModels(ollama),
|
|
4143
4256
|
downloading: keyframeDownload,
|
|
4144
4257
|
mlxVlmAvailable: mlxVlmAvailable(),
|
|
4145
4258
|
ffmpegAvailable: ffmpegAvailable(),
|
|
4259
|
+
// Setup routes that actually work on THIS machine, best first — no
|
|
4260
|
+
// `brew install` without Homebrew, no venv one-liner without
|
|
4261
|
+
// python3, and "start it" rather than "install it" when the binary
|
|
4262
|
+
// is already there. Computed from the SAME paths the probes check,
|
|
4263
|
+
// so the shown command and the detection can't drift.
|
|
4264
|
+
mlx: mlxSetup(),
|
|
4265
|
+
ollama: {
|
|
4266
|
+
...ollama,
|
|
4267
|
+
recommendedModel: OLLAMA_RECOMMENDED_MODEL,
|
|
4268
|
+
pullCommand: `ollama pull ${OLLAMA_RECOMMENDED_MODEL}`,
|
|
4269
|
+
setup: ollamaSetupOptions(ollama),
|
|
4270
|
+
},
|
|
4146
4271
|
},
|
|
4147
4272
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4148
4273
|
);
|
|
@@ -4158,23 +4283,34 @@ export function createHttp(
|
|
|
4158
4283
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4159
4284
|
if (!sameOriginWrite(req))
|
|
4160
4285
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
4161
|
-
|
|
4286
|
+
const body = await readJson<{ id?: unknown }>(req, 4 * 1024);
|
|
4287
|
+
const id = typeof body?.id === 'string' ? body.id : '';
|
|
4288
|
+
// One route, two runtimes: an `ollama:` id pulls through the local Ollama
|
|
4289
|
+
// server, anything else is an mlx HF snapshot. Each gates on ITS OWN
|
|
4290
|
+
// runtime — an Ollama user must not be blocked by a missing mlx-vlm.
|
|
4291
|
+
const isOllama = Boolean(getOllamaModel(id));
|
|
4292
|
+
if (!isOllama && !listGemmaModels().some((m) => m.id === id))
|
|
4293
|
+
return new Response('unknown model id', { status: 400 });
|
|
4294
|
+
if (isOllama) {
|
|
4295
|
+
const status = await ollamaStatus();
|
|
4296
|
+
if (!status.available)
|
|
4297
|
+
return new Response('Ollama is not running — start it (`ollama serve`) and try again.', {
|
|
4298
|
+
status: 400,
|
|
4299
|
+
});
|
|
4300
|
+
} else if (!mlxVlmAvailable())
|
|
4162
4301
|
return new Response(
|
|
4163
|
-
'mlx-vlm not installed — the
|
|
4302
|
+
'mlx-vlm not installed — run the install command shown in Settings → Video. (Ollama users don’t need this download: `ollama pull` fetches its own models.)',
|
|
4164
4303
|
{
|
|
4165
4304
|
status: 400,
|
|
4166
4305
|
}
|
|
4167
4306
|
);
|
|
4168
|
-
const body = await readJson<{ id?: unknown }>(req, 4 * 1024);
|
|
4169
|
-
const id = typeof body?.id === 'string' ? body.id : '';
|
|
4170
|
-
if (!listGemmaModels().some((m) => m.id === id))
|
|
4171
|
-
return new Response('unknown model id', { status: 400 });
|
|
4172
4307
|
if (keyframeDownload && !keyframeDownload.error)
|
|
4173
4308
|
return new Response(`a download is already in progress (${keyframeDownload.id})`, {
|
|
4174
4309
|
status: 409,
|
|
4175
4310
|
});
|
|
4176
4311
|
keyframeDownload = { id, received: 0, total: 0 };
|
|
4177
|
-
|
|
4312
|
+
const run = isOllama ? pullOllamaModel : downloadGemmaModel;
|
|
4313
|
+
void run(
|
|
4178
4314
|
id,
|
|
4179
4315
|
(received, total) => {
|
|
4180
4316
|
if (keyframeDownload && keyframeDownload.id === id) {
|
|
@@ -4212,7 +4348,7 @@ export function createHttp(
|
|
|
4212
4348
|
// dataURLs of local footage → disclosure) or WRITE arbitrary JSON later
|
|
4213
4349
|
// rendered as <img src> in the trusted shell. The loopback-host check is
|
|
4214
4350
|
// the same backstop every sibling privileged route carries.
|
|
4215
|
-
if (!
|
|
4351
|
+
if (!isTrustedRequestHost(req))
|
|
4216
4352
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4217
4353
|
const url = new URL(req.url);
|
|
4218
4354
|
const key = url.searchParams.get('key') ?? '';
|