@1agh/maude 0.54.0 → 0.56.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/annotations-layer.tsx +11 -1
- 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/canvas-build-sandbox.ts +101 -9
- package/apps/studio/canvas-build-worker.ts +7 -1
- package/apps/studio/canvas-build.ts +9 -1
- package/apps/studio/client/app.jsx +49 -3
- package/apps/studio/client/github.js +7 -0
- package/apps/studio/client/panels/CloudBar.jsx +505 -45
- package/apps/studio/client/panels/GitPanel.jsx +38 -25
- package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
- package/apps/studio/client/styles/3-shell-maude.css +30 -0
- package/apps/studio/client/styles/4-components.css +72 -0
- package/apps/studio/cloud/endpoints.ts +104 -9
- 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 +1545 -1545
- 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 +210 -110
- package/apps/studio/input-router.tsx +55 -2
- package/apps/studio/server.ts +11 -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/index.ts +239 -26
- package/apps/studio/sync/limits.ts +49 -0
- package/apps/studio/sync/loopback.ts +21 -0
- package/apps/studio/sync/projection.ts +47 -12
- package/apps/studio/sync/supervisor.ts +178 -0
- package/apps/studio/test/cloud-endpoints.test.ts +326 -4
- package/apps/studio/test/csrf-write-guard.test.ts +19 -2
- 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-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
|
@@ -22,7 +22,7 @@ import { type Api, ASSET_MAX_BYTES, ASSET_MAX_VIDEO_BYTES } from './api.ts';
|
|
|
22
22
|
import { ImportAssetError, importSvg, SVG_MAX_BYTES } from './bin/_import-asset.mjs';
|
|
23
23
|
import { ImportBrandError, importBrand } from './bin/_import-brand.mjs';
|
|
24
24
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
25
|
-
import { buildCanvasSandboxed } from './canvas-build-sandbox.ts';
|
|
25
|
+
import { buildCanvasSandboxed, buildStats } from './canvas-build-sandbox.ts';
|
|
26
26
|
import { canvasLibPath } from './canvas-lib-resolver.ts';
|
|
27
27
|
import { TranspileError } from './canvas-pipeline.ts';
|
|
28
28
|
import { createCloudEndpoints } from './cloud/endpoints.ts';
|
|
@@ -46,8 +46,15 @@ import { localizeGenAsset } from './generation/download.ts';
|
|
|
46
46
|
import {
|
|
47
47
|
downloadGemmaModel,
|
|
48
48
|
ffmpegAvailable,
|
|
49
|
+
getOllamaModel,
|
|
49
50
|
listGemmaModels,
|
|
51
|
+
listScoutModels,
|
|
52
|
+
mlxSetup,
|
|
50
53
|
mlxVlmAvailable,
|
|
54
|
+
OLLAMA_RECOMMENDED_MODEL,
|
|
55
|
+
ollamaSetupOptions,
|
|
56
|
+
ollamaStatus,
|
|
57
|
+
pullOllamaModel,
|
|
51
58
|
} from './generation/gemma-models.ts';
|
|
52
59
|
import { type GenerationJobQueue, GenerationQueueFullError } from './generation/jobs.ts';
|
|
53
60
|
import {
|
|
@@ -77,6 +84,8 @@ import {
|
|
|
77
84
|
getWhisperModel,
|
|
78
85
|
listWhisperModels,
|
|
79
86
|
removeWhisperModel,
|
|
87
|
+
resolveAutoEngine,
|
|
88
|
+
whisperSetup,
|
|
80
89
|
} from './generation/whisper-models.ts';
|
|
81
90
|
import { createGitEndpoints } from './git/endpoints.ts';
|
|
82
91
|
import { gitShowFile } from './git/service.ts';
|
|
@@ -289,12 +298,53 @@ export function sameOriginWrite(req: Request): boolean {
|
|
|
289
298
|
const origin = req.headers.get('origin');
|
|
290
299
|
if (!origin) return true; // non-browser / same-origin omitting Origin → allow
|
|
291
300
|
try {
|
|
292
|
-
|
|
301
|
+
// HOSTS, not full origins — the same reason isOwnShellOrigin (studio-proxy)
|
|
302
|
+
// compares host-only for the collab WS upgrade. In a cell, TLS terminates at
|
|
303
|
+
// the proxy, so the browser's `https://<host>` Origin reaches a studio that
|
|
304
|
+
// serves plaintext and computes `req.url` as `http://<host>`; a full-origin
|
|
305
|
+
// compare then rejects every legitimate shell write on the SCHEME alone
|
|
306
|
+
// (403 "cross-origin write rejected" — the second half of the inspector-edits
|
|
307
|
+
// RCA, found when the loopback fix uncovered it). Host+port still pins the
|
|
308
|
+
// origin: the port rides in `host`, and scheme was never the CSRF
|
|
309
|
+
// discriminator here — locally everything is http, and a real cross-site page
|
|
310
|
+
// has a different host.
|
|
311
|
+
return new URL(origin).host === new URL(req.url).host;
|
|
293
312
|
} catch {
|
|
294
313
|
return false;
|
|
295
314
|
}
|
|
296
315
|
}
|
|
297
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The DNS-rebind guard, mode-aware — Cloud Phase 27, the inspector-edits RCA
|
|
319
|
+
* (`issue-cloud-inspector-edits-refused-by-loopback-guard`).
|
|
320
|
+
*
|
|
321
|
+
* `isLoopbackHost(req.headers.get('host'))` is right on a laptop: a malicious
|
|
322
|
+
* page that resolves `evil.com` → 127.0.0.1 makes the browser send a FOREIGN
|
|
323
|
+
* Host to the locally-bound dev server, and the guard rejects it. In a CELL that
|
|
324
|
+
* same check can NEVER pass — the hub's authenticating proxy rewrites Host to the
|
|
325
|
+
* project's public name (D4, `upstreamHeaders`) on purpose — so every shell edit
|
|
326
|
+
* (Inspector CSS, artboard ops, insert/delete/reorder, edit-attr/text) 403'd
|
|
327
|
+
* with "local request required (DNS-rebinding guard)". Annotations and comments
|
|
328
|
+
* were spared only because their handlers carry no such guard; that split of
|
|
329
|
+
* which routes worked was exactly the fingerprint of this bug.
|
|
330
|
+
*
|
|
331
|
+
* The cell doesn't NEED the Host check: the proxy already terminated the session,
|
|
332
|
+
* authorized it against the one role table (studio-manifest's `decide()`), and
|
|
333
|
+
* injects `x-maude-role` after stripping every inbound `x-maude-*` — so the
|
|
334
|
+
* header's PRESENCE is unforgeable proof the request came through the hub. Same
|
|
335
|
+
* `WORKSPACE ? x-maude-role` vouch the collab-WS upgrade already trusts
|
|
336
|
+
* (server.ts, RCA issue-cloud-live-collaboration-dead). Outside workspace mode
|
|
337
|
+
* this is the loopback check, verbatim.
|
|
338
|
+
*
|
|
339
|
+
* Safe against secret leakage by construction: a cell PRUNES every secret-bearing
|
|
340
|
+
* route family out of its table (`pruneForWorkspace` + the boot-assert), so the
|
|
341
|
+
* vouched path can only ever reach the project-editing surfaces that survive.
|
|
342
|
+
*/
|
|
343
|
+
export function isTrustedRequestHost(req: Request): boolean {
|
|
344
|
+
if (isLoopbackHost(req.headers.get('host'))) return true;
|
|
345
|
+
return isWorkspaceMode() && !!req.headers.get('x-maude-role');
|
|
346
|
+
}
|
|
347
|
+
|
|
298
348
|
/**
|
|
299
349
|
* CSRF guard for a key-bearing / side-effecting GET (Task 2.5 audio-search —
|
|
300
350
|
* ethical-hacker F1). A cross-site page can issue a `no-cors` GET to
|
|
@@ -1108,6 +1158,19 @@ export function createHttp(
|
|
|
1108
1158
|
// supervisor hashes its own expected root and compares.
|
|
1109
1159
|
rootId: rootIdentity(ctx.paths.repoRoot),
|
|
1110
1160
|
pid: process.pid,
|
|
1161
|
+
// Cloud Phase 26 Stage 4 — the build sandbox's own counters, so the
|
|
1162
|
+
// €3/cell model finally has figures instead of a prediction. Counts and
|
|
1163
|
+
// durations ONLY: never a canvas name, never a path, and never the text
|
|
1164
|
+
// of a rejected import specifier (that specifier is tenant-authored
|
|
1165
|
+
// content — the operational fact is that a rejection happened, not what
|
|
1166
|
+
// it said).
|
|
1167
|
+
//
|
|
1168
|
+
// On `/_health`, which the untrusted canvas origin may read, and that
|
|
1169
|
+
// is deliberate: every figure here is about the reader's OWN cell, so
|
|
1170
|
+
// it discloses nothing across a tenant boundary. Inventing a privileged
|
|
1171
|
+
// route would have meant inventing an auth path for the supervisor's
|
|
1172
|
+
// loopback probe, for data the tenant already has.
|
|
1173
|
+
render: buildStats(),
|
|
1111
1174
|
}),
|
|
1112
1175
|
|
|
1113
1176
|
'/_active': () => Response.json(inspect().state),
|
|
@@ -1124,7 +1187,7 @@ export function createHttp(
|
|
|
1124
1187
|
// but newly worth closing given what this diff makes it able to reveal.
|
|
1125
1188
|
'/_api/acp/status': async (req: Request) => {
|
|
1126
1189
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1127
|
-
if (!
|
|
1190
|
+
if (!isTrustedRequestHost(req))
|
|
1128
1191
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1129
1192
|
return Response.json(await probeAcpAvailabilityAuthed(), {
|
|
1130
1193
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1142,7 +1205,7 @@ export function createHttp(
|
|
|
1142
1205
|
// ChatPanel not-connected explainer + the Help-menu modal read this.
|
|
1143
1206
|
'/_api/preflight': async (req: Request) => {
|
|
1144
1207
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1145
|
-
if (!
|
|
1208
|
+
if (!isTrustedRequestHost(req))
|
|
1146
1209
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1147
1210
|
return Response.json(await probeReadiness(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1148
1211
|
},
|
|
@@ -1156,7 +1219,7 @@ export function createHttp(
|
|
|
1156
1219
|
// (SetupChecklist.jsx) polls this.
|
|
1157
1220
|
'/_api/setup-readiness': async (req: Request) => {
|
|
1158
1221
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1159
|
-
if (!
|
|
1222
|
+
if (!isTrustedRequestHost(req))
|
|
1160
1223
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1161
1224
|
return Response.json(await probeSetupReadiness(ctx), {
|
|
1162
1225
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1172,7 +1235,7 @@ export function createHttp(
|
|
|
1172
1235
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1173
1236
|
if (!sameOriginWrite(req))
|
|
1174
1237
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1175
|
-
if (!
|
|
1238
|
+
if (!isTrustedRequestHost(req))
|
|
1176
1239
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1177
1240
|
// Fire-and-forget — the install can run past Bun's default 10s idle
|
|
1178
1241
|
// timeout on a slow network, so this returns as soon as the spawn is
|
|
@@ -1193,7 +1256,7 @@ export function createHttp(
|
|
|
1193
1256
|
// two routes, which used the wrong guard.
|
|
1194
1257
|
'/_api/claude/install-status': (req: Request) => {
|
|
1195
1258
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1196
|
-
if (!
|
|
1259
|
+
if (!isTrustedRequestHost(req))
|
|
1197
1260
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1198
1261
|
return Response.json(getInstallState(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1199
1262
|
},
|
|
@@ -1210,7 +1273,7 @@ export function createHttp(
|
|
|
1210
1273
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1211
1274
|
if (!sameOriginWrite(req))
|
|
1212
1275
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1213
|
-
if (!
|
|
1276
|
+
if (!isTrustedRequestHost(req))
|
|
1214
1277
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1215
1278
|
const result = startSignin();
|
|
1216
1279
|
return Response.json(result, {
|
|
@@ -1222,7 +1285,7 @@ export function createHttp(
|
|
|
1222
1285
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1223
1286
|
if (!sameOriginWrite(req))
|
|
1224
1287
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1225
|
-
if (!
|
|
1288
|
+
if (!isTrustedRequestHost(req))
|
|
1226
1289
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1227
1290
|
cancelSignin();
|
|
1228
1291
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -1232,7 +1295,7 @@ export function createHttp(
|
|
|
1232
1295
|
// Narrowed fields only (never email/orgId/orgName — see login-state.ts).
|
|
1233
1296
|
'/_api/claude/signin-status': async (req: Request) => {
|
|
1234
1297
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1235
|
-
if (!
|
|
1298
|
+
if (!isTrustedRequestHost(req))
|
|
1236
1299
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1237
1300
|
const status = await getClaudeAuthStatus();
|
|
1238
1301
|
return Response.json(
|
|
@@ -1250,7 +1313,7 @@ export function createHttp(
|
|
|
1250
1313
|
// chat-open` driver omits Origin (allowed); a browser drive-by can't forge it.
|
|
1251
1314
|
if (!sameOriginWrite(req))
|
|
1252
1315
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1253
|
-
if (!
|
|
1316
|
+
if (!isTrustedRequestHost(req))
|
|
1254
1317
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1255
1318
|
ctx.bus.emit('acp-focus', {});
|
|
1256
1319
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -1276,7 +1339,7 @@ export function createHttp(
|
|
|
1276
1339
|
if (req.method === 'PATCH') {
|
|
1277
1340
|
if (!sameOriginWrite(req))
|
|
1278
1341
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1279
|
-
if (!
|
|
1342
|
+
if (!isTrustedRequestHost(req))
|
|
1280
1343
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1281
1344
|
if (!id) return new Response('missing id', { status: 400 });
|
|
1282
1345
|
let body: unknown;
|
|
@@ -1337,7 +1400,7 @@ export function createHttp(
|
|
|
1337
1400
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1338
1401
|
if (!sameOriginWrite(req))
|
|
1339
1402
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1340
|
-
if (!
|
|
1403
|
+
if (!isTrustedRequestHost(req))
|
|
1341
1404
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1342
1405
|
const declared = Number(req.headers.get('content-length') || '0');
|
|
1343
1406
|
if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
|
|
@@ -1444,7 +1507,7 @@ export function createHttp(
|
|
|
1444
1507
|
// /_api/acp/status (a drive-by page must not read logs via DNS rebinding).
|
|
1445
1508
|
'/_api/debug-bundle': (req: Request) => {
|
|
1446
1509
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1447
|
-
if (!
|
|
1510
|
+
if (!isTrustedRequestHost(req))
|
|
1448
1511
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1449
1512
|
return Response.json(
|
|
1450
1513
|
buildDebugBundle({
|
|
@@ -1477,7 +1540,7 @@ export function createHttp(
|
|
|
1477
1540
|
'/_api/shell-shot': async (req: Request) => {
|
|
1478
1541
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1479
1542
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1480
|
-
if (!
|
|
1543
|
+
if (!isTrustedRequestHost(req))
|
|
1481
1544
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1482
1545
|
// Target the very server handling this request — no _server.json read, so
|
|
1483
1546
|
// a stale/absent state file can't point the capture at another instance.
|
|
@@ -1547,7 +1610,7 @@ export function createHttp(
|
|
|
1547
1610
|
'/_api/report': async (req: Request) => {
|
|
1548
1611
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1549
1612
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1550
|
-
if (!
|
|
1613
|
+
if (!isTrustedRequestHost(req))
|
|
1551
1614
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1552
1615
|
const endpoint = process.env.MAUDE_REPORT_URL || 'https://cloud.maude.sh/report';
|
|
1553
1616
|
try {
|
|
@@ -1580,7 +1643,7 @@ export function createHttp(
|
|
|
1580
1643
|
'/_api/report-fallback': async (req: Request) => {
|
|
1581
1644
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1582
1645
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1583
|
-
if (!
|
|
1646
|
+
if (!isTrustedRequestHost(req))
|
|
1584
1647
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1585
1648
|
const body = await readJson<{ report?: unknown; screenshots?: unknown }>(req);
|
|
1586
1649
|
if (!body || typeof body.report !== 'object' || body.report === null) {
|
|
@@ -1681,7 +1744,7 @@ export function createHttp(
|
|
|
1681
1744
|
// the photo-store cap stack (sha8 validate + containment + validatePhotoEdit
|
|
1682
1745
|
// + size cap) is the load-bearing mitigation. loopback-Host gates DNS-
|
|
1683
1746
|
// rebinding on every method.
|
|
1684
|
-
if (!
|
|
1747
|
+
if (!isTrustedRequestHost(req))
|
|
1685
1748
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1686
1749
|
const asset = new URL(req.url).searchParams.get('asset');
|
|
1687
1750
|
if (req.method === 'GET') {
|
|
@@ -1719,7 +1782,7 @@ export function createHttp(
|
|
|
1719
1782
|
// DNS-rebinding on every method; the footage-store cap stack (sha8/slug
|
|
1720
1783
|
// validate + containment + validate{FootageAnalysis,Edl} + size cap) is the
|
|
1721
1784
|
// load-bearing mitigation for the caller-derived write path.
|
|
1722
|
-
if (!
|
|
1785
|
+
if (!isTrustedRequestHost(req))
|
|
1723
1786
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1724
1787
|
const params = new URL(req.url).searchParams;
|
|
1725
1788
|
const asset = params.get('asset');
|
|
@@ -1762,7 +1825,7 @@ export function createHttp(
|
|
|
1762
1825
|
// a rebound page shares this origin; the dev-server always binds 127.0.0.1,
|
|
1763
1826
|
// so a real browser (Host: localhost:<port>) passes and only a rebound
|
|
1764
1827
|
// foreign hostname 403s. (No sameOriginWrite here — GET.)
|
|
1765
|
-
if (!
|
|
1828
|
+
if (!isTrustedRequestHost(req))
|
|
1766
1829
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1767
1830
|
const file = new URL(req.url).searchParams.get('file');
|
|
1768
1831
|
const r = await api.loadCanvasSource(file);
|
|
@@ -1827,7 +1890,7 @@ export function createHttp(
|
|
|
1827
1890
|
// slash-command driver omits Origin (→ allowed); a browser drive-by can't.
|
|
1828
1891
|
if (!sameOriginWrite(req))
|
|
1829
1892
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1830
|
-
if (!
|
|
1893
|
+
if (!isTrustedRequestHost(req))
|
|
1831
1894
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1832
1895
|
const body = await readJson<{ file?: string; author?: string }>(req);
|
|
1833
1896
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1847,7 +1910,7 @@ export function createHttp(
|
|
|
1847
1910
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1848
1911
|
if (!sameOriginWrite(req))
|
|
1849
1912
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1850
|
-
if (!
|
|
1913
|
+
if (!isTrustedRequestHost(req))
|
|
1851
1914
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1852
1915
|
const body = await readJson<{ file?: string }>(req);
|
|
1853
1916
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1863,7 +1926,7 @@ export function createHttp(
|
|
|
1863
1926
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1864
1927
|
if (!sameOriginWrite(req))
|
|
1865
1928
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1866
|
-
if (!
|
|
1929
|
+
if (!isTrustedRequestHost(req))
|
|
1867
1930
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1868
1931
|
const body = await readJson<{ file?: string }>(req);
|
|
1869
1932
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1924,7 +1987,7 @@ export function createHttp(
|
|
|
1924
1987
|
if (req.method === 'DELETE' || req.method === 'POST') {
|
|
1925
1988
|
if (!sameOriginWrite(req))
|
|
1926
1989
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1927
|
-
if (!
|
|
1990
|
+
if (!isTrustedRequestHost(req))
|
|
1928
1991
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1929
1992
|
}
|
|
1930
1993
|
if (req.method === 'DELETE') {
|
|
@@ -1987,7 +2050,7 @@ export function createHttp(
|
|
|
1987
2050
|
'/_api/fs-move': async (req: Request) => {
|
|
1988
2051
|
if (!sameOriginWrite(req))
|
|
1989
2052
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1990
|
-
if (!
|
|
2053
|
+
if (!isTrustedRequestHost(req))
|
|
1991
2054
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1992
2055
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1993
2056
|
const body = await readJson<{ file?: unknown; toDir?: unknown }>(req, 4 * 1024);
|
|
@@ -2018,7 +2081,7 @@ export function createHttp(
|
|
|
2018
2081
|
'/_api/fs-mkdir': async (req: Request) => {
|
|
2019
2082
|
if (!sameOriginWrite(req))
|
|
2020
2083
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2021
|
-
if (!
|
|
2084
|
+
if (!isTrustedRequestHost(req))
|
|
2022
2085
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2023
2086
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2024
2087
|
const body = await readJson<{ parent?: unknown; name?: unknown }>(req, 4 * 1024);
|
|
@@ -2079,7 +2142,7 @@ export function createHttp(
|
|
|
2079
2142
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2080
2143
|
if (!sameOriginWrite(req))
|
|
2081
2144
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2082
|
-
if (!
|
|
2145
|
+
if (!isTrustedRequestHost(req))
|
|
2083
2146
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2084
2147
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2085
2148
|
return gitJson(await gitApi.createBranch(body));
|
|
@@ -2089,7 +2152,7 @@ export function createHttp(
|
|
|
2089
2152
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2090
2153
|
if (!sameOriginWrite(req))
|
|
2091
2154
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2092
|
-
if (!
|
|
2155
|
+
if (!isTrustedRequestHost(req))
|
|
2093
2156
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2094
2157
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2095
2158
|
return gitJson(await gitApi.checkout(body));
|
|
@@ -2101,7 +2164,7 @@ export function createHttp(
|
|
|
2101
2164
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2102
2165
|
if (!sameOriginWrite(req))
|
|
2103
2166
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2104
|
-
if (!
|
|
2167
|
+
if (!isTrustedRequestHost(req))
|
|
2105
2168
|
return new Response('adding a draft requires a local request', { status: 403 });
|
|
2106
2169
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2107
2170
|
return gitJson(await gitApi.fold(body));
|
|
@@ -2113,7 +2176,7 @@ export function createHttp(
|
|
|
2113
2176
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2114
2177
|
if (!sameOriginWrite(req))
|
|
2115
2178
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2116
|
-
if (!
|
|
2179
|
+
if (!isTrustedRequestHost(req))
|
|
2117
2180
|
return new Response('refresh requires a local request', { status: 403 });
|
|
2118
2181
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2119
2182
|
return gitJson(await gitApi.fetchRemote(body));
|
|
@@ -2123,7 +2186,7 @@ export function createHttp(
|
|
|
2123
2186
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2124
2187
|
if (!sameOriginWrite(req))
|
|
2125
2188
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2126
|
-
if (!
|
|
2189
|
+
if (!isTrustedRequestHost(req))
|
|
2127
2190
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2128
2191
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2129
2192
|
return gitJson(await gitApi.commit(body));
|
|
@@ -2133,7 +2196,7 @@ export function createHttp(
|
|
|
2133
2196
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2134
2197
|
if (!sameOriginWrite(req))
|
|
2135
2198
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2136
|
-
if (!
|
|
2199
|
+
if (!isTrustedRequestHost(req))
|
|
2137
2200
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2138
2201
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2139
2202
|
return gitJson(await gitApi.discard(body));
|
|
@@ -2143,11 +2206,11 @@ export function createHttp(
|
|
|
2143
2206
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2144
2207
|
if (!sameOriginWrite(req))
|
|
2145
2208
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2146
|
-
if (!
|
|
2209
|
+
if (!isTrustedRequestHost(req))
|
|
2147
2210
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2148
2211
|
// Token-bearing: refuse anything not from a loopback Host (the server binds
|
|
2149
2212
|
// 127.0.0.1, so this is belt-and-suspenders against a forwarded/rebound Host).
|
|
2150
|
-
if (!
|
|
2213
|
+
if (!isTrustedRequestHost(req))
|
|
2151
2214
|
return new Response('publish requires a local request', { status: 403 });
|
|
2152
2215
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2153
2216
|
return gitJson(await gitApi.push(body));
|
|
@@ -2157,7 +2220,7 @@ export function createHttp(
|
|
|
2157
2220
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2158
2221
|
if (!sameOriginWrite(req))
|
|
2159
2222
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2160
|
-
if (!
|
|
2223
|
+
if (!isTrustedRequestHost(req))
|
|
2161
2224
|
return new Response('get latest requires a local request', { status: 403 });
|
|
2162
2225
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2163
2226
|
return gitJson(await gitApi.pull(body));
|
|
@@ -2171,7 +2234,7 @@ export function createHttp(
|
|
|
2171
2234
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2172
2235
|
if (!sameOriginWrite(req))
|
|
2173
2236
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2174
|
-
if (!
|
|
2237
|
+
if (!isTrustedRequestHost(req))
|
|
2175
2238
|
return new Response('resolve requires a local request', { status: 403 });
|
|
2176
2239
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2177
2240
|
return gitJson(await gitApi.resolve(body));
|
|
@@ -2192,7 +2255,7 @@ export function createHttp(
|
|
|
2192
2255
|
// whose 401 DELETES the stored credential — a confused deputy worth
|
|
2193
2256
|
// closing (validate 2026-07-30, defender F6).
|
|
2194
2257
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2195
|
-
if (!
|
|
2258
|
+
if (!isTrustedRequestHost(req))
|
|
2196
2259
|
return new Response('local request required', { status: 403 });
|
|
2197
2260
|
return gitJson(cloudApi.status());
|
|
2198
2261
|
},
|
|
@@ -2200,7 +2263,7 @@ export function createHttp(
|
|
|
2200
2263
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2201
2264
|
if (!sameOriginWrite(req))
|
|
2202
2265
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2203
|
-
if (!
|
|
2266
|
+
if (!isTrustedRequestHost(req))
|
|
2204
2267
|
return new Response('local request required', { status: 403 });
|
|
2205
2268
|
return gitJson(await cloudApi.signinStart());
|
|
2206
2269
|
},
|
|
@@ -2208,7 +2271,7 @@ export function createHttp(
|
|
|
2208
2271
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2209
2272
|
if (!sameOriginWrite(req))
|
|
2210
2273
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2211
|
-
if (!
|
|
2274
|
+
if (!isTrustedRequestHost(req))
|
|
2212
2275
|
return new Response('local request required', { status: 403 });
|
|
2213
2276
|
const body = (await req.json().catch(() => ({}))) as { deviceCode?: string };
|
|
2214
2277
|
return gitJson(await cloudApi.signinPoll(String(body.deviceCode ?? '')));
|
|
@@ -2217,14 +2280,14 @@ export function createHttp(
|
|
|
2217
2280
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2218
2281
|
if (!sameOriginWrite(req))
|
|
2219
2282
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2220
|
-
if (!
|
|
2283
|
+
if (!isTrustedRequestHost(req))
|
|
2221
2284
|
return new Response('local request required', { status: 403 });
|
|
2222
2285
|
return gitJson(cloudApi.signout());
|
|
2223
2286
|
},
|
|
2224
2287
|
'/_api/cloud/projects': async (req: Request) => {
|
|
2225
2288
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2226
2289
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2227
|
-
if (!
|
|
2290
|
+
if (!isTrustedRequestHost(req))
|
|
2228
2291
|
return new Response('local request required', { status: 403 });
|
|
2229
2292
|
return gitJson(await cloudApi.projects());
|
|
2230
2293
|
},
|
|
@@ -2232,7 +2295,7 @@ export function createHttp(
|
|
|
2232
2295
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2233
2296
|
if (!sameOriginWrite(req))
|
|
2234
2297
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2235
|
-
if (!
|
|
2298
|
+
if (!isTrustedRequestHost(req))
|
|
2236
2299
|
return new Response('local request required', { status: 403 });
|
|
2237
2300
|
const body = (await req.json().catch(() => ({}))) as { project?: string };
|
|
2238
2301
|
return gitJson(await cloudApi.attach(String(body.project ?? '')));
|
|
@@ -2244,7 +2307,7 @@ export function createHttp(
|
|
|
2244
2307
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2245
2308
|
if (!sameOriginWrite(req))
|
|
2246
2309
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2247
|
-
if (!
|
|
2310
|
+
if (!isTrustedRequestHost(req))
|
|
2248
2311
|
return new Response('local request required', { status: 403 });
|
|
2249
2312
|
const body = (await req.json().catch(() => ({}))) as { code?: string; project?: string };
|
|
2250
2313
|
return gitJson(
|
|
@@ -2257,14 +2320,14 @@ export function createHttp(
|
|
|
2257
2320
|
|
|
2258
2321
|
'/_api/github/identity': async (req: Request) => {
|
|
2259
2322
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2260
|
-
if (!
|
|
2323
|
+
if (!isTrustedRequestHost(req))
|
|
2261
2324
|
return new Response('local request required', { status: 403 });
|
|
2262
2325
|
return gitJson(await githubApi.identity());
|
|
2263
2326
|
},
|
|
2264
2327
|
|
|
2265
2328
|
'/_api/github/repos': async (req: Request) => {
|
|
2266
2329
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2267
|
-
if (!
|
|
2330
|
+
if (!isTrustedRequestHost(req))
|
|
2268
2331
|
return new Response('local request required', { status: 403 });
|
|
2269
2332
|
return gitJson(await githubApi.repos());
|
|
2270
2333
|
},
|
|
@@ -2273,7 +2336,7 @@ export function createHttp(
|
|
|
2273
2336
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2274
2337
|
if (!sameOriginWrite(req))
|
|
2275
2338
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2276
|
-
if (!
|
|
2339
|
+
if (!isTrustedRequestHost(req))
|
|
2277
2340
|
return new Response('local request required', { status: 403 });
|
|
2278
2341
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2279
2342
|
return gitJson(await githubApi.createRepo(body));
|
|
@@ -2283,7 +2346,7 @@ export function createHttp(
|
|
|
2283
2346
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2284
2347
|
if (!sameOriginWrite(req))
|
|
2285
2348
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2286
|
-
if (!
|
|
2349
|
+
if (!isTrustedRequestHost(req))
|
|
2287
2350
|
return new Response('local request required', { status: 403 });
|
|
2288
2351
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2289
2352
|
return gitJson(await githubApi.invite(body));
|
|
@@ -2293,7 +2356,7 @@ export function createHttp(
|
|
|
2293
2356
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2294
2357
|
if (!sameOriginWrite(req))
|
|
2295
2358
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2296
|
-
if (!
|
|
2359
|
+
if (!isTrustedRequestHost(req))
|
|
2297
2360
|
return new Response('local request required', { status: 403 });
|
|
2298
2361
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2299
2362
|
return gitJson(await githubApi.clone(body));
|
|
@@ -2303,7 +2366,7 @@ export function createHttp(
|
|
|
2303
2366
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2304
2367
|
if (!sameOriginWrite(req))
|
|
2305
2368
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2306
|
-
if (!
|
|
2369
|
+
if (!isTrustedRequestHost(req))
|
|
2307
2370
|
return new Response('local request required', { status: 403 });
|
|
2308
2371
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2309
2372
|
return gitJson(await githubApi.createProject(body));
|
|
@@ -2318,7 +2381,7 @@ export function createHttp(
|
|
|
2318
2381
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2319
2382
|
if (!sameOriginWrite(req))
|
|
2320
2383
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2321
|
-
if (!
|
|
2384
|
+
if (!isTrustedRequestHost(req))
|
|
2322
2385
|
return new Response('local request required', { status: 403 });
|
|
2323
2386
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2324
2387
|
return gitJson(await githubApi.createLocalProject(body));
|
|
@@ -2331,7 +2394,7 @@ export function createHttp(
|
|
|
2331
2394
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2332
2395
|
if (!sameOriginWrite(req))
|
|
2333
2396
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2334
|
-
if (!
|
|
2397
|
+
if (!isTrustedRequestHost(req))
|
|
2335
2398
|
return new Response('local request required', { status: 403 });
|
|
2336
2399
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2337
2400
|
return gitJson(await githubApi.initDesign(body));
|
|
@@ -2346,7 +2409,7 @@ export function createHttp(
|
|
|
2346
2409
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2347
2410
|
if (!sameOriginWrite(req))
|
|
2348
2411
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2349
|
-
if (!
|
|
2412
|
+
if (!isTrustedRequestHost(req))
|
|
2350
2413
|
return new Response('local request required', { status: 403 });
|
|
2351
2414
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2352
2415
|
return gitJson(await linkHub(body));
|
|
@@ -2361,7 +2424,7 @@ export function createHttp(
|
|
|
2361
2424
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2362
2425
|
if (!sameOriginWrite(req))
|
|
2363
2426
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2364
|
-
if (!
|
|
2427
|
+
if (!isTrustedRequestHost(req))
|
|
2365
2428
|
return new Response('local request required', { status: 403 });
|
|
2366
2429
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2367
2430
|
const result = await signInToWorkspace((body ?? {}) as Record<string, unknown>);
|
|
@@ -2377,7 +2440,7 @@ export function createHttp(
|
|
|
2377
2440
|
// for, and every route the canvas can reach is one more thing to reason about.
|
|
2378
2441
|
'/_api/workspace/disclosure': async (req: Request) => {
|
|
2379
2442
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2380
|
-
if (!
|
|
2443
|
+
if (!isTrustedRequestHost(req))
|
|
2381
2444
|
return new Response('local request required', { status: 403 });
|
|
2382
2445
|
const url = new URL(req.url);
|
|
2383
2446
|
const linked = ctx.cfg.linkedHub?.url ?? null;
|
|
@@ -2407,7 +2470,7 @@ export function createHttp(
|
|
|
2407
2470
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2408
2471
|
if (!sameOriginWrite(req))
|
|
2409
2472
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2410
|
-
if (!
|
|
2473
|
+
if (!isTrustedRequestHost(req))
|
|
2411
2474
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2412
2475
|
const body = await readJson<{
|
|
2413
2476
|
canvas?: unknown;
|
|
@@ -2440,7 +2503,7 @@ export function createHttp(
|
|
|
2440
2503
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2441
2504
|
if (!sameOriginWrite(req))
|
|
2442
2505
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2443
|
-
if (!
|
|
2506
|
+
if (!isTrustedRequestHost(req))
|
|
2444
2507
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2445
2508
|
const body = await readJson<{
|
|
2446
2509
|
canvas?: unknown;
|
|
@@ -2473,7 +2536,7 @@ export function createHttp(
|
|
|
2473
2536
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2474
2537
|
if (!sameOriginWrite(req))
|
|
2475
2538
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2476
|
-
if (!
|
|
2539
|
+
if (!isTrustedRequestHost(req))
|
|
2477
2540
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2478
2541
|
const body = await readJson<{
|
|
2479
2542
|
canvas?: unknown;
|
|
@@ -2509,7 +2572,7 @@ export function createHttp(
|
|
|
2509
2572
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2510
2573
|
if (!sameOriginWrite(req))
|
|
2511
2574
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2512
|
-
if (!
|
|
2575
|
+
if (!isTrustedRequestHost(req))
|
|
2513
2576
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2514
2577
|
const body = await readJson<{
|
|
2515
2578
|
canvas?: unknown;
|
|
@@ -2549,7 +2612,7 @@ export function createHttp(
|
|
|
2549
2612
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2550
2613
|
if (!sameOriginWrite(req))
|
|
2551
2614
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2552
|
-
if (!
|
|
2615
|
+
if (!isTrustedRequestHost(req))
|
|
2553
2616
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2554
2617
|
const body = await readJson<{
|
|
2555
2618
|
canvas?: unknown;
|
|
@@ -2585,7 +2648,7 @@ export function createHttp(
|
|
|
2585
2648
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2586
2649
|
if (!sameOriginWrite(req))
|
|
2587
2650
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2588
|
-
if (!
|
|
2651
|
+
if (!isTrustedRequestHost(req))
|
|
2589
2652
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2590
2653
|
const body = await readJson<{
|
|
2591
2654
|
canvas?: unknown;
|
|
@@ -2630,7 +2693,7 @@ export function createHttp(
|
|
|
2630
2693
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2631
2694
|
if (!sameOriginWrite(req))
|
|
2632
2695
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2633
|
-
if (!
|
|
2696
|
+
if (!isTrustedRequestHost(req))
|
|
2634
2697
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2635
2698
|
const body = await readJson<{
|
|
2636
2699
|
canvas?: unknown;
|
|
@@ -2660,7 +2723,7 @@ export function createHttp(
|
|
|
2660
2723
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2661
2724
|
if (!sameOriginWrite(req))
|
|
2662
2725
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2663
|
-
if (!
|
|
2726
|
+
if (!isTrustedRequestHost(req))
|
|
2664
2727
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2665
2728
|
const body = await readJson<{
|
|
2666
2729
|
canvas?: unknown;
|
|
@@ -2697,7 +2760,7 @@ export function createHttp(
|
|
|
2697
2760
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2698
2761
|
if (!sameOriginWrite(req))
|
|
2699
2762
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2700
|
-
if (!
|
|
2763
|
+
if (!isTrustedRequestHost(req))
|
|
2701
2764
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2702
2765
|
const body = await readJson<{
|
|
2703
2766
|
canvas?: unknown;
|
|
@@ -2730,7 +2793,7 @@ export function createHttp(
|
|
|
2730
2793
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2731
2794
|
if (!sameOriginWrite(req))
|
|
2732
2795
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2733
|
-
if (!
|
|
2796
|
+
if (!isTrustedRequestHost(req))
|
|
2734
2797
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2735
2798
|
const body = await readJson<{
|
|
2736
2799
|
canvas?: unknown;
|
|
@@ -2761,7 +2824,7 @@ export function createHttp(
|
|
|
2761
2824
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2762
2825
|
if (!sameOriginWrite(req))
|
|
2763
2826
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2764
|
-
if (!
|
|
2827
|
+
if (!isTrustedRequestHost(req))
|
|
2765
2828
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2766
2829
|
const body = await readJson<{
|
|
2767
2830
|
canvas?: unknown;
|
|
@@ -2795,7 +2858,7 @@ export function createHttp(
|
|
|
2795
2858
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2796
2859
|
// DNS-rebinding guard — this GET echoes project source structure; loopback-
|
|
2797
2860
|
// only Host (real browser passes, rebound hostname 403s). No sameOriginWrite (GET).
|
|
2798
|
-
if (!
|
|
2861
|
+
if (!isTrustedRequestHost(req))
|
|
2799
2862
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2800
2863
|
const u = new URL(req.url);
|
|
2801
2864
|
const result = await api.compClips({
|
|
@@ -2821,7 +2884,7 @@ export function createHttp(
|
|
|
2821
2884
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2822
2885
|
if (!sameOriginWrite(req))
|
|
2823
2886
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2824
|
-
if (!
|
|
2887
|
+
if (!isTrustedRequestHost(req))
|
|
2825
2888
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2826
2889
|
const body = await readJson<{ canvas?: unknown; seq?: unknown; dir?: unknown }>(
|
|
2827
2890
|
req,
|
|
@@ -2852,7 +2915,7 @@ export function createHttp(
|
|
|
2852
2915
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2853
2916
|
if (!sameOriginWrite(req))
|
|
2854
2917
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2855
|
-
if (!
|
|
2918
|
+
if (!isTrustedRequestHost(req))
|
|
2856
2919
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2857
2920
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2858
2921
|
req,
|
|
@@ -2884,7 +2947,7 @@ export function createHttp(
|
|
|
2884
2947
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2885
2948
|
if (!sameOriginWrite(req))
|
|
2886
2949
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2887
|
-
if (!
|
|
2950
|
+
if (!isTrustedRequestHost(req))
|
|
2888
2951
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2889
2952
|
const body = await readJson<{
|
|
2890
2953
|
canvas?: unknown;
|
|
@@ -2919,7 +2982,7 @@ export function createHttp(
|
|
|
2919
2982
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2920
2983
|
if (!sameOriginWrite(req))
|
|
2921
2984
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2922
|
-
if (!
|
|
2985
|
+
if (!isTrustedRequestHost(req))
|
|
2923
2986
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2924
2987
|
const body = await readJson<{
|
|
2925
2988
|
canvas?: unknown;
|
|
@@ -2953,7 +3016,7 @@ export function createHttp(
|
|
|
2953
3016
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2954
3017
|
if (!sameOriginWrite(req))
|
|
2955
3018
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2956
|
-
if (!
|
|
3019
|
+
if (!isTrustedRequestHost(req))
|
|
2957
3020
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2958
3021
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2959
3022
|
req,
|
|
@@ -2980,7 +3043,7 @@ export function createHttp(
|
|
|
2980
3043
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2981
3044
|
if (!sameOriginWrite(req))
|
|
2982
3045
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2983
|
-
if (!
|
|
3046
|
+
if (!isTrustedRequestHost(req))
|
|
2984
3047
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2985
3048
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2986
3049
|
req,
|
|
@@ -3009,7 +3072,7 @@ export function createHttp(
|
|
|
3009
3072
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3010
3073
|
if (!sameOriginWrite(req))
|
|
3011
3074
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3012
|
-
if (!
|
|
3075
|
+
if (!isTrustedRequestHost(req))
|
|
3013
3076
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3014
3077
|
const body = await readJson<{
|
|
3015
3078
|
canvas?: unknown;
|
|
@@ -3043,7 +3106,7 @@ export function createHttp(
|
|
|
3043
3106
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3044
3107
|
if (!sameOriginWrite(req))
|
|
3045
3108
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3046
|
-
if (!
|
|
3109
|
+
if (!isTrustedRequestHost(req))
|
|
3047
3110
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3048
3111
|
const body = await readJson<{
|
|
3049
3112
|
canvas?: unknown;
|
|
@@ -3072,7 +3135,7 @@ export function createHttp(
|
|
|
3072
3135
|
// + startCanvasServer routes (dual-allowlist, DDR-054). Loopback-Host gated
|
|
3073
3136
|
// (DNS-rebinding); read-only, so no sameOriginWrite (GET).
|
|
3074
3137
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3075
|
-
if (!
|
|
3138
|
+
if (!isTrustedRequestHost(req))
|
|
3076
3139
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3077
3140
|
const r = await api.listAssets();
|
|
3078
3141
|
return Response.json(
|
|
@@ -3090,7 +3153,7 @@ export function createHttp(
|
|
|
3090
3153
|
// routes (dual-allowlist, DDR-054). Loopback-Host gated (DNS-rebinding);
|
|
3091
3154
|
// read-only, so no sameOriginWrite (GET).
|
|
3092
3155
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3093
|
-
if (!
|
|
3156
|
+
if (!isTrustedRequestHost(req))
|
|
3094
3157
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3095
3158
|
const r = await api.listStickers();
|
|
3096
3159
|
return Response.json(
|
|
@@ -3107,7 +3170,7 @@ export function createHttp(
|
|
|
3107
3170
|
// from CANVAS_SAFE_API + startCanvasServer routes (dual-allowlist, DDR-054);
|
|
3108
3171
|
// loopback-Host gated (DNS-rebinding); no sameOriginWrite (GET).
|
|
3109
3172
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3110
|
-
if (!
|
|
3173
|
+
if (!isTrustedRequestHost(req))
|
|
3111
3174
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3112
3175
|
const url = new URL(req.url);
|
|
3113
3176
|
const result = await api.editScopeOp({
|
|
@@ -3131,7 +3194,7 @@ export function createHttp(
|
|
|
3131
3194
|
// CANVAS_SAFE_API + startCanvasServer routes (DDR-054); loopback-Host
|
|
3132
3195
|
// gated; no sameOriginWrite (GET).
|
|
3133
3196
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3134
|
-
if (!
|
|
3197
|
+
if (!isTrustedRequestHost(req))
|
|
3135
3198
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3136
3199
|
const url = new URL(req.url);
|
|
3137
3200
|
const result = await api.componentMapOp({
|
|
@@ -3155,7 +3218,7 @@ export function createHttp(
|
|
|
3155
3218
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3156
3219
|
if (!sameOriginWrite(req))
|
|
3157
3220
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3158
|
-
if (!
|
|
3221
|
+
if (!isTrustedRequestHost(req))
|
|
3159
3222
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3160
3223
|
const body = await readJson<{
|
|
3161
3224
|
canvas?: unknown;
|
|
@@ -3187,7 +3250,7 @@ export function createHttp(
|
|
|
3187
3250
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3188
3251
|
if (!sameOriginWrite(req))
|
|
3189
3252
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3190
|
-
if (!
|
|
3253
|
+
if (!isTrustedRequestHost(req))
|
|
3191
3254
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3192
3255
|
const body = await readJson<{
|
|
3193
3256
|
canvas?: unknown;
|
|
@@ -3218,7 +3281,7 @@ export function createHttp(
|
|
|
3218
3281
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3219
3282
|
if (!sameOriginWrite(req))
|
|
3220
3283
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3221
|
-
if (!
|
|
3284
|
+
if (!isTrustedRequestHost(req))
|
|
3222
3285
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3223
3286
|
const body = await readJson<{
|
|
3224
3287
|
canvas?: unknown;
|
|
@@ -3252,7 +3315,7 @@ export function createHttp(
|
|
|
3252
3315
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3253
3316
|
if (!sameOriginWrite(req))
|
|
3254
3317
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3255
|
-
if (!
|
|
3318
|
+
if (!isTrustedRequestHost(req))
|
|
3256
3319
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3257
3320
|
const body = await readJson<{
|
|
3258
3321
|
canvas?: unknown;
|
|
@@ -3282,7 +3345,7 @@ export function createHttp(
|
|
|
3282
3345
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3283
3346
|
if (!sameOriginWrite(req))
|
|
3284
3347
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3285
|
-
if (!
|
|
3348
|
+
if (!isTrustedRequestHost(req))
|
|
3286
3349
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3287
3350
|
const body = await readJson<{
|
|
3288
3351
|
canvas?: unknown;
|
|
@@ -3313,7 +3376,7 @@ export function createHttp(
|
|
|
3313
3376
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3314
3377
|
if (!sameOriginWrite(req))
|
|
3315
3378
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3316
|
-
if (!
|
|
3379
|
+
if (!isTrustedRequestHost(req))
|
|
3317
3380
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3318
3381
|
const body = await readJson<{
|
|
3319
3382
|
canvas?: unknown;
|
|
@@ -3342,7 +3405,7 @@ export function createHttp(
|
|
|
3342
3405
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3343
3406
|
if (!sameOriginWrite(req))
|
|
3344
3407
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3345
|
-
if (!
|
|
3408
|
+
if (!isTrustedRequestHost(req))
|
|
3346
3409
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3347
3410
|
const body = await readJson<{ canvas?: unknown; artboardId?: unknown }>(req, 8 * 1024);
|
|
3348
3411
|
if (!body) return new Response('body required', { status: 400 });
|
|
@@ -3410,7 +3473,7 @@ export function createHttp(
|
|
|
3410
3473
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3411
3474
|
if (!sameOriginWrite(req))
|
|
3412
3475
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3413
|
-
if (!
|
|
3476
|
+
if (!isTrustedRequestHost(req))
|
|
3414
3477
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3415
3478
|
const kind = (req.headers.get('x-import-kind') || 'svg').toLowerCase();
|
|
3416
3479
|
if (kind !== 'svg') {
|
|
@@ -3477,7 +3540,7 @@ export function createHttp(
|
|
|
3477
3540
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3478
3541
|
if (!sameOriginWrite(req))
|
|
3479
3542
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3480
|
-
if (!
|
|
3543
|
+
if (!isTrustedRequestHost(req))
|
|
3481
3544
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3482
3545
|
let body: { assetPath?: unknown };
|
|
3483
3546
|
try {
|
|
@@ -3551,7 +3614,7 @@ export function createHttp(
|
|
|
3551
3614
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3552
3615
|
if (!sameOriginWrite(req))
|
|
3553
3616
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3554
|
-
if (!
|
|
3617
|
+
if (!isTrustedRequestHost(req))
|
|
3555
3618
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3556
3619
|
const body = await readJson<{
|
|
3557
3620
|
format?: unknown;
|
|
@@ -3595,7 +3658,7 @@ export function createHttp(
|
|
|
3595
3658
|
// /_api/export today — DDR-060). loopback-Host gated on every method;
|
|
3596
3659
|
// the mutating POST is additionally sameOriginWrite CSRF gated (same
|
|
3597
3660
|
// /flow:done security fan-out fix as /_api/export above).
|
|
3598
|
-
if (!
|
|
3661
|
+
if (!isTrustedRequestHost(req))
|
|
3599
3662
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3600
3663
|
if (req.method === 'GET') {
|
|
3601
3664
|
return Response.json(
|
|
@@ -3643,7 +3706,7 @@ export function createHttp(
|
|
|
3643
3706
|
// unguessable job-scoped UUID plus this guard closes the DNS-rebinding
|
|
3644
3707
|
// angle the /flow:done security fan-out checked for).
|
|
3645
3708
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3646
|
-
if (!
|
|
3709
|
+
if (!isTrustedRequestHost(req))
|
|
3647
3710
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3648
3711
|
const id = new URL(req.url).searchParams.get('id');
|
|
3649
3712
|
if (!id) return new Response('id query param required', { status: 400 });
|
|
@@ -3671,7 +3734,7 @@ export function createHttp(
|
|
|
3671
3734
|
// loopback-Host gated on every method; the mutating POST is additionally
|
|
3672
3735
|
// sameOriginWrite CSRF-gated. See DDR-16x + the canvas-origin-gate test.
|
|
3673
3736
|
'/_api/generate-jobs': async (req: Request) => {
|
|
3674
|
-
if (!
|
|
3737
|
+
if (!isTrustedRequestHost(req))
|
|
3675
3738
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3676
3739
|
if (req.method === 'GET') {
|
|
3677
3740
|
return Response.json(
|
|
@@ -3790,7 +3853,7 @@ export function createHttp(
|
|
|
3790
3853
|
// keyUrl) + presence-only `configured` flags. NO secret ever crosses here.
|
|
3791
3854
|
// MAIN-ORIGIN ONLY, loopback-gated.
|
|
3792
3855
|
'/_api/generate/providers': async (req: Request) => {
|
|
3793
|
-
if (!
|
|
3856
|
+
if (!isTrustedRequestHost(req))
|
|
3794
3857
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3795
3858
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3796
3859
|
const configured = new Set(configuredProviders());
|
|
@@ -3807,7 +3870,7 @@ export function createHttp(
|
|
|
3807
3870
|
// ({configured:[...]}) — a key value is NEVER echoed back (mirrors
|
|
3808
3871
|
// github_is_signed_in). MAIN-ORIGIN ONLY, loopback + sameOriginWrite gated.
|
|
3809
3872
|
'/_api/generate/keys': async (req: Request) => {
|
|
3810
|
-
if (!
|
|
3873
|
+
if (!isTrustedRequestHost(req))
|
|
3811
3874
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3812
3875
|
if (req.method === 'GET') {
|
|
3813
3876
|
return Response.json(
|
|
@@ -3851,7 +3914,7 @@ export function createHttp(
|
|
|
3851
3914
|
// ONLY, loopback + sameOriginWrite gated. NEVER touches a key — that's the
|
|
3852
3915
|
// separate /_api/generate/keys route.
|
|
3853
3916
|
'/_api/generate/prefs': async (req: Request) => {
|
|
3854
|
-
if (!
|
|
3917
|
+
if (!isTrustedRequestHost(req))
|
|
3855
3918
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3856
3919
|
if (req.method === 'GET') {
|
|
3857
3920
|
return Response.json(
|
|
@@ -3884,7 +3947,7 @@ export function createHttp(
|
|
|
3884
3947
|
}
|
|
3885
3948
|
const provider = body?.transcriptionProvider;
|
|
3886
3949
|
if (!isTranscriptionProvider(provider))
|
|
3887
|
-
return new Response('transcriptionProvider must be whisper|elevenlabs|groq', {
|
|
3950
|
+
return new Response('transcriptionProvider must be auto|whisper|elevenlabs|groq', {
|
|
3888
3951
|
status: 400,
|
|
3889
3952
|
});
|
|
3890
3953
|
await writeTranscriptionProvider(ctx.paths.repoRoot, provider);
|
|
@@ -3906,7 +3969,7 @@ export function createHttp(
|
|
|
3906
3969
|
// prefs; POST merge-patches only the provided keys. MAIN-ORIGIN ONLY, loopback
|
|
3907
3970
|
// + sameOriginWrite gated (never reachable from the untrusted canvas origin).
|
|
3908
3971
|
'/_api/ui-prefs': async (req: Request) => {
|
|
3909
|
-
if (!
|
|
3972
|
+
if (!isTrustedRequestHost(req))
|
|
3910
3973
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3911
3974
|
if (req.method === 'GET') {
|
|
3912
3975
|
return Response.json(readUiPrefs(), { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -3963,7 +4026,7 @@ export function createHttp(
|
|
|
3963
4026
|
// already paid), returning ranked reuse candidates. MAIN-ORIGIN ONLY,
|
|
3964
4027
|
// loopback-gated (it resolves the provider key server-side).
|
|
3965
4028
|
'/_api/generate/audio-search': async (req: Request) => {
|
|
3966
|
-
if (!
|
|
4029
|
+
if (!isTrustedRequestHost(req))
|
|
3967
4030
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3968
4031
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3969
4032
|
// F1 (ethical-hacker) — this GET resolves the user's key + fans out to the
|
|
@@ -4011,7 +4074,7 @@ export function createHttp(
|
|
|
4011
4074
|
// reused track is itself searchable. MAIN-ORIGIN ONLY, loopback +
|
|
4012
4075
|
// sameOriginWrite gated.
|
|
4013
4076
|
'/_api/generate/audio-reuse': async (req: Request) => {
|
|
4014
|
-
if (!
|
|
4077
|
+
if (!isTrustedRequestHost(req))
|
|
4015
4078
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4016
4079
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4017
4080
|
if (!sameOriginWrite(req))
|
|
@@ -4051,11 +4114,21 @@ export function createHttp(
|
|
|
4051
4114
|
// `maude design transcribe --provider whisper` auto-resolves it. MAIN-ORIGIN
|
|
4052
4115
|
// ONLY, loopback + sameOriginWrite gated.
|
|
4053
4116
|
'/_api/generate/whisper-model': async (req: Request) => {
|
|
4054
|
-
if (!
|
|
4117
|
+
if (!isTrustedRequestHost(req))
|
|
4055
4118
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4056
4119
|
if (req.method === 'GET') {
|
|
4120
|
+
// `setup` tells the card whether the ENGINE is actually installed and
|
|
4121
|
+
// how to get it on THIS machine (no `brew install` without Homebrew);
|
|
4122
|
+
// `auto` reports what the automatic engine choice currently resolves
|
|
4123
|
+
// to, so the card can state it out loud rather than switch silently.
|
|
4124
|
+
const setup = whisperSetup();
|
|
4057
4125
|
return Response.json(
|
|
4058
|
-
{
|
|
4126
|
+
{
|
|
4127
|
+
models: listWhisperModels(),
|
|
4128
|
+
downloading: whisperDownload,
|
|
4129
|
+
setup,
|
|
4130
|
+
auto: resolveAutoEngine(configuredProviders(), setup.installed),
|
|
4131
|
+
},
|
|
4059
4132
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4060
4133
|
);
|
|
4061
4134
|
}
|
|
@@ -4114,7 +4187,7 @@ export function createHttp(
|
|
|
4114
4187
|
// which tier will run; POST {id} downloads one via huggingface_hub (gated on
|
|
4115
4188
|
// mlx-vlm being installed — the model is useless without it). MAIN-ORIGIN ONLY.
|
|
4116
4189
|
'/_api/generate/keyframe-model': async (req: Request) => {
|
|
4117
|
-
if (!
|
|
4190
|
+
if (!isTrustedRequestHost(req))
|
|
4118
4191
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4119
4192
|
if (req.method === 'GET') {
|
|
4120
4193
|
// The GET builds availability by spawning subprocesses (import mlx_vlm is
|
|
@@ -4124,12 +4197,28 @@ export function createHttp(
|
|
|
4124
4197
|
// are also TTL-cached in gemma-models.ts as defence-in-depth.
|
|
4125
4198
|
if (!sameOriginRead(req))
|
|
4126
4199
|
return new Response('cross-origin read rejected', { status: 403 });
|
|
4200
|
+
const ollama = await ollamaStatus();
|
|
4127
4201
|
return Response.json(
|
|
4128
4202
|
{
|
|
4129
|
-
models
|
|
4203
|
+
// Both runtimes' models in one list, each tagged with the runtime
|
|
4204
|
+
// it needs — the card enables the button for whichever runtime this
|
|
4205
|
+
// machine actually has, instead of a dead "Needs mlx-vlm".
|
|
4206
|
+
models: listScoutModels(ollama),
|
|
4130
4207
|
downloading: keyframeDownload,
|
|
4131
4208
|
mlxVlmAvailable: mlxVlmAvailable(),
|
|
4132
4209
|
ffmpegAvailable: ffmpegAvailable(),
|
|
4210
|
+
// Setup routes that actually work on THIS machine, best first — no
|
|
4211
|
+
// `brew install` without Homebrew, no venv one-liner without
|
|
4212
|
+
// python3, and "start it" rather than "install it" when the binary
|
|
4213
|
+
// is already there. Computed from the SAME paths the probes check,
|
|
4214
|
+
// so the shown command and the detection can't drift.
|
|
4215
|
+
mlx: mlxSetup(),
|
|
4216
|
+
ollama: {
|
|
4217
|
+
...ollama,
|
|
4218
|
+
recommendedModel: OLLAMA_RECOMMENDED_MODEL,
|
|
4219
|
+
pullCommand: `ollama pull ${OLLAMA_RECOMMENDED_MODEL}`,
|
|
4220
|
+
setup: ollamaSetupOptions(ollama),
|
|
4221
|
+
},
|
|
4133
4222
|
},
|
|
4134
4223
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4135
4224
|
);
|
|
@@ -4145,23 +4234,34 @@ export function createHttp(
|
|
|
4145
4234
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4146
4235
|
if (!sameOriginWrite(req))
|
|
4147
4236
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
4148
|
-
|
|
4237
|
+
const body = await readJson<{ id?: unknown }>(req, 4 * 1024);
|
|
4238
|
+
const id = typeof body?.id === 'string' ? body.id : '';
|
|
4239
|
+
// One route, two runtimes: an `ollama:` id pulls through the local Ollama
|
|
4240
|
+
// server, anything else is an mlx HF snapshot. Each gates on ITS OWN
|
|
4241
|
+
// runtime — an Ollama user must not be blocked by a missing mlx-vlm.
|
|
4242
|
+
const isOllama = Boolean(getOllamaModel(id));
|
|
4243
|
+
if (!isOllama && !listGemmaModels().some((m) => m.id === id))
|
|
4244
|
+
return new Response('unknown model id', { status: 400 });
|
|
4245
|
+
if (isOllama) {
|
|
4246
|
+
const status = await ollamaStatus();
|
|
4247
|
+
if (!status.available)
|
|
4248
|
+
return new Response('Ollama is not running — start it (`ollama serve`) and try again.', {
|
|
4249
|
+
status: 400,
|
|
4250
|
+
});
|
|
4251
|
+
} else if (!mlxVlmAvailable())
|
|
4149
4252
|
return new Response(
|
|
4150
|
-
'mlx-vlm not installed — the
|
|
4253
|
+
'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.)',
|
|
4151
4254
|
{
|
|
4152
4255
|
status: 400,
|
|
4153
4256
|
}
|
|
4154
4257
|
);
|
|
4155
|
-
const body = await readJson<{ id?: unknown }>(req, 4 * 1024);
|
|
4156
|
-
const id = typeof body?.id === 'string' ? body.id : '';
|
|
4157
|
-
if (!listGemmaModels().some((m) => m.id === id))
|
|
4158
|
-
return new Response('unknown model id', { status: 400 });
|
|
4159
4258
|
if (keyframeDownload && !keyframeDownload.error)
|
|
4160
4259
|
return new Response(`a download is already in progress (${keyframeDownload.id})`, {
|
|
4161
4260
|
status: 409,
|
|
4162
4261
|
});
|
|
4163
4262
|
keyframeDownload = { id, received: 0, total: 0 };
|
|
4164
|
-
|
|
4263
|
+
const run = isOllama ? pullOllamaModel : downloadGemmaModel;
|
|
4264
|
+
void run(
|
|
4165
4265
|
id,
|
|
4166
4266
|
(received, total) => {
|
|
4167
4267
|
if (keyframeDownload && keyframeDownload.id === id) {
|
|
@@ -4199,7 +4299,7 @@ export function createHttp(
|
|
|
4199
4299
|
// dataURLs of local footage → disclosure) or WRITE arbitrary JSON later
|
|
4200
4300
|
// rendered as <img src> in the trusted shell. The loopback-host check is
|
|
4201
4301
|
// the same backstop every sibling privileged route carries.
|
|
4202
|
-
if (!
|
|
4302
|
+
if (!isTrustedRequestHost(req))
|
|
4203
4303
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4204
4304
|
const url = new URL(req.url);
|
|
4205
4305
|
const key = url.searchParams.get('key') ?? '';
|