@agent-native/core 0.207.1 → 0.207.2-nightly-20261013011333
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/dist/agent/production-agent.js +2 -0
- package/dist/cli/design-connect.d.ts +6 -0
- package/dist/cli/design-connect.js +880 -9
- package/dist/client/action-query-cache.d.ts +3 -1
- package/dist/client/action-query-cache.js +628 -161
- package/dist/client/action-query-cache.session-test-fixture.d.ts +49 -0
- package/dist/client/action-query-cache.session-test-fixture.js +151 -0
- package/dist/client/action-query-cache.test-fixtures.d.ts +2 -0
- package/dist/client/action-query-cache.test-fixtures.js +56 -0
- package/dist/client/motion-text.css +3 -0
- package/dist/client/motion-text.d.ts +38 -0
- package/dist/client/motion-text.js +551 -0
- package/dist/client/org/hooks.d.ts +3 -1
- package/dist/client/org/hooks.js +8 -2
- package/dist/client/route-chunk-recovery.d.ts +2 -0
- package/dist/client/route-chunk-recovery.js +11 -0
- package/dist/client/use-action.d.ts +10 -2
- package/dist/client/use-action.js +81 -12
- package/dist/client/use-settings-shortcut.js +8 -3
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/extensions/routes.js +7 -3
- package/dist/extensions/web-content.js +17 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/build-server.js +5 -0
- package/dist/observability/routes.d.ts +4 -4
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/visual-edit-bridge-capability.d.ts +26 -0
- package/dist/server/visual-edit-bridge-capability.js +233 -0
- package/dist/shared/motion-easing.d.ts +6 -0
- package/dist/shared/motion-easing.js +109 -0
- package/dist/shared/motion-source-node-id.d.ts +3 -0
- package/dist/shared/motion-source-node-id.js +12 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
- package/dist/triggers/actions/manage-automation.d.ts +5 -5
- package/dist/vite/client.js +2 -0
- package/dist/vite/motion-source-provenance-plugin.d.ts +3 -0
- package/dist/vite/motion-source-provenance-plugin.js +339 -0
- package/package.json +14 -5
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
|
@@ -4,13 +4,20 @@ import fsSync from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import https from "node:https";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
7
8
|
import path from "node:path";
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
8
10
|
import { WebSocket, WebSocketServer } from "ws";
|
|
11
|
+
import { createLocalhostBridgeAudience, LOCALHOST_BRIDGE_OPERATION_CAPABILITY_FIELD, verifyLocalhostBridgeOperationCapability, } from "../server/visual-edit-bridge-capability.js";
|
|
9
12
|
import { injectDocumentMarkup } from "../shared/html-document.js";
|
|
10
13
|
const DEFAULT_BRIDGE_PORT = 7331;
|
|
11
14
|
const ROUTE_MANIFEST_FILE = path.join(".agent-native", "design-routes.json");
|
|
12
15
|
const BRIDGE_TOKEN_FILE = path.join(".agent-native", "design-bridge-token");
|
|
13
16
|
const LIVE_EDIT_REVISION_FILE = path.join(".agent-native", "live-edit-revisions.json");
|
|
17
|
+
const MAX_BRIDGE_CAPABILITY_REPLAY_ENTRIES = 4096;
|
|
18
|
+
const MAX_BRIDGE_CAPABILITY_REPLAY_SCAN = 8192;
|
|
19
|
+
const BRIDGE_CAPABILITY_REPLAY_ORPHAN_TTL_MS = 120_000;
|
|
20
|
+
const MAX_BRIDGE_OPERATION_BODY_BYTES = 20 * 1024 * 1024;
|
|
14
21
|
const LOCALHOST_CONNECTION_ID = /^localhost_[A-Za-z0-9_-]{16}$/;
|
|
15
22
|
const DEFAULT_DEV_SERVER_CANDIDATES = [
|
|
16
23
|
"http://127.0.0.1:5173",
|
|
@@ -379,10 +386,12 @@ export function discoverDesignRoutes(root) {
|
|
|
379
386
|
if (routes.size === 0) {
|
|
380
387
|
for (const dir of [absoluteRoot, path.join(absoluteRoot, "public")]) {
|
|
381
388
|
for (const file of walkFiles(dir)) {
|
|
382
|
-
if (
|
|
389
|
+
if (!/\.html?$/i.test(file))
|
|
383
390
|
continue;
|
|
384
391
|
const rel = normalizeSlash(path.relative(dir, file));
|
|
385
|
-
const pathName =
|
|
392
|
+
const pathName = /^index\.html?$/i.test(rel)
|
|
393
|
+
? "/"
|
|
394
|
+
: `/${rel.replace(/\.html?$/i, "")}`;
|
|
386
395
|
routes.set(pathName, {
|
|
387
396
|
id: routeId(pathName),
|
|
388
397
|
path: pathName,
|
|
@@ -1370,14 +1379,126 @@ function injectLiveEditBridge(html, baseHref, script, targetPath, identity = {})
|
|
|
1370
1379
|
return withBase;
|
|
1371
1380
|
return injectDocumentMarkup(withBase, script);
|
|
1372
1381
|
}
|
|
1373
|
-
|
|
1382
|
+
class RequestBodyTooLargeError extends Error {
|
|
1383
|
+
}
|
|
1384
|
+
async function readRequestBody(req, maxBytes) {
|
|
1374
1385
|
return new Promise((resolve, reject) => {
|
|
1375
1386
|
const chunks = [];
|
|
1376
|
-
|
|
1377
|
-
|
|
1387
|
+
let size = 0;
|
|
1388
|
+
let tooLarge = false;
|
|
1389
|
+
req.on("data", (chunk) => {
|
|
1390
|
+
if (tooLarge)
|
|
1391
|
+
return;
|
|
1392
|
+
size += chunk.length;
|
|
1393
|
+
if (maxBytes !== undefined && size > maxBytes) {
|
|
1394
|
+
tooLarge = true;
|
|
1395
|
+
chunks.length = 0;
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
chunks.push(chunk);
|
|
1399
|
+
});
|
|
1400
|
+
req.on("end", () => {
|
|
1401
|
+
if (tooLarge) {
|
|
1402
|
+
reject(new RequestBodyTooLargeError("request body exceeds size limit"));
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
1406
|
+
});
|
|
1378
1407
|
req.on("error", reject);
|
|
1379
1408
|
});
|
|
1380
1409
|
}
|
|
1410
|
+
async function ensurePrivateReplayDirectory(directory) {
|
|
1411
|
+
try {
|
|
1412
|
+
await fs.mkdir(directory, { mode: 0o700 });
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
if (!error ||
|
|
1416
|
+
typeof error !== "object" ||
|
|
1417
|
+
!("code" in error) ||
|
|
1418
|
+
error.code !== "EEXIST") {
|
|
1419
|
+
throw error;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const stat = await fs.lstat(directory);
|
|
1423
|
+
const processUid = process.getuid?.();
|
|
1424
|
+
if (!stat.isDirectory() ||
|
|
1425
|
+
stat.isSymbolicLink() ||
|
|
1426
|
+
(processUid !== undefined && stat.uid !== processUid) ||
|
|
1427
|
+
(stat.mode & 0o077) !== 0) {
|
|
1428
|
+
throw new Error("bridge capability replay directory is not private");
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function consumeBridgeCapabilityNonce(rootPath, nonce, expiresAt) {
|
|
1432
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(nonce))
|
|
1433
|
+
return false;
|
|
1434
|
+
const projectRoot = await fs.realpath(rootPath);
|
|
1435
|
+
const metadataDirectory = path.join(projectRoot, ".agent-native");
|
|
1436
|
+
await fs.mkdir(metadataDirectory, { recursive: true, mode: 0o700 });
|
|
1437
|
+
const canonicalMetadataDirectory = await fs.realpath(metadataDirectory);
|
|
1438
|
+
if (canonicalMetadataDirectory !== metadataDirectory) {
|
|
1439
|
+
throw new Error("bridge metadata directory escapes the project root");
|
|
1440
|
+
}
|
|
1441
|
+
const projectReplayDirectory = path.join(canonicalMetadataDirectory, "bridge-capability-replays");
|
|
1442
|
+
await ensurePrivateReplayDirectory(projectReplayDirectory);
|
|
1443
|
+
const now = Date.now();
|
|
1444
|
+
let activeEntries = 0;
|
|
1445
|
+
let scannedEntries = 0;
|
|
1446
|
+
const directory = await fs.opendir(projectReplayDirectory);
|
|
1447
|
+
for await (const entry of directory) {
|
|
1448
|
+
scannedEntries += 1;
|
|
1449
|
+
if (scannedEntries > MAX_BRIDGE_CAPABILITY_REPLAY_SCAN) {
|
|
1450
|
+
throw new Error("bridge capability replay store is over capacity");
|
|
1451
|
+
}
|
|
1452
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(entry.name) || !entry.isFile()) {
|
|
1453
|
+
throw new Error("bridge capability replay store contains an invalid entry");
|
|
1454
|
+
}
|
|
1455
|
+
const entryPath = path.join(projectReplayDirectory, entry.name);
|
|
1456
|
+
const stat = await fs.lstat(entryPath);
|
|
1457
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1458
|
+
throw new Error("bridge capability replay store contains an invalid entry");
|
|
1459
|
+
}
|
|
1460
|
+
const storedExpiration = Number(await fs.readFile(entryPath, "utf8"));
|
|
1461
|
+
if (Number.isSafeInteger(storedExpiration) && storedExpiration > now) {
|
|
1462
|
+
activeEntries += 1;
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (!Number.isSafeInteger(storedExpiration) &&
|
|
1466
|
+
stat.mtimeMs > now - BRIDGE_CAPABILITY_REPLAY_ORPHAN_TTL_MS) {
|
|
1467
|
+
activeEntries += 1;
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
await fs.unlink(entryPath);
|
|
1471
|
+
}
|
|
1472
|
+
if (activeEntries >= MAX_BRIDGE_CAPABILITY_REPLAY_ENTRIES) {
|
|
1473
|
+
throw new Error("bridge capability replay store is over capacity");
|
|
1474
|
+
}
|
|
1475
|
+
const replayMarker = path.join(projectReplayDirectory, nonce);
|
|
1476
|
+
let marker;
|
|
1477
|
+
try {
|
|
1478
|
+
marker = await fs.open(replayMarker, "wx", 0o600);
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
if (error &&
|
|
1482
|
+
typeof error === "object" &&
|
|
1483
|
+
"code" in error &&
|
|
1484
|
+
error.code === "EEXIST") {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
throw error;
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
await marker.writeFile(String(expiresAt), "utf8");
|
|
1491
|
+
await marker.sync();
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
await fs.unlink(replayMarker).catch(() => { });
|
|
1495
|
+
throw error;
|
|
1496
|
+
}
|
|
1497
|
+
finally {
|
|
1498
|
+
await marker.close();
|
|
1499
|
+
}
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1381
1502
|
const MAX_LIVE_EDIT_PENDING_PROMPT_LENGTH = 64 * 1024;
|
|
1382
1503
|
const MAX_LIVE_EDIT_PENDING_BYTES = MAX_LIVE_EDIT_PENDING_PROMPT_LENGTH + 8 * 1024;
|
|
1383
1504
|
const MAX_LIVE_EDIT_PENDING_DESIGNS = 32;
|
|
@@ -1641,6 +1762,569 @@ async function resolveSafeBridgeFileTarget(rootPath, targetPath) {
|
|
|
1641
1762
|
}
|
|
1642
1763
|
return { absolutePath, canonicalPath };
|
|
1643
1764
|
}
|
|
1765
|
+
const SOURCE_MODULE_EXTENSIONS = [
|
|
1766
|
+
".tsx",
|
|
1767
|
+
".ts",
|
|
1768
|
+
".jsx",
|
|
1769
|
+
".js",
|
|
1770
|
+
".mts",
|
|
1771
|
+
".cts",
|
|
1772
|
+
".mjs",
|
|
1773
|
+
".cjs",
|
|
1774
|
+
];
|
|
1775
|
+
const MAX_MOTION_IMPORT_GRAPH_MODULES = 512;
|
|
1776
|
+
const MAX_MOTION_IMPORT_GRAPH_BYTES = 12 * 1024 * 1024;
|
|
1777
|
+
const MAX_MOTION_IMPORT_READ_CHUNK_BYTES = 64 * 1024;
|
|
1778
|
+
const NON_SOURCE_IMPORT_EXTENSIONS = new Set([
|
|
1779
|
+
".css",
|
|
1780
|
+
".scss",
|
|
1781
|
+
".sass",
|
|
1782
|
+
".less",
|
|
1783
|
+
".svg",
|
|
1784
|
+
".png",
|
|
1785
|
+
".jpg",
|
|
1786
|
+
".jpeg",
|
|
1787
|
+
".gif",
|
|
1788
|
+
".webp",
|
|
1789
|
+
".ico",
|
|
1790
|
+
".woff",
|
|
1791
|
+
".woff2",
|
|
1792
|
+
".ttf",
|
|
1793
|
+
".otf",
|
|
1794
|
+
".eot",
|
|
1795
|
+
".json",
|
|
1796
|
+
".wasm",
|
|
1797
|
+
]);
|
|
1798
|
+
const MOTION_BRIDGE_RECEIPT_DOMAIN = "agent-native-design-motion-bridge-receipt-v1\0";
|
|
1799
|
+
function sha256String(value) {
|
|
1800
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
1801
|
+
}
|
|
1802
|
+
function failMotionImportGraphByteLimit() {
|
|
1803
|
+
throw new Error("Motion import graph exceeds the 12 MiB verification limit.");
|
|
1804
|
+
}
|
|
1805
|
+
function readMotionImportConfig(filePath, remainingBytes) {
|
|
1806
|
+
if (fsSync.lstatSync(filePath).isSymbolicLink()) {
|
|
1807
|
+
throw new Error(`Symlinked tsconfig/jsconfig files are unsupported for Motion import verification: ${filePath}`);
|
|
1808
|
+
}
|
|
1809
|
+
const noFollow = fsSync.constants.O_NOFOLLOW ?? 0;
|
|
1810
|
+
const descriptor = fsSync.openSync(filePath, fsSync.constants.O_RDONLY | noFollow);
|
|
1811
|
+
try {
|
|
1812
|
+
const stat = fsSync.fstatSync(descriptor);
|
|
1813
|
+
if (!stat.isFile()) {
|
|
1814
|
+
throw new Error(`Motion import config is not a regular file: ${filePath}`);
|
|
1815
|
+
}
|
|
1816
|
+
if (stat.size > remainingBytes)
|
|
1817
|
+
failMotionImportGraphByteLimit();
|
|
1818
|
+
const chunks = [];
|
|
1819
|
+
let bytesRead = 0;
|
|
1820
|
+
let position = 0;
|
|
1821
|
+
while (true) {
|
|
1822
|
+
const buffer = Buffer.alloc(Math.min(MAX_MOTION_IMPORT_READ_CHUNK_BYTES, remainingBytes - bytesRead + 1));
|
|
1823
|
+
const count = fsSync.readSync(descriptor, buffer, 0, buffer.length, position);
|
|
1824
|
+
if (count === 0)
|
|
1825
|
+
break;
|
|
1826
|
+
bytesRead += count;
|
|
1827
|
+
if (bytesRead > remainingBytes)
|
|
1828
|
+
failMotionImportGraphByteLimit();
|
|
1829
|
+
chunks.push(buffer.subarray(0, count));
|
|
1830
|
+
position += count;
|
|
1831
|
+
}
|
|
1832
|
+
return {
|
|
1833
|
+
source: Buffer.concat(chunks, bytesRead).toString("utf8"),
|
|
1834
|
+
bytesRead,
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
finally {
|
|
1838
|
+
fsSync.closeSync(descriptor);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
async function readMotionGraphModule(filePath, remainingBytes) {
|
|
1842
|
+
const noFollow = fsSync.constants.O_NOFOLLOW ?? 0;
|
|
1843
|
+
const handle = await fs.open(filePath, fsSync.constants.O_RDONLY | noFollow);
|
|
1844
|
+
try {
|
|
1845
|
+
const stat = await handle.stat();
|
|
1846
|
+
if (!stat.isFile()) {
|
|
1847
|
+
throw new Error(`Motion source module is not a regular file: ${filePath}`);
|
|
1848
|
+
}
|
|
1849
|
+
if (stat.size > remainingBytes)
|
|
1850
|
+
failMotionImportGraphByteLimit();
|
|
1851
|
+
const chunks = [];
|
|
1852
|
+
let bytesRead = 0;
|
|
1853
|
+
let position = 0;
|
|
1854
|
+
while (true) {
|
|
1855
|
+
const buffer = Buffer.alloc(Math.min(MAX_MOTION_IMPORT_READ_CHUNK_BYTES, remainingBytes - bytesRead + 1));
|
|
1856
|
+
const { bytesRead: count } = await handle.read(buffer, 0, buffer.length, position);
|
|
1857
|
+
if (count === 0)
|
|
1858
|
+
break;
|
|
1859
|
+
bytesRead += count;
|
|
1860
|
+
if (bytesRead > remainingBytes)
|
|
1861
|
+
failMotionImportGraphByteLimit();
|
|
1862
|
+
chunks.push(buffer.subarray(0, count));
|
|
1863
|
+
position += count;
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
source: Buffer.concat(chunks, bytesRead).toString("utf8"),
|
|
1867
|
+
bytesRead,
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
finally {
|
|
1871
|
+
await handle.close();
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function deriveMotionBridgeReceipt(args) {
|
|
1875
|
+
const payloadHash = args.operation === "read-file" ||
|
|
1876
|
+
args.operation === "verify-module-provenance"
|
|
1877
|
+
? ""
|
|
1878
|
+
: typeof args.content === "string"
|
|
1879
|
+
? sha256String(`content\0${args.content}`)
|
|
1880
|
+
: typeof args.search === "string" && typeof args.replace === "string"
|
|
1881
|
+
? sha256String(`patch\0${JSON.stringify([args.search, args.replace])}`)
|
|
1882
|
+
: "";
|
|
1883
|
+
const resultContentHash = args.operation === "read-file" && typeof args.resultContent === "string"
|
|
1884
|
+
? sha256String(args.resultContent)
|
|
1885
|
+
: "";
|
|
1886
|
+
return crypto
|
|
1887
|
+
.createHmac("sha256", args.bridgeToken)
|
|
1888
|
+
.update(MOTION_BRIDGE_RECEIPT_DOMAIN)
|
|
1889
|
+
.update(JSON.stringify([
|
|
1890
|
+
args.challenge,
|
|
1891
|
+
args.operation,
|
|
1892
|
+
args.relPath,
|
|
1893
|
+
args.routePath ?? null,
|
|
1894
|
+
typeof args.reachable === "boolean" ? args.reachable : null,
|
|
1895
|
+
args.expectedVersionHash ?? null,
|
|
1896
|
+
args.requireExpectedVersionHash,
|
|
1897
|
+
payloadHash,
|
|
1898
|
+
args.versionHash ?? "",
|
|
1899
|
+
resultContentHash,
|
|
1900
|
+
]))
|
|
1901
|
+
.digest("hex");
|
|
1902
|
+
}
|
|
1903
|
+
function stripJsonComments(source) {
|
|
1904
|
+
let result = "";
|
|
1905
|
+
let quote = false;
|
|
1906
|
+
let escaped = false;
|
|
1907
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1908
|
+
const char = source[index];
|
|
1909
|
+
const next = source[index + 1];
|
|
1910
|
+
if (quote) {
|
|
1911
|
+
result += char;
|
|
1912
|
+
if (escaped)
|
|
1913
|
+
escaped = false;
|
|
1914
|
+
else if (char === "\\")
|
|
1915
|
+
escaped = true;
|
|
1916
|
+
else if (char === '"')
|
|
1917
|
+
quote = false;
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (char === '"') {
|
|
1921
|
+
quote = true;
|
|
1922
|
+
result += char;
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
if (char === "/" && next === "/") {
|
|
1926
|
+
result += " ";
|
|
1927
|
+
index += 1;
|
|
1928
|
+
while (index + 1 < source.length && source[index + 1] !== "\n") {
|
|
1929
|
+
result += " ";
|
|
1930
|
+
index += 1;
|
|
1931
|
+
}
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (char === "/" && next === "*") {
|
|
1935
|
+
result += " ";
|
|
1936
|
+
index += 1;
|
|
1937
|
+
while (index + 1 < source.length) {
|
|
1938
|
+
if (source[index + 1] === "*" && source[index + 2] === "/") {
|
|
1939
|
+
result += " ";
|
|
1940
|
+
index += 2;
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
const commentChar = source[index + 1];
|
|
1944
|
+
result += commentChar === "\n" ? "\n" : " ";
|
|
1945
|
+
index += 1;
|
|
1946
|
+
}
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
result += char;
|
|
1950
|
+
}
|
|
1951
|
+
let withoutTrailingCommas = "";
|
|
1952
|
+
quote = false;
|
|
1953
|
+
escaped = false;
|
|
1954
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
1955
|
+
const char = result[index];
|
|
1956
|
+
if (quote) {
|
|
1957
|
+
withoutTrailingCommas += char;
|
|
1958
|
+
if (escaped)
|
|
1959
|
+
escaped = false;
|
|
1960
|
+
else if (char === "\\")
|
|
1961
|
+
escaped = true;
|
|
1962
|
+
else if (char === '"')
|
|
1963
|
+
quote = false;
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (char === '"') {
|
|
1967
|
+
quote = true;
|
|
1968
|
+
withoutTrailingCommas += char;
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
if (char === ",") {
|
|
1972
|
+
let next = index + 1;
|
|
1973
|
+
while (/\s/.test(result[next] ?? ""))
|
|
1974
|
+
next += 1;
|
|
1975
|
+
if (result[next] === "}" || result[next] === "]")
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
withoutTrailingCommas += char;
|
|
1979
|
+
}
|
|
1980
|
+
return withoutTrailingCommas;
|
|
1981
|
+
}
|
|
1982
|
+
function readMotionImportAliases(rootPath) {
|
|
1983
|
+
const rootConfig = ["tsconfig.json", "jsconfig.json"]
|
|
1984
|
+
.map((name) => path.join(rootPath, name))
|
|
1985
|
+
.find((candidate) => fsSync.existsSync(candidate));
|
|
1986
|
+
if (!rootConfig)
|
|
1987
|
+
return { baseUrl: rootPath, aliases: [], bytesRead: 0 };
|
|
1988
|
+
const configChain = [];
|
|
1989
|
+
const seen = new Set();
|
|
1990
|
+
let bytesRead = 0;
|
|
1991
|
+
const load = (configPath, depth) => {
|
|
1992
|
+
const canonicalPath = path.resolve(configPath);
|
|
1993
|
+
if (seen.has(canonicalPath)) {
|
|
1994
|
+
throw new Error("Cyclic tsconfig extends chain cannot verify Motion imports.");
|
|
1995
|
+
}
|
|
1996
|
+
if (depth > 12) {
|
|
1997
|
+
throw new Error("tsconfig extends chain is too deep to verify Motion imports.");
|
|
1998
|
+
}
|
|
1999
|
+
seen.add(canonicalPath);
|
|
2000
|
+
const configSource = readMotionImportConfig(canonicalPath, MAX_MOTION_IMPORT_GRAPH_BYTES - bytesRead);
|
|
2001
|
+
bytesRead += configSource.bytesRead;
|
|
2002
|
+
let parsed;
|
|
2003
|
+
try {
|
|
2004
|
+
parsed = JSON.parse(stripJsonComments(configSource.source));
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
throw new Error(`Cannot parse ${path.basename(canonicalPath)} import aliases.`);
|
|
2008
|
+
}
|
|
2009
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2010
|
+
throw new Error(`Malformed ${path.basename(canonicalPath)} import aliases.`);
|
|
2011
|
+
}
|
|
2012
|
+
const config = parsed;
|
|
2013
|
+
if (config.extends !== undefined) {
|
|
2014
|
+
if (typeof config.extends !== "string") {
|
|
2015
|
+
throw new Error("Unsupported tsconfig extends form for Motion import verification.");
|
|
2016
|
+
}
|
|
2017
|
+
const parent = resolveTsConfigExtends(canonicalPath, config.extends, rootPath);
|
|
2018
|
+
load(parent, depth + 1);
|
|
2019
|
+
}
|
|
2020
|
+
configChain.push({ path: canonicalPath, config });
|
|
2021
|
+
};
|
|
2022
|
+
load(rootConfig, 0);
|
|
2023
|
+
let baseUrl = rootPath;
|
|
2024
|
+
const aliases = new Map();
|
|
2025
|
+
for (const { path: configPath, config } of configChain) {
|
|
2026
|
+
const options = config.compilerOptions ?? {};
|
|
2027
|
+
if (typeof options.baseUrl === "string") {
|
|
2028
|
+
baseUrl = path.resolve(path.dirname(configPath), options.baseUrl);
|
|
2029
|
+
}
|
|
2030
|
+
if (!options.paths ||
|
|
2031
|
+
typeof options.paths !== "object" ||
|
|
2032
|
+
Array.isArray(options.paths)) {
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
aliases.clear();
|
|
2036
|
+
for (const [pattern, rawTargets] of Object.entries(options.paths)) {
|
|
2037
|
+
const targets = Array.isArray(rawTargets)
|
|
2038
|
+
? rawTargets.filter((target) => typeof target === "string")
|
|
2039
|
+
: [];
|
|
2040
|
+
if (targets.length > 0)
|
|
2041
|
+
aliases.set(pattern, { pattern, targets, baseUrl });
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const sortedAliases = [...aliases.values()].sort((left, right) => right.pattern.length - left.pattern.length);
|
|
2045
|
+
return { baseUrl, aliases: sortedAliases, bytesRead };
|
|
2046
|
+
}
|
|
2047
|
+
function resolveTsConfigExtends(configPath, specifier, rootPath) {
|
|
2048
|
+
if (path.isAbsolute(specifier))
|
|
2049
|
+
return resolveTsConfigFile(specifier);
|
|
2050
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
2051
|
+
return resolveTsConfigFile(path.resolve(path.dirname(configPath), specifier));
|
|
2052
|
+
}
|
|
2053
|
+
try {
|
|
2054
|
+
return createRequire(import.meta.url).resolve(specifier, {
|
|
2055
|
+
paths: [path.dirname(configPath), rootPath],
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
catch {
|
|
2059
|
+
throw new Error(`Cannot resolve tsconfig extends "${specifier}" for Motion imports.`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
function resolveTsConfigFile(candidate) {
|
|
2063
|
+
const options = [
|
|
2064
|
+
candidate,
|
|
2065
|
+
`${candidate}.json`,
|
|
2066
|
+
path.join(candidate, "tsconfig.json"),
|
|
2067
|
+
];
|
|
2068
|
+
const resolved = options.find((option) => fsSync.existsSync(option));
|
|
2069
|
+
if (!resolved)
|
|
2070
|
+
throw new Error(`Cannot resolve inherited tsconfig ${candidate}.`);
|
|
2071
|
+
return resolved;
|
|
2072
|
+
}
|
|
2073
|
+
function aliasTargetsForSpecifier(specifier, aliases) {
|
|
2074
|
+
for (const alias of aliases) {
|
|
2075
|
+
const starIndex = alias.pattern.indexOf("*");
|
|
2076
|
+
if (starIndex === -1) {
|
|
2077
|
+
if (specifier !== alias.pattern)
|
|
2078
|
+
continue;
|
|
2079
|
+
return alias;
|
|
2080
|
+
}
|
|
2081
|
+
const prefix = alias.pattern.slice(0, starIndex);
|
|
2082
|
+
const suffix = alias.pattern.slice(starIndex + 1);
|
|
2083
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix))
|
|
2084
|
+
continue;
|
|
2085
|
+
const capture = specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
2086
|
+
return {
|
|
2087
|
+
...alias,
|
|
2088
|
+
targets: alias.targets.map((target) => target.replace("*", capture)),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
function isInsideRoot(rootPath, targetPath) {
|
|
2094
|
+
const relative = path.relative(rootPath, targetPath);
|
|
2095
|
+
return (relative === "" ||
|
|
2096
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
2097
|
+
relative !== ".." &&
|
|
2098
|
+
!path.isAbsolute(relative)));
|
|
2099
|
+
}
|
|
2100
|
+
async function resolveSourceModule(rootPath, importerPath, specifier, aliasConfig) {
|
|
2101
|
+
const alias = aliasTargetsForSpecifier(specifier, aliasConfig.aliases);
|
|
2102
|
+
const isRelative = specifier.startsWith("./") || specifier.startsWith("../");
|
|
2103
|
+
if (!isRelative && !alias) {
|
|
2104
|
+
if (specifier.startsWith("/") ||
|
|
2105
|
+
/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)) {
|
|
2106
|
+
throw new Error(`Unsupported source import: ${specifier}`);
|
|
2107
|
+
}
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
const bases = isRelative
|
|
2111
|
+
? [path.resolve(path.dirname(importerPath), specifier)]
|
|
2112
|
+
: alias.targets.map((target) => path.resolve(alias.baseUrl, target));
|
|
2113
|
+
const explicitExtension = path.extname(specifier).toLowerCase();
|
|
2114
|
+
if (explicitExtension &&
|
|
2115
|
+
!SOURCE_MODULE_EXTENSIONS.includes(explicitExtension)) {
|
|
2116
|
+
if (NON_SOURCE_IMPORT_EXTENSIONS.has(explicitExtension))
|
|
2117
|
+
return null;
|
|
2118
|
+
throw new Error(`Unsupported source import extension: ${specifier}`);
|
|
2119
|
+
}
|
|
2120
|
+
const candidates = bases.flatMap((base) => explicitExtension
|
|
2121
|
+
? [base]
|
|
2122
|
+
: [
|
|
2123
|
+
base,
|
|
2124
|
+
...SOURCE_MODULE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2125
|
+
...SOURCE_MODULE_EXTENSIONS.map((extension) => path.join(base, `index${extension}`)),
|
|
2126
|
+
]);
|
|
2127
|
+
for (const candidate of candidates) {
|
|
2128
|
+
if (!isInsideRoot(rootPath, candidate)) {
|
|
2129
|
+
throw new Error(`Source import escapes the connected project: ${specifier}`);
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
const resolved = await resolveSafeBridgeFileTarget(rootPath, path.relative(rootPath, candidate)).then(async (target) => {
|
|
2133
|
+
const stat = await fs.stat(target.absolutePath);
|
|
2134
|
+
return stat.isFile() ? target : null;
|
|
2135
|
+
});
|
|
2136
|
+
if (resolved)
|
|
2137
|
+
return resolved;
|
|
2138
|
+
}
|
|
2139
|
+
catch (error) {
|
|
2140
|
+
if (error instanceof Error && /not found|ENOENT/i.test(error.message))
|
|
2141
|
+
continue;
|
|
2142
|
+
throw error;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (alias?.pattern === "*")
|
|
2146
|
+
return null;
|
|
2147
|
+
throw new Error(`Cannot resolve source import "${specifier}" from ${importerPath}`);
|
|
2148
|
+
}
|
|
2149
|
+
function moduleDependencies(source, filePath) {
|
|
2150
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
2151
|
+
let ast;
|
|
2152
|
+
try {
|
|
2153
|
+
ast = parse(source, {
|
|
2154
|
+
sourceType: "unambiguous",
|
|
2155
|
+
plugins: extension === ".tsx" || extension === ".jsx"
|
|
2156
|
+
? ["jsx", ...(extension === ".tsx" ? ["typescript"] : [])]
|
|
2157
|
+
: ["typescript", "jsx"],
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
catch (error) {
|
|
2161
|
+
throw new Error(`Cannot parse source module ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2162
|
+
}
|
|
2163
|
+
const dependencies = new Set();
|
|
2164
|
+
const addStaticSource = (value) => {
|
|
2165
|
+
if (value &&
|
|
2166
|
+
typeof value === "object" &&
|
|
2167
|
+
value.type === "StringLiteral") {
|
|
2168
|
+
const literal = value.value;
|
|
2169
|
+
if (typeof literal === "string")
|
|
2170
|
+
dependencies.add(literal);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
if (value &&
|
|
2174
|
+
typeof value === "object" &&
|
|
2175
|
+
value.type === "Literal") {
|
|
2176
|
+
const literal = value.value;
|
|
2177
|
+
if (typeof literal === "string")
|
|
2178
|
+
dependencies.add(literal);
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
throw new Error(`Unresolved dynamic import in ${filePath}`);
|
|
2182
|
+
};
|
|
2183
|
+
const visit = (node) => {
|
|
2184
|
+
if (!node || typeof node !== "object")
|
|
2185
|
+
return;
|
|
2186
|
+
if (Array.isArray(node)) {
|
|
2187
|
+
for (const child of node)
|
|
2188
|
+
visit(child);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const record = node;
|
|
2192
|
+
const type = record.type;
|
|
2193
|
+
const sourceSpecifiers = Array.isArray(record.specifiers)
|
|
2194
|
+
? record.specifiers.filter((specifier) => Boolean(specifier && typeof specifier === "object"))
|
|
2195
|
+
: [];
|
|
2196
|
+
const typeOnlyImport = type === "ImportDeclaration" &&
|
|
2197
|
+
(record.importKind === "type" ||
|
|
2198
|
+
(sourceSpecifiers.length > 0 &&
|
|
2199
|
+
sourceSpecifiers.every((specifier) => specifier.importKind === "type")));
|
|
2200
|
+
const typeOnlyExport = (type === "ExportNamedDeclaration" || type === "ExportAllDeclaration") &&
|
|
2201
|
+
(record.exportKind === "type" ||
|
|
2202
|
+
(type === "ExportNamedDeclaration" &&
|
|
2203
|
+
sourceSpecifiers.length > 0 &&
|
|
2204
|
+
sourceSpecifiers.every((specifier) => specifier.exportKind === "type")));
|
|
2205
|
+
if ((type === "ImportDeclaration" ||
|
|
2206
|
+
type === "ExportNamedDeclaration" ||
|
|
2207
|
+
type === "ExportAllDeclaration") &&
|
|
2208
|
+
record.source !== null &&
|
|
2209
|
+
record.source !== undefined &&
|
|
2210
|
+
!typeOnlyImport &&
|
|
2211
|
+
!typeOnlyExport) {
|
|
2212
|
+
addStaticSource(record.source);
|
|
2213
|
+
}
|
|
2214
|
+
else if (type === "TSImportEqualsDeclaration") {
|
|
2215
|
+
const moduleReference = record.moduleReference;
|
|
2216
|
+
if (record.importKind !== "type" &&
|
|
2217
|
+
moduleReference?.type === "TSExternalModuleReference") {
|
|
2218
|
+
addStaticSource(moduleReference.expression);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
else if (type === "ImportExpression") {
|
|
2222
|
+
addStaticSource(record.source);
|
|
2223
|
+
}
|
|
2224
|
+
else if (type === "CallExpression") {
|
|
2225
|
+
const callee = record.callee;
|
|
2226
|
+
const calleeObject = callee?.type === "MemberExpression"
|
|
2227
|
+
? callee.object
|
|
2228
|
+
: undefined;
|
|
2229
|
+
const calleeProperty = callee?.type === "MemberExpression"
|
|
2230
|
+
? callee.property
|
|
2231
|
+
: undefined;
|
|
2232
|
+
const memberName = calleeProperty?.name ?? calleeProperty?.value;
|
|
2233
|
+
const isImportMeta = calleeObject?.type === "MetaProperty" &&
|
|
2234
|
+
calleeObject.meta?.name ===
|
|
2235
|
+
"import" &&
|
|
2236
|
+
calleeObject.property?.name ===
|
|
2237
|
+
"meta";
|
|
2238
|
+
if (callee?.type === "Import") {
|
|
2239
|
+
const args = Array.isArray(record.arguments) ? record.arguments : [];
|
|
2240
|
+
if (args.length !== 1)
|
|
2241
|
+
throw new Error(`Unresolved dynamic import in ${filePath}`);
|
|
2242
|
+
addStaticSource(args[0]);
|
|
2243
|
+
}
|
|
2244
|
+
else if (callee?.type === "Identifier" && callee.name === "require") {
|
|
2245
|
+
const args = Array.isArray(record.arguments) ? record.arguments : [];
|
|
2246
|
+
if (args.length !== 1)
|
|
2247
|
+
throw new Error(`Unresolved dynamic require in ${filePath}`);
|
|
2248
|
+
addStaticSource(args[0]);
|
|
2249
|
+
}
|
|
2250
|
+
else if (callee?.type === "MemberExpression" &&
|
|
2251
|
+
isImportMeta &&
|
|
2252
|
+
(memberName === "glob" || memberName === "globEager")) {
|
|
2253
|
+
throw new Error(`Unresolved import.meta.glob dependency in ${filePath}`);
|
|
2254
|
+
}
|
|
2255
|
+
else if (callee?.type === "MemberExpression" &&
|
|
2256
|
+
calleeObject?.type === "Identifier" &&
|
|
2257
|
+
calleeObject.name === "require" &&
|
|
2258
|
+
memberName === "context") {
|
|
2259
|
+
throw new Error(`Unresolved require.context dependency in ${filePath}`);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
for (const [key, child] of Object.entries(record)) {
|
|
2263
|
+
if (key === "loc" ||
|
|
2264
|
+
key === "start" ||
|
|
2265
|
+
key === "end" ||
|
|
2266
|
+
key === "tokens" ||
|
|
2267
|
+
key === "comments")
|
|
2268
|
+
continue;
|
|
2269
|
+
visit(child);
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
visit(ast);
|
|
2273
|
+
return [...dependencies];
|
|
2274
|
+
}
|
|
2275
|
+
/** Static project-rooted import graph used only to authorize a Motion source module. */
|
|
2276
|
+
export async function isMotionModuleReachableFromRoute(args) {
|
|
2277
|
+
const rootPath = await fs.realpath(args.rootPath);
|
|
2278
|
+
const isSafeSourcePath = (value) => value.length > 0 &&
|
|
2279
|
+
!path.isAbsolute(value) &&
|
|
2280
|
+
!value.includes("\\") &&
|
|
2281
|
+
!/[\u0000-\u001f?#]/.test(value) &&
|
|
2282
|
+
value
|
|
2283
|
+
.split("/")
|
|
2284
|
+
.every((segment) => segment && segment !== "." && segment !== "..");
|
|
2285
|
+
if (!isSafeSourcePath(args.routePath) || !isSafeSourcePath(args.modulePath)) {
|
|
2286
|
+
throw new Error("Motion source paths must be project-relative files.");
|
|
2287
|
+
}
|
|
2288
|
+
const route = await resolveSafeBridgeFileTarget(rootPath, args.routePath);
|
|
2289
|
+
const target = await resolveSafeBridgeFileTarget(rootPath, args.modulePath);
|
|
2290
|
+
const routeIsHtml = /\.html?$/i.test(args.routePath);
|
|
2291
|
+
if (routeIsHtml) {
|
|
2292
|
+
if (!/\.html?$/i.test(args.modulePath)) {
|
|
2293
|
+
throw new Error("HTML Motion source writes must target the linked HTML entrypoint.");
|
|
2294
|
+
}
|
|
2295
|
+
return route.canonicalPath === target.canonicalPath;
|
|
2296
|
+
}
|
|
2297
|
+
if (!/\.(?:tsx|jsx)$/i.test(args.routePath) ||
|
|
2298
|
+
!/\.(?:tsx|jsx)$/i.test(args.modulePath)) {
|
|
2299
|
+
throw new Error("React Motion source paths must be project-relative JSX modules.");
|
|
2300
|
+
}
|
|
2301
|
+
const targetCanonical = target.canonicalPath;
|
|
2302
|
+
const aliases = readMotionImportAliases(rootPath);
|
|
2303
|
+
const visited = new Set();
|
|
2304
|
+
const pending = [route];
|
|
2305
|
+
let reachable = false;
|
|
2306
|
+
let sourceBytes = aliases.bytesRead;
|
|
2307
|
+
while (pending.length > 0) {
|
|
2308
|
+
const current = pending.pop();
|
|
2309
|
+
if (visited.has(current.canonicalPath))
|
|
2310
|
+
continue;
|
|
2311
|
+
if (visited.size >= MAX_MOTION_IMPORT_GRAPH_MODULES) {
|
|
2312
|
+
throw new Error("Motion import graph exceeds the 512-module verification limit.");
|
|
2313
|
+
}
|
|
2314
|
+
visited.add(current.canonicalPath);
|
|
2315
|
+
if (current.canonicalPath === targetCanonical)
|
|
2316
|
+
reachable = true;
|
|
2317
|
+
const moduleSource = await readMotionGraphModule(current.absolutePath, MAX_MOTION_IMPORT_GRAPH_BYTES - sourceBytes);
|
|
2318
|
+
sourceBytes += moduleSource.bytesRead;
|
|
2319
|
+
const { source } = moduleSource;
|
|
2320
|
+
for (const specifier of moduleDependencies(source, current.absolutePath)) {
|
|
2321
|
+
const dependency = await resolveSourceModule(rootPath, current.absolutePath, specifier, aliases);
|
|
2322
|
+
if (dependency)
|
|
2323
|
+
pending.push(dependency);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return reachable;
|
|
2327
|
+
}
|
|
1644
2328
|
const bridgeWriteLocks = new Map();
|
|
1645
2329
|
async function withBridgeWriteLock(canonicalPath, work) {
|
|
1646
2330
|
const previous = bridgeWriteLocks.get(canonicalPath) ?? Promise.resolve();
|
|
@@ -2677,6 +3361,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2677
3361
|
if (pathname === "/read-file" ||
|
|
2678
3362
|
pathname === "/write-file" ||
|
|
2679
3363
|
pathname === "/apply-edit" ||
|
|
3364
|
+
pathname === "/verify-module-provenance" ||
|
|
2680
3365
|
pathname === "/list-files") {
|
|
2681
3366
|
if (req.method !== "POST") {
|
|
2682
3367
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
@@ -2684,7 +3369,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2684
3369
|
}
|
|
2685
3370
|
const providedToken = readHeader(req, "x-bridge-token");
|
|
2686
3371
|
const tokenValid = constantTimeTokenMatches(providedToken, bridgeToken);
|
|
2687
|
-
if (!tokenValid) {
|
|
3372
|
+
if (providedToken && !tokenValid) {
|
|
2688
3373
|
sendJson(res, 401, {
|
|
2689
3374
|
ok: false,
|
|
2690
3375
|
error: "invalid or missing bridge token",
|
|
@@ -2693,8 +3378,58 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2693
3378
|
}
|
|
2694
3379
|
void (async () => {
|
|
2695
3380
|
try {
|
|
2696
|
-
const raw = await readRequestBody(req);
|
|
2697
|
-
const
|
|
3381
|
+
const raw = await readRequestBody(req, tokenValid ? undefined : MAX_BRIDGE_OPERATION_BODY_BYTES);
|
|
3382
|
+
const parsedBody = JSON.parse(raw);
|
|
3383
|
+
if (!parsedBody ||
|
|
3384
|
+
typeof parsedBody !== "object" ||
|
|
3385
|
+
Array.isArray(parsedBody)) {
|
|
3386
|
+
sendJson(res, tokenValid ? 400 : 401, {
|
|
3387
|
+
ok: false,
|
|
3388
|
+
error: tokenValid
|
|
3389
|
+
? "request body must be a JSON object"
|
|
3390
|
+
: "invalid or missing bridge token",
|
|
3391
|
+
});
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const body = parsedBody;
|
|
3395
|
+
if (!tokenValid) {
|
|
3396
|
+
const capability = body[LOCALHOST_BRIDGE_OPERATION_CAPABILITY_FIELD];
|
|
3397
|
+
const verified = typeof capability === "string"
|
|
3398
|
+
? verifyLocalhostBridgeOperationCapability({
|
|
3399
|
+
bridgeToken,
|
|
3400
|
+
audience: createLocalhostBridgeAudience(manifest.rootPath),
|
|
3401
|
+
operation: `POST ${pathname}`,
|
|
3402
|
+
body,
|
|
3403
|
+
capability,
|
|
3404
|
+
})
|
|
3405
|
+
: null;
|
|
3406
|
+
if (!verified) {
|
|
3407
|
+
sendJson(res, 401, {
|
|
3408
|
+
ok: false,
|
|
3409
|
+
error: "invalid or missing bridge operation capability",
|
|
3410
|
+
});
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
let consumed;
|
|
3414
|
+
try {
|
|
3415
|
+
consumed = await consumeBridgeCapabilityNonce(manifest.rootPath, verified.nonce, verified.expiresAt);
|
|
3416
|
+
}
|
|
3417
|
+
catch {
|
|
3418
|
+
sendJson(res, 503, {
|
|
3419
|
+
ok: false,
|
|
3420
|
+
error: "bridge capability replay protection unavailable",
|
|
3421
|
+
});
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
if (!consumed) {
|
|
3425
|
+
sendJson(res, 401, {
|
|
3426
|
+
ok: false,
|
|
3427
|
+
error: "bridge operation capability already used",
|
|
3428
|
+
});
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
delete body[LOCALHOST_BRIDGE_OPERATION_CAPABILITY_FIELD];
|
|
2698
3433
|
if (pathname === "/list-files") {
|
|
2699
3434
|
try {
|
|
2700
3435
|
const result = await walkBridgeFiles(manifest.rootPath);
|
|
@@ -2712,6 +3447,60 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2712
3447
|
}
|
|
2713
3448
|
return;
|
|
2714
3449
|
}
|
|
3450
|
+
if (pathname === "/verify-module-provenance") {
|
|
3451
|
+
const routePath = typeof body["routePath"] === "string"
|
|
3452
|
+
? body["routePath"]
|
|
3453
|
+
: undefined;
|
|
3454
|
+
const modulePath = typeof body["modulePath"] === "string"
|
|
3455
|
+
? body["modulePath"]
|
|
3456
|
+
: undefined;
|
|
3457
|
+
const challenge = typeof body["motionRelayChallenge"] === "string"
|
|
3458
|
+
? body["motionRelayChallenge"]
|
|
3459
|
+
: undefined;
|
|
3460
|
+
if (!routePath ||
|
|
3461
|
+
!modulePath ||
|
|
3462
|
+
!challenge ||
|
|
3463
|
+
challenge.length > 4096) {
|
|
3464
|
+
sendJson(res, 400, {
|
|
3465
|
+
ok: false,
|
|
3466
|
+
error: "routePath, modulePath, and a Motion receipt challenge are required",
|
|
3467
|
+
});
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
const normalizedRoutePath = routePath.replace(/\\/g, "/");
|
|
3471
|
+
const routeIsRegistered = manifest.routes.some((route) => typeof route.sourceFile === "string" &&
|
|
3472
|
+
route.sourceFile.replace(/\\/g, "/") === normalizedRoutePath);
|
|
3473
|
+
if (!routeIsRegistered) {
|
|
3474
|
+
sendJson(res, 403, {
|
|
3475
|
+
ok: false,
|
|
3476
|
+
error: "routePath is not a saved route in this connected project",
|
|
3477
|
+
});
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
assertNotBlockedSecretPath(routePath);
|
|
3481
|
+
assertNotBlockedSecretPath(modulePath);
|
|
3482
|
+
const reachable = await isMotionModuleReachableFromRoute({
|
|
3483
|
+
rootPath: manifest.rootPath,
|
|
3484
|
+
routePath,
|
|
3485
|
+
modulePath,
|
|
3486
|
+
});
|
|
3487
|
+
sendJson(res, 200, {
|
|
3488
|
+
ok: true,
|
|
3489
|
+
routePath: normalizedRoutePath,
|
|
3490
|
+
modulePath: modulePath.replace(/\\/g, "/"),
|
|
3491
|
+
reachable,
|
|
3492
|
+
operationReceipt: deriveMotionBridgeReceipt({
|
|
3493
|
+
bridgeToken,
|
|
3494
|
+
challenge,
|
|
3495
|
+
operation: "verify-module-provenance",
|
|
3496
|
+
relPath: modulePath.replace(/\\/g, "/"),
|
|
3497
|
+
routePath: normalizedRoutePath,
|
|
3498
|
+
reachable,
|
|
3499
|
+
requireExpectedVersionHash: false,
|
|
3500
|
+
}),
|
|
3501
|
+
});
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
2715
3504
|
const relPath = typeof body["relPath"] === "string" ? body["relPath"] : undefined;
|
|
2716
3505
|
if (!relPath) {
|
|
2717
3506
|
sendJson(res, 400, { ok: false, error: "relPath is required" });
|
|
@@ -2725,10 +3514,27 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2725
3514
|
sendJson(res, 404, { ok: false, error: "file not found" });
|
|
2726
3515
|
return;
|
|
2727
3516
|
}
|
|
3517
|
+
const challenge = typeof body["motionRelayChallenge"] === "string" &&
|
|
3518
|
+
body["motionRelayChallenge"].length <= 4096
|
|
3519
|
+
? body["motionRelayChallenge"]
|
|
3520
|
+
: undefined;
|
|
2728
3521
|
sendJson(res, 200, {
|
|
2729
3522
|
ok: true,
|
|
2730
3523
|
content: snapshot.content,
|
|
2731
3524
|
versionHash: snapshot.versionHash,
|
|
3525
|
+
...(challenge
|
|
3526
|
+
? {
|
|
3527
|
+
operationReceipt: deriveMotionBridgeReceipt({
|
|
3528
|
+
bridgeToken,
|
|
3529
|
+
challenge,
|
|
3530
|
+
operation: "read-file",
|
|
3531
|
+
relPath: relPath.replace(/\\/g, "/"),
|
|
3532
|
+
requireExpectedVersionHash: false,
|
|
3533
|
+
versionHash: snapshot.versionHash,
|
|
3534
|
+
resultContent: snapshot.content,
|
|
3535
|
+
}),
|
|
3536
|
+
}
|
|
3537
|
+
: {}),
|
|
2732
3538
|
});
|
|
2733
3539
|
return;
|
|
2734
3540
|
}
|
|
@@ -2736,6 +3542,10 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2736
3542
|
? body["expectedVersionHash"]
|
|
2737
3543
|
: undefined;
|
|
2738
3544
|
const requireExpectedVersionHash = body["requireExpectedVersionHash"] === true;
|
|
3545
|
+
const motionRelayChallenge = typeof body["motionRelayChallenge"] === "string" &&
|
|
3546
|
+
body["motionRelayChallenge"].length <= 4096
|
|
3547
|
+
? body["motionRelayChallenge"]
|
|
3548
|
+
: undefined;
|
|
2739
3549
|
await withBridgeWriteLock(initialTarget.canonicalPath, async () => {
|
|
2740
3550
|
const lockedTarget = await resolveSafeBridgeFileTarget(manifest.rootPath, relPath);
|
|
2741
3551
|
if (lockedTarget.canonicalPath !== initialTarget.canonicalPath) {
|
|
@@ -2764,7 +3574,25 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2764
3574
|
requireExpectedVersionHash,
|
|
2765
3575
|
originalMode: existing?.mode,
|
|
2766
3576
|
});
|
|
2767
|
-
sendJson(res, 200, {
|
|
3577
|
+
sendJson(res, 200, {
|
|
3578
|
+
ok: true,
|
|
3579
|
+
relPath,
|
|
3580
|
+
versionHash,
|
|
3581
|
+
...(motionRelayChallenge
|
|
3582
|
+
? {
|
|
3583
|
+
operationReceipt: deriveMotionBridgeReceipt({
|
|
3584
|
+
bridgeToken,
|
|
3585
|
+
challenge: motionRelayChallenge,
|
|
3586
|
+
operation: "write-file",
|
|
3587
|
+
relPath: relPath.replace(/\\/g, "/"),
|
|
3588
|
+
expectedVersionHash,
|
|
3589
|
+
requireExpectedVersionHash,
|
|
3590
|
+
content,
|
|
3591
|
+
versionHash,
|
|
3592
|
+
}),
|
|
3593
|
+
}
|
|
3594
|
+
: {}),
|
|
3595
|
+
});
|
|
2768
3596
|
return;
|
|
2769
3597
|
}
|
|
2770
3598
|
if (typeof body["content"] === "string") {
|
|
@@ -2782,6 +3610,20 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2782
3610
|
relPath,
|
|
2783
3611
|
method: "replace",
|
|
2784
3612
|
versionHash,
|
|
3613
|
+
...(motionRelayChallenge
|
|
3614
|
+
? {
|
|
3615
|
+
operationReceipt: deriveMotionBridgeReceipt({
|
|
3616
|
+
bridgeToken,
|
|
3617
|
+
challenge: motionRelayChallenge,
|
|
3618
|
+
operation: "apply-edit",
|
|
3619
|
+
relPath: relPath.replace(/\\/g, "/"),
|
|
3620
|
+
expectedVersionHash,
|
|
3621
|
+
requireExpectedVersionHash,
|
|
3622
|
+
content: body["content"],
|
|
3623
|
+
versionHash,
|
|
3624
|
+
}),
|
|
3625
|
+
}
|
|
3626
|
+
: {}),
|
|
2785
3627
|
});
|
|
2786
3628
|
return;
|
|
2787
3629
|
}
|
|
@@ -2840,10 +3682,39 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
|
2840
3682
|
relPath,
|
|
2841
3683
|
method: "patch",
|
|
2842
3684
|
versionHash,
|
|
3685
|
+
...(motionRelayChallenge
|
|
3686
|
+
? {
|
|
3687
|
+
operationReceipt: deriveMotionBridgeReceipt({
|
|
3688
|
+
bridgeToken,
|
|
3689
|
+
challenge: motionRelayChallenge,
|
|
3690
|
+
operation: "apply-edit",
|
|
3691
|
+
relPath: relPath.replace(/\\/g, "/"),
|
|
3692
|
+
expectedVersionHash,
|
|
3693
|
+
requireExpectedVersionHash,
|
|
3694
|
+
search,
|
|
3695
|
+
replace,
|
|
3696
|
+
versionHash,
|
|
3697
|
+
}),
|
|
3698
|
+
}
|
|
3699
|
+
: {}),
|
|
2843
3700
|
});
|
|
2844
3701
|
});
|
|
2845
3702
|
}
|
|
2846
3703
|
catch (err) {
|
|
3704
|
+
if (err instanceof RequestBodyTooLargeError) {
|
|
3705
|
+
sendJson(res, 413, {
|
|
3706
|
+
ok: false,
|
|
3707
|
+
error: "bridge operation body exceeds the size limit",
|
|
3708
|
+
});
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
if (!tokenValid) {
|
|
3712
|
+
sendJson(res, 401, {
|
|
3713
|
+
ok: false,
|
|
3714
|
+
error: "invalid or missing bridge operation capability",
|
|
3715
|
+
});
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
2847
3718
|
if (err instanceof BridgePreconditionRequiredError) {
|
|
2848
3719
|
sendJson(res, 428, {
|
|
2849
3720
|
ok: false,
|