@1agh/maude 0.55.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/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/client/app.jsx +31 -3
- package/apps/studio/client/panels/CloudBar.jsx +44 -5
- package/apps/studio/client/panels/GitPanel.jsx +38 -25
- package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
- package/apps/studio/client/styles/4-components.css +72 -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 +1464 -1464
- 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 +196 -109
- 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 +95 -0
- 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 +27 -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
|
@@ -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
|
|
@@ -1137,7 +1187,7 @@ export function createHttp(
|
|
|
1137
1187
|
// but newly worth closing given what this diff makes it able to reveal.
|
|
1138
1188
|
'/_api/acp/status': async (req: Request) => {
|
|
1139
1189
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1140
|
-
if (!
|
|
1190
|
+
if (!isTrustedRequestHost(req))
|
|
1141
1191
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1142
1192
|
return Response.json(await probeAcpAvailabilityAuthed(), {
|
|
1143
1193
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1155,7 +1205,7 @@ export function createHttp(
|
|
|
1155
1205
|
// ChatPanel not-connected explainer + the Help-menu modal read this.
|
|
1156
1206
|
'/_api/preflight': async (req: Request) => {
|
|
1157
1207
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1158
|
-
if (!
|
|
1208
|
+
if (!isTrustedRequestHost(req))
|
|
1159
1209
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1160
1210
|
return Response.json(await probeReadiness(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1161
1211
|
},
|
|
@@ -1169,7 +1219,7 @@ export function createHttp(
|
|
|
1169
1219
|
// (SetupChecklist.jsx) polls this.
|
|
1170
1220
|
'/_api/setup-readiness': async (req: Request) => {
|
|
1171
1221
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1172
|
-
if (!
|
|
1222
|
+
if (!isTrustedRequestHost(req))
|
|
1173
1223
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1174
1224
|
return Response.json(await probeSetupReadiness(ctx), {
|
|
1175
1225
|
headers: { 'Cache-Control': 'no-store' },
|
|
@@ -1185,7 +1235,7 @@ export function createHttp(
|
|
|
1185
1235
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1186
1236
|
if (!sameOriginWrite(req))
|
|
1187
1237
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1188
|
-
if (!
|
|
1238
|
+
if (!isTrustedRequestHost(req))
|
|
1189
1239
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1190
1240
|
// Fire-and-forget — the install can run past Bun's default 10s idle
|
|
1191
1241
|
// timeout on a slow network, so this returns as soon as the spawn is
|
|
@@ -1206,7 +1256,7 @@ export function createHttp(
|
|
|
1206
1256
|
// two routes, which used the wrong guard.
|
|
1207
1257
|
'/_api/claude/install-status': (req: Request) => {
|
|
1208
1258
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1209
|
-
if (!
|
|
1259
|
+
if (!isTrustedRequestHost(req))
|
|
1210
1260
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1211
1261
|
return Response.json(getInstallState(), { headers: { 'Cache-Control': 'no-store' } });
|
|
1212
1262
|
},
|
|
@@ -1223,7 +1273,7 @@ export function createHttp(
|
|
|
1223
1273
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1224
1274
|
if (!sameOriginWrite(req))
|
|
1225
1275
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1226
|
-
if (!
|
|
1276
|
+
if (!isTrustedRequestHost(req))
|
|
1227
1277
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1228
1278
|
const result = startSignin();
|
|
1229
1279
|
return Response.json(result, {
|
|
@@ -1235,7 +1285,7 @@ export function createHttp(
|
|
|
1235
1285
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1236
1286
|
if (!sameOriginWrite(req))
|
|
1237
1287
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1238
|
-
if (!
|
|
1288
|
+
if (!isTrustedRequestHost(req))
|
|
1239
1289
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1240
1290
|
cancelSignin();
|
|
1241
1291
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -1245,7 +1295,7 @@ export function createHttp(
|
|
|
1245
1295
|
// Narrowed fields only (never email/orgId/orgName — see login-state.ts).
|
|
1246
1296
|
'/_api/claude/signin-status': async (req: Request) => {
|
|
1247
1297
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1248
|
-
if (!
|
|
1298
|
+
if (!isTrustedRequestHost(req))
|
|
1249
1299
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1250
1300
|
const status = await getClaudeAuthStatus();
|
|
1251
1301
|
return Response.json(
|
|
@@ -1263,7 +1313,7 @@ export function createHttp(
|
|
|
1263
1313
|
// chat-open` driver omits Origin (allowed); a browser drive-by can't forge it.
|
|
1264
1314
|
if (!sameOriginWrite(req))
|
|
1265
1315
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1266
|
-
if (!
|
|
1316
|
+
if (!isTrustedRequestHost(req))
|
|
1267
1317
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1268
1318
|
ctx.bus.emit('acp-focus', {});
|
|
1269
1319
|
return Response.json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -1289,7 +1339,7 @@ export function createHttp(
|
|
|
1289
1339
|
if (req.method === 'PATCH') {
|
|
1290
1340
|
if (!sameOriginWrite(req))
|
|
1291
1341
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1292
|
-
if (!
|
|
1342
|
+
if (!isTrustedRequestHost(req))
|
|
1293
1343
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1294
1344
|
if (!id) return new Response('missing id', { status: 400 });
|
|
1295
1345
|
let body: unknown;
|
|
@@ -1350,7 +1400,7 @@ export function createHttp(
|
|
|
1350
1400
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1351
1401
|
if (!sameOriginWrite(req))
|
|
1352
1402
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1353
|
-
if (!
|
|
1403
|
+
if (!isTrustedRequestHost(req))
|
|
1354
1404
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1355
1405
|
const declared = Number(req.headers.get('content-length') || '0');
|
|
1356
1406
|
if (Number.isFinite(declared) && declared > ASSET_MAX_BYTES) {
|
|
@@ -1457,7 +1507,7 @@ export function createHttp(
|
|
|
1457
1507
|
// /_api/acp/status (a drive-by page must not read logs via DNS rebinding).
|
|
1458
1508
|
'/_api/debug-bundle': (req: Request) => {
|
|
1459
1509
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1460
|
-
if (!
|
|
1510
|
+
if (!isTrustedRequestHost(req))
|
|
1461
1511
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1462
1512
|
return Response.json(
|
|
1463
1513
|
buildDebugBundle({
|
|
@@ -1490,7 +1540,7 @@ export function createHttp(
|
|
|
1490
1540
|
'/_api/shell-shot': async (req: Request) => {
|
|
1491
1541
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1492
1542
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1493
|
-
if (!
|
|
1543
|
+
if (!isTrustedRequestHost(req))
|
|
1494
1544
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1495
1545
|
// Target the very server handling this request — no _server.json read, so
|
|
1496
1546
|
// a stale/absent state file can't point the capture at another instance.
|
|
@@ -1560,7 +1610,7 @@ export function createHttp(
|
|
|
1560
1610
|
'/_api/report': async (req: Request) => {
|
|
1561
1611
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1562
1612
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1563
|
-
if (!
|
|
1613
|
+
if (!isTrustedRequestHost(req))
|
|
1564
1614
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1565
1615
|
const endpoint = process.env.MAUDE_REPORT_URL || 'https://cloud.maude.sh/report';
|
|
1566
1616
|
try {
|
|
@@ -1593,7 +1643,7 @@ export function createHttp(
|
|
|
1593
1643
|
'/_api/report-fallback': async (req: Request) => {
|
|
1594
1644
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1595
1645
|
if (!sameOriginWrite(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
1596
|
-
if (!
|
|
1646
|
+
if (!isTrustedRequestHost(req))
|
|
1597
1647
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1598
1648
|
const body = await readJson<{ report?: unknown; screenshots?: unknown }>(req);
|
|
1599
1649
|
if (!body || typeof body.report !== 'object' || body.report === null) {
|
|
@@ -1694,7 +1744,7 @@ export function createHttp(
|
|
|
1694
1744
|
// the photo-store cap stack (sha8 validate + containment + validatePhotoEdit
|
|
1695
1745
|
// + size cap) is the load-bearing mitigation. loopback-Host gates DNS-
|
|
1696
1746
|
// rebinding on every method.
|
|
1697
|
-
if (!
|
|
1747
|
+
if (!isTrustedRequestHost(req))
|
|
1698
1748
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1699
1749
|
const asset = new URL(req.url).searchParams.get('asset');
|
|
1700
1750
|
if (req.method === 'GET') {
|
|
@@ -1732,7 +1782,7 @@ export function createHttp(
|
|
|
1732
1782
|
// DNS-rebinding on every method; the footage-store cap stack (sha8/slug
|
|
1733
1783
|
// validate + containment + validate{FootageAnalysis,Edl} + size cap) is the
|
|
1734
1784
|
// load-bearing mitigation for the caller-derived write path.
|
|
1735
|
-
if (!
|
|
1785
|
+
if (!isTrustedRequestHost(req))
|
|
1736
1786
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1737
1787
|
const params = new URL(req.url).searchParams;
|
|
1738
1788
|
const asset = params.get('asset');
|
|
@@ -1775,7 +1825,7 @@ export function createHttp(
|
|
|
1775
1825
|
// a rebound page shares this origin; the dev-server always binds 127.0.0.1,
|
|
1776
1826
|
// so a real browser (Host: localhost:<port>) passes and only a rebound
|
|
1777
1827
|
// foreign hostname 403s. (No sameOriginWrite here — GET.)
|
|
1778
|
-
if (!
|
|
1828
|
+
if (!isTrustedRequestHost(req))
|
|
1779
1829
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1780
1830
|
const file = new URL(req.url).searchParams.get('file');
|
|
1781
1831
|
const r = await api.loadCanvasSource(file);
|
|
@@ -1840,7 +1890,7 @@ export function createHttp(
|
|
|
1840
1890
|
// slash-command driver omits Origin (→ allowed); a browser drive-by can't.
|
|
1841
1891
|
if (!sameOriginWrite(req))
|
|
1842
1892
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1843
|
-
if (!
|
|
1893
|
+
if (!isTrustedRequestHost(req))
|
|
1844
1894
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1845
1895
|
const body = await readJson<{ file?: string; author?: string }>(req);
|
|
1846
1896
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1860,7 +1910,7 @@ export function createHttp(
|
|
|
1860
1910
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1861
1911
|
if (!sameOriginWrite(req))
|
|
1862
1912
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1863
|
-
if (!
|
|
1913
|
+
if (!isTrustedRequestHost(req))
|
|
1864
1914
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1865
1915
|
const body = await readJson<{ file?: string }>(req);
|
|
1866
1916
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1876,7 +1926,7 @@ export function createHttp(
|
|
|
1876
1926
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
1877
1927
|
if (!sameOriginWrite(req))
|
|
1878
1928
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1879
|
-
if (!
|
|
1929
|
+
if (!isTrustedRequestHost(req))
|
|
1880
1930
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1881
1931
|
const body = await readJson<{ file?: string }>(req);
|
|
1882
1932
|
if (!body || typeof body.file !== 'string' || !body.file.trim()) {
|
|
@@ -1937,7 +1987,7 @@ export function createHttp(
|
|
|
1937
1987
|
if (req.method === 'DELETE' || req.method === 'POST') {
|
|
1938
1988
|
if (!sameOriginWrite(req))
|
|
1939
1989
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
1940
|
-
if (!
|
|
1990
|
+
if (!isTrustedRequestHost(req))
|
|
1941
1991
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
1942
1992
|
}
|
|
1943
1993
|
if (req.method === 'DELETE') {
|
|
@@ -2000,7 +2050,7 @@ export function createHttp(
|
|
|
2000
2050
|
'/_api/fs-move': async (req: Request) => {
|
|
2001
2051
|
if (!sameOriginWrite(req))
|
|
2002
2052
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2003
|
-
if (!
|
|
2053
|
+
if (!isTrustedRequestHost(req))
|
|
2004
2054
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2005
2055
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2006
2056
|
const body = await readJson<{ file?: unknown; toDir?: unknown }>(req, 4 * 1024);
|
|
@@ -2031,7 +2081,7 @@ export function createHttp(
|
|
|
2031
2081
|
'/_api/fs-mkdir': async (req: Request) => {
|
|
2032
2082
|
if (!sameOriginWrite(req))
|
|
2033
2083
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2034
|
-
if (!
|
|
2084
|
+
if (!isTrustedRequestHost(req))
|
|
2035
2085
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2036
2086
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2037
2087
|
const body = await readJson<{ parent?: unknown; name?: unknown }>(req, 4 * 1024);
|
|
@@ -2092,7 +2142,7 @@ export function createHttp(
|
|
|
2092
2142
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2093
2143
|
if (!sameOriginWrite(req))
|
|
2094
2144
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2095
|
-
if (!
|
|
2145
|
+
if (!isTrustedRequestHost(req))
|
|
2096
2146
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2097
2147
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2098
2148
|
return gitJson(await gitApi.createBranch(body));
|
|
@@ -2102,7 +2152,7 @@ export function createHttp(
|
|
|
2102
2152
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2103
2153
|
if (!sameOriginWrite(req))
|
|
2104
2154
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2105
|
-
if (!
|
|
2155
|
+
if (!isTrustedRequestHost(req))
|
|
2106
2156
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2107
2157
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2108
2158
|
return gitJson(await gitApi.checkout(body));
|
|
@@ -2114,7 +2164,7 @@ export function createHttp(
|
|
|
2114
2164
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2115
2165
|
if (!sameOriginWrite(req))
|
|
2116
2166
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2117
|
-
if (!
|
|
2167
|
+
if (!isTrustedRequestHost(req))
|
|
2118
2168
|
return new Response('adding a draft requires a local request', { status: 403 });
|
|
2119
2169
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2120
2170
|
return gitJson(await gitApi.fold(body));
|
|
@@ -2126,7 +2176,7 @@ export function createHttp(
|
|
|
2126
2176
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2127
2177
|
if (!sameOriginWrite(req))
|
|
2128
2178
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2129
|
-
if (!
|
|
2179
|
+
if (!isTrustedRequestHost(req))
|
|
2130
2180
|
return new Response('refresh requires a local request', { status: 403 });
|
|
2131
2181
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2132
2182
|
return gitJson(await gitApi.fetchRemote(body));
|
|
@@ -2136,7 +2186,7 @@ export function createHttp(
|
|
|
2136
2186
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2137
2187
|
if (!sameOriginWrite(req))
|
|
2138
2188
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2139
|
-
if (!
|
|
2189
|
+
if (!isTrustedRequestHost(req))
|
|
2140
2190
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2141
2191
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2142
2192
|
return gitJson(await gitApi.commit(body));
|
|
@@ -2146,7 +2196,7 @@ export function createHttp(
|
|
|
2146
2196
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2147
2197
|
if (!sameOriginWrite(req))
|
|
2148
2198
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2149
|
-
if (!
|
|
2199
|
+
if (!isTrustedRequestHost(req))
|
|
2150
2200
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2151
2201
|
const body = await readJson<unknown>(req, 256 * 1024);
|
|
2152
2202
|
return gitJson(await gitApi.discard(body));
|
|
@@ -2156,11 +2206,11 @@ export function createHttp(
|
|
|
2156
2206
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2157
2207
|
if (!sameOriginWrite(req))
|
|
2158
2208
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2159
|
-
if (!
|
|
2209
|
+
if (!isTrustedRequestHost(req))
|
|
2160
2210
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2161
2211
|
// Token-bearing: refuse anything not from a loopback Host (the server binds
|
|
2162
2212
|
// 127.0.0.1, so this is belt-and-suspenders against a forwarded/rebound Host).
|
|
2163
|
-
if (!
|
|
2213
|
+
if (!isTrustedRequestHost(req))
|
|
2164
2214
|
return new Response('publish requires a local request', { status: 403 });
|
|
2165
2215
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2166
2216
|
return gitJson(await gitApi.push(body));
|
|
@@ -2170,7 +2220,7 @@ export function createHttp(
|
|
|
2170
2220
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2171
2221
|
if (!sameOriginWrite(req))
|
|
2172
2222
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2173
|
-
if (!
|
|
2223
|
+
if (!isTrustedRequestHost(req))
|
|
2174
2224
|
return new Response('get latest requires a local request', { status: 403 });
|
|
2175
2225
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2176
2226
|
return gitJson(await gitApi.pull(body));
|
|
@@ -2184,7 +2234,7 @@ export function createHttp(
|
|
|
2184
2234
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2185
2235
|
if (!sameOriginWrite(req))
|
|
2186
2236
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2187
|
-
if (!
|
|
2237
|
+
if (!isTrustedRequestHost(req))
|
|
2188
2238
|
return new Response('resolve requires a local request', { status: 403 });
|
|
2189
2239
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2190
2240
|
return gitJson(await gitApi.resolve(body));
|
|
@@ -2205,7 +2255,7 @@ export function createHttp(
|
|
|
2205
2255
|
// whose 401 DELETES the stored credential — a confused deputy worth
|
|
2206
2256
|
// closing (validate 2026-07-30, defender F6).
|
|
2207
2257
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2208
|
-
if (!
|
|
2258
|
+
if (!isTrustedRequestHost(req))
|
|
2209
2259
|
return new Response('local request required', { status: 403 });
|
|
2210
2260
|
return gitJson(cloudApi.status());
|
|
2211
2261
|
},
|
|
@@ -2213,7 +2263,7 @@ export function createHttp(
|
|
|
2213
2263
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2214
2264
|
if (!sameOriginWrite(req))
|
|
2215
2265
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2216
|
-
if (!
|
|
2266
|
+
if (!isTrustedRequestHost(req))
|
|
2217
2267
|
return new Response('local request required', { status: 403 });
|
|
2218
2268
|
return gitJson(await cloudApi.signinStart());
|
|
2219
2269
|
},
|
|
@@ -2221,7 +2271,7 @@ export function createHttp(
|
|
|
2221
2271
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2222
2272
|
if (!sameOriginWrite(req))
|
|
2223
2273
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2224
|
-
if (!
|
|
2274
|
+
if (!isTrustedRequestHost(req))
|
|
2225
2275
|
return new Response('local request required', { status: 403 });
|
|
2226
2276
|
const body = (await req.json().catch(() => ({}))) as { deviceCode?: string };
|
|
2227
2277
|
return gitJson(await cloudApi.signinPoll(String(body.deviceCode ?? '')));
|
|
@@ -2230,14 +2280,14 @@ export function createHttp(
|
|
|
2230
2280
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2231
2281
|
if (!sameOriginWrite(req))
|
|
2232
2282
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2233
|
-
if (!
|
|
2283
|
+
if (!isTrustedRequestHost(req))
|
|
2234
2284
|
return new Response('local request required', { status: 403 });
|
|
2235
2285
|
return gitJson(cloudApi.signout());
|
|
2236
2286
|
},
|
|
2237
2287
|
'/_api/cloud/projects': async (req: Request) => {
|
|
2238
2288
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2239
2289
|
if (!sameOriginRead(req)) return new Response('cross-origin rejected', { status: 403 });
|
|
2240
|
-
if (!
|
|
2290
|
+
if (!isTrustedRequestHost(req))
|
|
2241
2291
|
return new Response('local request required', { status: 403 });
|
|
2242
2292
|
return gitJson(await cloudApi.projects());
|
|
2243
2293
|
},
|
|
@@ -2245,7 +2295,7 @@ export function createHttp(
|
|
|
2245
2295
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2246
2296
|
if (!sameOriginWrite(req))
|
|
2247
2297
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2248
|
-
if (!
|
|
2298
|
+
if (!isTrustedRequestHost(req))
|
|
2249
2299
|
return new Response('local request required', { status: 403 });
|
|
2250
2300
|
const body = (await req.json().catch(() => ({}))) as { project?: string };
|
|
2251
2301
|
return gitJson(await cloudApi.attach(String(body.project ?? '')));
|
|
@@ -2257,7 +2307,7 @@ export function createHttp(
|
|
|
2257
2307
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2258
2308
|
if (!sameOriginWrite(req))
|
|
2259
2309
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2260
|
-
if (!
|
|
2310
|
+
if (!isTrustedRequestHost(req))
|
|
2261
2311
|
return new Response('local request required', { status: 403 });
|
|
2262
2312
|
const body = (await req.json().catch(() => ({}))) as { code?: string; project?: string };
|
|
2263
2313
|
return gitJson(
|
|
@@ -2270,14 +2320,14 @@ export function createHttp(
|
|
|
2270
2320
|
|
|
2271
2321
|
'/_api/github/identity': async (req: Request) => {
|
|
2272
2322
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2273
|
-
if (!
|
|
2323
|
+
if (!isTrustedRequestHost(req))
|
|
2274
2324
|
return new Response('local request required', { status: 403 });
|
|
2275
2325
|
return gitJson(await githubApi.identity());
|
|
2276
2326
|
},
|
|
2277
2327
|
|
|
2278
2328
|
'/_api/github/repos': async (req: Request) => {
|
|
2279
2329
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2280
|
-
if (!
|
|
2330
|
+
if (!isTrustedRequestHost(req))
|
|
2281
2331
|
return new Response('local request required', { status: 403 });
|
|
2282
2332
|
return gitJson(await githubApi.repos());
|
|
2283
2333
|
},
|
|
@@ -2286,7 +2336,7 @@ export function createHttp(
|
|
|
2286
2336
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2287
2337
|
if (!sameOriginWrite(req))
|
|
2288
2338
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2289
|
-
if (!
|
|
2339
|
+
if (!isTrustedRequestHost(req))
|
|
2290
2340
|
return new Response('local request required', { status: 403 });
|
|
2291
2341
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2292
2342
|
return gitJson(await githubApi.createRepo(body));
|
|
@@ -2296,7 +2346,7 @@ export function createHttp(
|
|
|
2296
2346
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2297
2347
|
if (!sameOriginWrite(req))
|
|
2298
2348
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2299
|
-
if (!
|
|
2349
|
+
if (!isTrustedRequestHost(req))
|
|
2300
2350
|
return new Response('local request required', { status: 403 });
|
|
2301
2351
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2302
2352
|
return gitJson(await githubApi.invite(body));
|
|
@@ -2306,7 +2356,7 @@ export function createHttp(
|
|
|
2306
2356
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2307
2357
|
if (!sameOriginWrite(req))
|
|
2308
2358
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2309
|
-
if (!
|
|
2359
|
+
if (!isTrustedRequestHost(req))
|
|
2310
2360
|
return new Response('local request required', { status: 403 });
|
|
2311
2361
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2312
2362
|
return gitJson(await githubApi.clone(body));
|
|
@@ -2316,7 +2366,7 @@ export function createHttp(
|
|
|
2316
2366
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2317
2367
|
if (!sameOriginWrite(req))
|
|
2318
2368
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2319
|
-
if (!
|
|
2369
|
+
if (!isTrustedRequestHost(req))
|
|
2320
2370
|
return new Response('local request required', { status: 403 });
|
|
2321
2371
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2322
2372
|
return gitJson(await githubApi.createProject(body));
|
|
@@ -2331,7 +2381,7 @@ export function createHttp(
|
|
|
2331
2381
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2332
2382
|
if (!sameOriginWrite(req))
|
|
2333
2383
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2334
|
-
if (!
|
|
2384
|
+
if (!isTrustedRequestHost(req))
|
|
2335
2385
|
return new Response('local request required', { status: 403 });
|
|
2336
2386
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2337
2387
|
return gitJson(await githubApi.createLocalProject(body));
|
|
@@ -2344,7 +2394,7 @@ export function createHttp(
|
|
|
2344
2394
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2345
2395
|
if (!sameOriginWrite(req))
|
|
2346
2396
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2347
|
-
if (!
|
|
2397
|
+
if (!isTrustedRequestHost(req))
|
|
2348
2398
|
return new Response('local request required', { status: 403 });
|
|
2349
2399
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2350
2400
|
return gitJson(await githubApi.initDesign(body));
|
|
@@ -2359,7 +2409,7 @@ export function createHttp(
|
|
|
2359
2409
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2360
2410
|
if (!sameOriginWrite(req))
|
|
2361
2411
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2362
|
-
if (!
|
|
2412
|
+
if (!isTrustedRequestHost(req))
|
|
2363
2413
|
return new Response('local request required', { status: 403 });
|
|
2364
2414
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2365
2415
|
return gitJson(await linkHub(body));
|
|
@@ -2374,7 +2424,7 @@ export function createHttp(
|
|
|
2374
2424
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2375
2425
|
if (!sameOriginWrite(req))
|
|
2376
2426
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2377
|
-
if (!
|
|
2427
|
+
if (!isTrustedRequestHost(req))
|
|
2378
2428
|
return new Response('local request required', { status: 403 });
|
|
2379
2429
|
const body = await readJson<unknown>(req, 8 * 1024);
|
|
2380
2430
|
const result = await signInToWorkspace((body ?? {}) as Record<string, unknown>);
|
|
@@ -2390,7 +2440,7 @@ export function createHttp(
|
|
|
2390
2440
|
// for, and every route the canvas can reach is one more thing to reason about.
|
|
2391
2441
|
'/_api/workspace/disclosure': async (req: Request) => {
|
|
2392
2442
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2393
|
-
if (!
|
|
2443
|
+
if (!isTrustedRequestHost(req))
|
|
2394
2444
|
return new Response('local request required', { status: 403 });
|
|
2395
2445
|
const url = new URL(req.url);
|
|
2396
2446
|
const linked = ctx.cfg.linkedHub?.url ?? null;
|
|
@@ -2420,7 +2470,7 @@ export function createHttp(
|
|
|
2420
2470
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2421
2471
|
if (!sameOriginWrite(req))
|
|
2422
2472
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2423
|
-
if (!
|
|
2473
|
+
if (!isTrustedRequestHost(req))
|
|
2424
2474
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2425
2475
|
const body = await readJson<{
|
|
2426
2476
|
canvas?: unknown;
|
|
@@ -2453,7 +2503,7 @@ export function createHttp(
|
|
|
2453
2503
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2454
2504
|
if (!sameOriginWrite(req))
|
|
2455
2505
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2456
|
-
if (!
|
|
2506
|
+
if (!isTrustedRequestHost(req))
|
|
2457
2507
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2458
2508
|
const body = await readJson<{
|
|
2459
2509
|
canvas?: unknown;
|
|
@@ -2486,7 +2536,7 @@ export function createHttp(
|
|
|
2486
2536
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2487
2537
|
if (!sameOriginWrite(req))
|
|
2488
2538
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2489
|
-
if (!
|
|
2539
|
+
if (!isTrustedRequestHost(req))
|
|
2490
2540
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2491
2541
|
const body = await readJson<{
|
|
2492
2542
|
canvas?: unknown;
|
|
@@ -2522,7 +2572,7 @@ export function createHttp(
|
|
|
2522
2572
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2523
2573
|
if (!sameOriginWrite(req))
|
|
2524
2574
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2525
|
-
if (!
|
|
2575
|
+
if (!isTrustedRequestHost(req))
|
|
2526
2576
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2527
2577
|
const body = await readJson<{
|
|
2528
2578
|
canvas?: unknown;
|
|
@@ -2562,7 +2612,7 @@ export function createHttp(
|
|
|
2562
2612
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2563
2613
|
if (!sameOriginWrite(req))
|
|
2564
2614
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2565
|
-
if (!
|
|
2615
|
+
if (!isTrustedRequestHost(req))
|
|
2566
2616
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2567
2617
|
const body = await readJson<{
|
|
2568
2618
|
canvas?: unknown;
|
|
@@ -2598,7 +2648,7 @@ export function createHttp(
|
|
|
2598
2648
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2599
2649
|
if (!sameOriginWrite(req))
|
|
2600
2650
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2601
|
-
if (!
|
|
2651
|
+
if (!isTrustedRequestHost(req))
|
|
2602
2652
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2603
2653
|
const body = await readJson<{
|
|
2604
2654
|
canvas?: unknown;
|
|
@@ -2643,7 +2693,7 @@ export function createHttp(
|
|
|
2643
2693
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2644
2694
|
if (!sameOriginWrite(req))
|
|
2645
2695
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2646
|
-
if (!
|
|
2696
|
+
if (!isTrustedRequestHost(req))
|
|
2647
2697
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2648
2698
|
const body = await readJson<{
|
|
2649
2699
|
canvas?: unknown;
|
|
@@ -2673,7 +2723,7 @@ export function createHttp(
|
|
|
2673
2723
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2674
2724
|
if (!sameOriginWrite(req))
|
|
2675
2725
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2676
|
-
if (!
|
|
2726
|
+
if (!isTrustedRequestHost(req))
|
|
2677
2727
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2678
2728
|
const body = await readJson<{
|
|
2679
2729
|
canvas?: unknown;
|
|
@@ -2710,7 +2760,7 @@ export function createHttp(
|
|
|
2710
2760
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2711
2761
|
if (!sameOriginWrite(req))
|
|
2712
2762
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2713
|
-
if (!
|
|
2763
|
+
if (!isTrustedRequestHost(req))
|
|
2714
2764
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2715
2765
|
const body = await readJson<{
|
|
2716
2766
|
canvas?: unknown;
|
|
@@ -2743,7 +2793,7 @@ export function createHttp(
|
|
|
2743
2793
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2744
2794
|
if (!sameOriginWrite(req))
|
|
2745
2795
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2746
|
-
if (!
|
|
2796
|
+
if (!isTrustedRequestHost(req))
|
|
2747
2797
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2748
2798
|
const body = await readJson<{
|
|
2749
2799
|
canvas?: unknown;
|
|
@@ -2774,7 +2824,7 @@ export function createHttp(
|
|
|
2774
2824
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2775
2825
|
if (!sameOriginWrite(req))
|
|
2776
2826
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2777
|
-
if (!
|
|
2827
|
+
if (!isTrustedRequestHost(req))
|
|
2778
2828
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2779
2829
|
const body = await readJson<{
|
|
2780
2830
|
canvas?: unknown;
|
|
@@ -2808,7 +2858,7 @@ export function createHttp(
|
|
|
2808
2858
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
2809
2859
|
// DNS-rebinding guard — this GET echoes project source structure; loopback-
|
|
2810
2860
|
// only Host (real browser passes, rebound hostname 403s). No sameOriginWrite (GET).
|
|
2811
|
-
if (!
|
|
2861
|
+
if (!isTrustedRequestHost(req))
|
|
2812
2862
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2813
2863
|
const u = new URL(req.url);
|
|
2814
2864
|
const result = await api.compClips({
|
|
@@ -2834,7 +2884,7 @@ export function createHttp(
|
|
|
2834
2884
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2835
2885
|
if (!sameOriginWrite(req))
|
|
2836
2886
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2837
|
-
if (!
|
|
2887
|
+
if (!isTrustedRequestHost(req))
|
|
2838
2888
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2839
2889
|
const body = await readJson<{ canvas?: unknown; seq?: unknown; dir?: unknown }>(
|
|
2840
2890
|
req,
|
|
@@ -2865,7 +2915,7 @@ export function createHttp(
|
|
|
2865
2915
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2866
2916
|
if (!sameOriginWrite(req))
|
|
2867
2917
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2868
|
-
if (!
|
|
2918
|
+
if (!isTrustedRequestHost(req))
|
|
2869
2919
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2870
2920
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2871
2921
|
req,
|
|
@@ -2897,7 +2947,7 @@ export function createHttp(
|
|
|
2897
2947
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2898
2948
|
if (!sameOriginWrite(req))
|
|
2899
2949
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2900
|
-
if (!
|
|
2950
|
+
if (!isTrustedRequestHost(req))
|
|
2901
2951
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2902
2952
|
const body = await readJson<{
|
|
2903
2953
|
canvas?: unknown;
|
|
@@ -2932,7 +2982,7 @@ export function createHttp(
|
|
|
2932
2982
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2933
2983
|
if (!sameOriginWrite(req))
|
|
2934
2984
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2935
|
-
if (!
|
|
2985
|
+
if (!isTrustedRequestHost(req))
|
|
2936
2986
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2937
2987
|
const body = await readJson<{
|
|
2938
2988
|
canvas?: unknown;
|
|
@@ -2966,7 +3016,7 @@ export function createHttp(
|
|
|
2966
3016
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2967
3017
|
if (!sameOriginWrite(req))
|
|
2968
3018
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2969
|
-
if (!
|
|
3019
|
+
if (!isTrustedRequestHost(req))
|
|
2970
3020
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2971
3021
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2972
3022
|
req,
|
|
@@ -2993,7 +3043,7 @@ export function createHttp(
|
|
|
2993
3043
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
2994
3044
|
if (!sameOriginWrite(req))
|
|
2995
3045
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
2996
|
-
if (!
|
|
3046
|
+
if (!isTrustedRequestHost(req))
|
|
2997
3047
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
2998
3048
|
const body = await readJson<{ canvas?: unknown; id?: unknown; idIndex?: unknown }>(
|
|
2999
3049
|
req,
|
|
@@ -3022,7 +3072,7 @@ export function createHttp(
|
|
|
3022
3072
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3023
3073
|
if (!sameOriginWrite(req))
|
|
3024
3074
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3025
|
-
if (!
|
|
3075
|
+
if (!isTrustedRequestHost(req))
|
|
3026
3076
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3027
3077
|
const body = await readJson<{
|
|
3028
3078
|
canvas?: unknown;
|
|
@@ -3056,7 +3106,7 @@ export function createHttp(
|
|
|
3056
3106
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3057
3107
|
if (!sameOriginWrite(req))
|
|
3058
3108
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3059
|
-
if (!
|
|
3109
|
+
if (!isTrustedRequestHost(req))
|
|
3060
3110
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3061
3111
|
const body = await readJson<{
|
|
3062
3112
|
canvas?: unknown;
|
|
@@ -3085,7 +3135,7 @@ export function createHttp(
|
|
|
3085
3135
|
// + startCanvasServer routes (dual-allowlist, DDR-054). Loopback-Host gated
|
|
3086
3136
|
// (DNS-rebinding); read-only, so no sameOriginWrite (GET).
|
|
3087
3137
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3088
|
-
if (!
|
|
3138
|
+
if (!isTrustedRequestHost(req))
|
|
3089
3139
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3090
3140
|
const r = await api.listAssets();
|
|
3091
3141
|
return Response.json(
|
|
@@ -3103,7 +3153,7 @@ export function createHttp(
|
|
|
3103
3153
|
// routes (dual-allowlist, DDR-054). Loopback-Host gated (DNS-rebinding);
|
|
3104
3154
|
// read-only, so no sameOriginWrite (GET).
|
|
3105
3155
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3106
|
-
if (!
|
|
3156
|
+
if (!isTrustedRequestHost(req))
|
|
3107
3157
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3108
3158
|
const r = await api.listStickers();
|
|
3109
3159
|
return Response.json(
|
|
@@ -3120,7 +3170,7 @@ export function createHttp(
|
|
|
3120
3170
|
// from CANVAS_SAFE_API + startCanvasServer routes (dual-allowlist, DDR-054);
|
|
3121
3171
|
// loopback-Host gated (DNS-rebinding); no sameOriginWrite (GET).
|
|
3122
3172
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3123
|
-
if (!
|
|
3173
|
+
if (!isTrustedRequestHost(req))
|
|
3124
3174
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3125
3175
|
const url = new URL(req.url);
|
|
3126
3176
|
const result = await api.editScopeOp({
|
|
@@ -3144,7 +3194,7 @@ export function createHttp(
|
|
|
3144
3194
|
// CANVAS_SAFE_API + startCanvasServer routes (DDR-054); loopback-Host
|
|
3145
3195
|
// gated; no sameOriginWrite (GET).
|
|
3146
3196
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3147
|
-
if (!
|
|
3197
|
+
if (!isTrustedRequestHost(req))
|
|
3148
3198
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3149
3199
|
const url = new URL(req.url);
|
|
3150
3200
|
const result = await api.componentMapOp({
|
|
@@ -3168,7 +3218,7 @@ export function createHttp(
|
|
|
3168
3218
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3169
3219
|
if (!sameOriginWrite(req))
|
|
3170
3220
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3171
|
-
if (!
|
|
3221
|
+
if (!isTrustedRequestHost(req))
|
|
3172
3222
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3173
3223
|
const body = await readJson<{
|
|
3174
3224
|
canvas?: unknown;
|
|
@@ -3200,7 +3250,7 @@ export function createHttp(
|
|
|
3200
3250
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3201
3251
|
if (!sameOriginWrite(req))
|
|
3202
3252
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3203
|
-
if (!
|
|
3253
|
+
if (!isTrustedRequestHost(req))
|
|
3204
3254
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3205
3255
|
const body = await readJson<{
|
|
3206
3256
|
canvas?: unknown;
|
|
@@ -3231,7 +3281,7 @@ export function createHttp(
|
|
|
3231
3281
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3232
3282
|
if (!sameOriginWrite(req))
|
|
3233
3283
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3234
|
-
if (!
|
|
3284
|
+
if (!isTrustedRequestHost(req))
|
|
3235
3285
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3236
3286
|
const body = await readJson<{
|
|
3237
3287
|
canvas?: unknown;
|
|
@@ -3265,7 +3315,7 @@ export function createHttp(
|
|
|
3265
3315
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3266
3316
|
if (!sameOriginWrite(req))
|
|
3267
3317
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3268
|
-
if (!
|
|
3318
|
+
if (!isTrustedRequestHost(req))
|
|
3269
3319
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3270
3320
|
const body = await readJson<{
|
|
3271
3321
|
canvas?: unknown;
|
|
@@ -3295,7 +3345,7 @@ export function createHttp(
|
|
|
3295
3345
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3296
3346
|
if (!sameOriginWrite(req))
|
|
3297
3347
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3298
|
-
if (!
|
|
3348
|
+
if (!isTrustedRequestHost(req))
|
|
3299
3349
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3300
3350
|
const body = await readJson<{
|
|
3301
3351
|
canvas?: unknown;
|
|
@@ -3326,7 +3376,7 @@ export function createHttp(
|
|
|
3326
3376
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3327
3377
|
if (!sameOriginWrite(req))
|
|
3328
3378
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3329
|
-
if (!
|
|
3379
|
+
if (!isTrustedRequestHost(req))
|
|
3330
3380
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3331
3381
|
const body = await readJson<{
|
|
3332
3382
|
canvas?: unknown;
|
|
@@ -3355,7 +3405,7 @@ export function createHttp(
|
|
|
3355
3405
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3356
3406
|
if (!sameOriginWrite(req))
|
|
3357
3407
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3358
|
-
if (!
|
|
3408
|
+
if (!isTrustedRequestHost(req))
|
|
3359
3409
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3360
3410
|
const body = await readJson<{ canvas?: unknown; artboardId?: unknown }>(req, 8 * 1024);
|
|
3361
3411
|
if (!body) return new Response('body required', { status: 400 });
|
|
@@ -3423,7 +3473,7 @@ export function createHttp(
|
|
|
3423
3473
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3424
3474
|
if (!sameOriginWrite(req))
|
|
3425
3475
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3426
|
-
if (!
|
|
3476
|
+
if (!isTrustedRequestHost(req))
|
|
3427
3477
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3428
3478
|
const kind = (req.headers.get('x-import-kind') || 'svg').toLowerCase();
|
|
3429
3479
|
if (kind !== 'svg') {
|
|
@@ -3490,7 +3540,7 @@ export function createHttp(
|
|
|
3490
3540
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3491
3541
|
if (!sameOriginWrite(req))
|
|
3492
3542
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3493
|
-
if (!
|
|
3543
|
+
if (!isTrustedRequestHost(req))
|
|
3494
3544
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3495
3545
|
let body: { assetPath?: unknown };
|
|
3496
3546
|
try {
|
|
@@ -3564,7 +3614,7 @@ export function createHttp(
|
|
|
3564
3614
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
3565
3615
|
if (!sameOriginWrite(req))
|
|
3566
3616
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
3567
|
-
if (!
|
|
3617
|
+
if (!isTrustedRequestHost(req))
|
|
3568
3618
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3569
3619
|
const body = await readJson<{
|
|
3570
3620
|
format?: unknown;
|
|
@@ -3608,7 +3658,7 @@ export function createHttp(
|
|
|
3608
3658
|
// /_api/export today — DDR-060). loopback-Host gated on every method;
|
|
3609
3659
|
// the mutating POST is additionally sameOriginWrite CSRF gated (same
|
|
3610
3660
|
// /flow:done security fan-out fix as /_api/export above).
|
|
3611
|
-
if (!
|
|
3661
|
+
if (!isTrustedRequestHost(req))
|
|
3612
3662
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3613
3663
|
if (req.method === 'GET') {
|
|
3614
3664
|
return Response.json(
|
|
@@ -3656,7 +3706,7 @@ export function createHttp(
|
|
|
3656
3706
|
// unguessable job-scoped UUID plus this guard closes the DNS-rebinding
|
|
3657
3707
|
// angle the /flow:done security fan-out checked for).
|
|
3658
3708
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3659
|
-
if (!
|
|
3709
|
+
if (!isTrustedRequestHost(req))
|
|
3660
3710
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3661
3711
|
const id = new URL(req.url).searchParams.get('id');
|
|
3662
3712
|
if (!id) return new Response('id query param required', { status: 400 });
|
|
@@ -3684,7 +3734,7 @@ export function createHttp(
|
|
|
3684
3734
|
// loopback-Host gated on every method; the mutating POST is additionally
|
|
3685
3735
|
// sameOriginWrite CSRF-gated. See DDR-16x + the canvas-origin-gate test.
|
|
3686
3736
|
'/_api/generate-jobs': async (req: Request) => {
|
|
3687
|
-
if (!
|
|
3737
|
+
if (!isTrustedRequestHost(req))
|
|
3688
3738
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3689
3739
|
if (req.method === 'GET') {
|
|
3690
3740
|
return Response.json(
|
|
@@ -3803,7 +3853,7 @@ export function createHttp(
|
|
|
3803
3853
|
// keyUrl) + presence-only `configured` flags. NO secret ever crosses here.
|
|
3804
3854
|
// MAIN-ORIGIN ONLY, loopback-gated.
|
|
3805
3855
|
'/_api/generate/providers': async (req: Request) => {
|
|
3806
|
-
if (!
|
|
3856
|
+
if (!isTrustedRequestHost(req))
|
|
3807
3857
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3808
3858
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3809
3859
|
const configured = new Set(configuredProviders());
|
|
@@ -3820,7 +3870,7 @@ export function createHttp(
|
|
|
3820
3870
|
// ({configured:[...]}) — a key value is NEVER echoed back (mirrors
|
|
3821
3871
|
// github_is_signed_in). MAIN-ORIGIN ONLY, loopback + sameOriginWrite gated.
|
|
3822
3872
|
'/_api/generate/keys': async (req: Request) => {
|
|
3823
|
-
if (!
|
|
3873
|
+
if (!isTrustedRequestHost(req))
|
|
3824
3874
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3825
3875
|
if (req.method === 'GET') {
|
|
3826
3876
|
return Response.json(
|
|
@@ -3864,7 +3914,7 @@ export function createHttp(
|
|
|
3864
3914
|
// ONLY, loopback + sameOriginWrite gated. NEVER touches a key — that's the
|
|
3865
3915
|
// separate /_api/generate/keys route.
|
|
3866
3916
|
'/_api/generate/prefs': async (req: Request) => {
|
|
3867
|
-
if (!
|
|
3917
|
+
if (!isTrustedRequestHost(req))
|
|
3868
3918
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3869
3919
|
if (req.method === 'GET') {
|
|
3870
3920
|
return Response.json(
|
|
@@ -3897,7 +3947,7 @@ export function createHttp(
|
|
|
3897
3947
|
}
|
|
3898
3948
|
const provider = body?.transcriptionProvider;
|
|
3899
3949
|
if (!isTranscriptionProvider(provider))
|
|
3900
|
-
return new Response('transcriptionProvider must be whisper|elevenlabs|groq', {
|
|
3950
|
+
return new Response('transcriptionProvider must be auto|whisper|elevenlabs|groq', {
|
|
3901
3951
|
status: 400,
|
|
3902
3952
|
});
|
|
3903
3953
|
await writeTranscriptionProvider(ctx.paths.repoRoot, provider);
|
|
@@ -3919,7 +3969,7 @@ export function createHttp(
|
|
|
3919
3969
|
// prefs; POST merge-patches only the provided keys. MAIN-ORIGIN ONLY, loopback
|
|
3920
3970
|
// + sameOriginWrite gated (never reachable from the untrusted canvas origin).
|
|
3921
3971
|
'/_api/ui-prefs': async (req: Request) => {
|
|
3922
|
-
if (!
|
|
3972
|
+
if (!isTrustedRequestHost(req))
|
|
3923
3973
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3924
3974
|
if (req.method === 'GET') {
|
|
3925
3975
|
return Response.json(readUiPrefs(), { headers: { 'Cache-Control': 'no-store' } });
|
|
@@ -3976,7 +4026,7 @@ export function createHttp(
|
|
|
3976
4026
|
// already paid), returning ranked reuse candidates. MAIN-ORIGIN ONLY,
|
|
3977
4027
|
// loopback-gated (it resolves the provider key server-side).
|
|
3978
4028
|
'/_api/generate/audio-search': async (req: Request) => {
|
|
3979
|
-
if (!
|
|
4029
|
+
if (!isTrustedRequestHost(req))
|
|
3980
4030
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
3981
4031
|
if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
|
|
3982
4032
|
// F1 (ethical-hacker) — this GET resolves the user's key + fans out to the
|
|
@@ -4024,7 +4074,7 @@ export function createHttp(
|
|
|
4024
4074
|
// reused track is itself searchable. MAIN-ORIGIN ONLY, loopback +
|
|
4025
4075
|
// sameOriginWrite gated.
|
|
4026
4076
|
'/_api/generate/audio-reuse': async (req: Request) => {
|
|
4027
|
-
if (!
|
|
4077
|
+
if (!isTrustedRequestHost(req))
|
|
4028
4078
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4029
4079
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4030
4080
|
if (!sameOriginWrite(req))
|
|
@@ -4064,11 +4114,21 @@ export function createHttp(
|
|
|
4064
4114
|
// `maude design transcribe --provider whisper` auto-resolves it. MAIN-ORIGIN
|
|
4065
4115
|
// ONLY, loopback + sameOriginWrite gated.
|
|
4066
4116
|
'/_api/generate/whisper-model': async (req: Request) => {
|
|
4067
|
-
if (!
|
|
4117
|
+
if (!isTrustedRequestHost(req))
|
|
4068
4118
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4069
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();
|
|
4070
4125
|
return Response.json(
|
|
4071
|
-
{
|
|
4126
|
+
{
|
|
4127
|
+
models: listWhisperModels(),
|
|
4128
|
+
downloading: whisperDownload,
|
|
4129
|
+
setup,
|
|
4130
|
+
auto: resolveAutoEngine(configuredProviders(), setup.installed),
|
|
4131
|
+
},
|
|
4072
4132
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4073
4133
|
);
|
|
4074
4134
|
}
|
|
@@ -4127,7 +4187,7 @@ export function createHttp(
|
|
|
4127
4187
|
// which tier will run; POST {id} downloads one via huggingface_hub (gated on
|
|
4128
4188
|
// mlx-vlm being installed — the model is useless without it). MAIN-ORIGIN ONLY.
|
|
4129
4189
|
'/_api/generate/keyframe-model': async (req: Request) => {
|
|
4130
|
-
if (!
|
|
4190
|
+
if (!isTrustedRequestHost(req))
|
|
4131
4191
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4132
4192
|
if (req.method === 'GET') {
|
|
4133
4193
|
// The GET builds availability by spawning subprocesses (import mlx_vlm is
|
|
@@ -4137,12 +4197,28 @@ export function createHttp(
|
|
|
4137
4197
|
// are also TTL-cached in gemma-models.ts as defence-in-depth.
|
|
4138
4198
|
if (!sameOriginRead(req))
|
|
4139
4199
|
return new Response('cross-origin read rejected', { status: 403 });
|
|
4200
|
+
const ollama = await ollamaStatus();
|
|
4140
4201
|
return Response.json(
|
|
4141
4202
|
{
|
|
4142
|
-
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),
|
|
4143
4207
|
downloading: keyframeDownload,
|
|
4144
4208
|
mlxVlmAvailable: mlxVlmAvailable(),
|
|
4145
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
|
+
},
|
|
4146
4222
|
},
|
|
4147
4223
|
{ headers: { 'Cache-Control': 'no-store' } }
|
|
4148
4224
|
);
|
|
@@ -4158,23 +4234,34 @@ export function createHttp(
|
|
|
4158
4234
|
if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
|
|
4159
4235
|
if (!sameOriginWrite(req))
|
|
4160
4236
|
return new Response('cross-origin write rejected', { status: 403 });
|
|
4161
|
-
|
|
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())
|
|
4162
4252
|
return new Response(
|
|
4163
|
-
'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.)',
|
|
4164
4254
|
{
|
|
4165
4255
|
status: 400,
|
|
4166
4256
|
}
|
|
4167
4257
|
);
|
|
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
4258
|
if (keyframeDownload && !keyframeDownload.error)
|
|
4173
4259
|
return new Response(`a download is already in progress (${keyframeDownload.id})`, {
|
|
4174
4260
|
status: 409,
|
|
4175
4261
|
});
|
|
4176
4262
|
keyframeDownload = { id, received: 0, total: 0 };
|
|
4177
|
-
|
|
4263
|
+
const run = isOllama ? pullOllamaModel : downloadGemmaModel;
|
|
4264
|
+
void run(
|
|
4178
4265
|
id,
|
|
4179
4266
|
(received, total) => {
|
|
4180
4267
|
if (keyframeDownload && keyframeDownload.id === id) {
|
|
@@ -4212,7 +4299,7 @@ export function createHttp(
|
|
|
4212
4299
|
// dataURLs of local footage → disclosure) or WRITE arbitrary JSON later
|
|
4213
4300
|
// rendered as <img src> in the trusted shell. The loopback-host check is
|
|
4214
4301
|
// the same backstop every sibling privileged route carries.
|
|
4215
|
-
if (!
|
|
4302
|
+
if (!isTrustedRequestHost(req))
|
|
4216
4303
|
return new Response('local request required (DNS-rebinding guard)', { status: 403 });
|
|
4217
4304
|
const url = new URL(req.url);
|
|
4218
4305
|
const key = url.searchParams.get('key') ?? '';
|