@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.
Files changed (51) hide show
  1. package/dist/agent/production-agent.js +2 -0
  2. package/dist/cli/design-connect.d.ts +6 -0
  3. package/dist/cli/design-connect.js +880 -9
  4. package/dist/client/action-query-cache.d.ts +3 -1
  5. package/dist/client/action-query-cache.js +628 -161
  6. package/dist/client/action-query-cache.session-test-fixture.d.ts +49 -0
  7. package/dist/client/action-query-cache.session-test-fixture.js +151 -0
  8. package/dist/client/action-query-cache.test-fixtures.d.ts +2 -0
  9. package/dist/client/action-query-cache.test-fixtures.js +56 -0
  10. package/dist/client/motion-text.css +3 -0
  11. package/dist/client/motion-text.d.ts +38 -0
  12. package/dist/client/motion-text.js +551 -0
  13. package/dist/client/org/hooks.d.ts +3 -1
  14. package/dist/client/org/hooks.js +8 -2
  15. package/dist/client/route-chunk-recovery.d.ts +2 -0
  16. package/dist/client/route-chunk-recovery.js +11 -0
  17. package/dist/client/use-action.d.ts +10 -2
  18. package/dist/client/use-action.js +81 -12
  19. package/dist/client/use-settings-shortcut.js +8 -3
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/routes.d.ts +1 -1
  22. package/dist/collab/struct-routes.d.ts +1 -1
  23. package/dist/extensions/fetch-tool.js +8 -2
  24. package/dist/extensions/routes.js +7 -3
  25. package/dist/extensions/web-content.js +17 -2
  26. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  27. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  28. package/dist/mcp/build-server.js +5 -0
  29. package/dist/observability/routes.d.ts +4 -4
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/resources/handlers.d.ts +1 -1
  32. package/dist/secrets/routes.d.ts +5 -5
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  35. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/dist/server/transcribe-voice.d.ts +1 -1
  38. package/dist/server/visual-edit-bridge-capability.d.ts +26 -0
  39. package/dist/server/visual-edit-bridge-capability.js +233 -0
  40. package/dist/shared/motion-easing.d.ts +6 -0
  41. package/dist/shared/motion-easing.js +109 -0
  42. package/dist/shared/motion-source-node-id.d.ts +3 -0
  43. package/dist/shared/motion-source-node-id.js +12 -0
  44. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  45. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
  46. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  47. package/dist/vite/client.js +2 -0
  48. package/dist/vite/motion-source-provenance-plugin.d.ts +3 -0
  49. package/dist/vite/motion-source-provenance-plugin.js +339 -0
  50. package/package.json +14 -5
  51. 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 (!file.endsWith(".html"))
389
+ if (!/\.html?$/i.test(file))
383
390
  continue;
384
391
  const rel = normalizeSlash(path.relative(dir, file));
385
- const pathName = rel === "index.html" ? "/" : `/${rel.replace(/\.html$/, "")}`;
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
- async function readRequestBody(req) {
1382
+ class RequestBodyTooLargeError extends Error {
1383
+ }
1384
+ async function readRequestBody(req, maxBytes) {
1374
1385
  return new Promise((resolve, reject) => {
1375
1386
  const chunks = [];
1376
- req.on("data", (chunk) => chunks.push(chunk));
1377
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
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 body = JSON.parse(raw);
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, { ok: true, relPath, versionHash });
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,