@chrrxs/robloxstudio-mcp-inspector 2.20.0 → 2.21.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/dist/index.js CHANGED
@@ -653,9 +653,57 @@ var init_mcp_compat = __esm({
653
653
  }
654
654
  });
655
655
 
656
+ // ../core/dist/auth.js
657
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
658
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
659
+ import { join, dirname } from "path";
660
+ import { homedir } from "os";
661
+ function authTokenFilePath() {
662
+ return join(homedir(), ".robloxstudio-mcp", "auth-token");
663
+ }
664
+ function resolveAuthToken() {
665
+ const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
666
+ if (noAuth === "1" || noAuth === "true") {
667
+ return { source: "disabled" };
668
+ }
669
+ const envToken = process.env.ROBLOX_STUDIO_AUTH_TOKEN?.trim();
670
+ if (envToken) {
671
+ return { token: envToken, source: "env" };
672
+ }
673
+ const filePath = authTokenFilePath();
674
+ try {
675
+ if (existsSync(filePath)) {
676
+ const existing = readFileSync(filePath, "utf8").trim();
677
+ if (existing) {
678
+ return { token: existing, source: "file", filePath };
679
+ }
680
+ }
681
+ const fresh = randomBytes(32).toString("hex");
682
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
683
+ writeFileSync(filePath, fresh + "\n", { encoding: "utf8", mode: 384 });
684
+ try {
685
+ chmodSync(filePath, 384);
686
+ } catch {
687
+ }
688
+ return { token: fresh, source: "file", filePath };
689
+ } catch (err) {
690
+ console.error(`[auth] Could not read/create ${filePath} (${err instanceof Error ? err.message : err}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
691
+ return { token: randomBytes(32).toString("hex"), source: "file" };
692
+ }
693
+ }
694
+ function tokensMatch(provided, expected) {
695
+ const a = createHash("sha256").update(provided).digest();
696
+ const b = createHash("sha256").update(expected).digest();
697
+ return timingSafeEqual(a, b);
698
+ }
699
+ var init_auth = __esm({
700
+ "../core/dist/auth.js"() {
701
+ "use strict";
702
+ }
703
+ });
704
+
656
705
  // ../core/dist/http-server.js
657
706
  import express from "express";
658
- import cors from "cors";
659
707
  import http from "http";
660
708
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
661
709
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -708,7 +756,7 @@ function requiredClosedLineRange(body, toolName) {
708
756
  }
709
757
  return { startLine: parsed.startLine, endLine: parsed.endLine };
710
758
  }
711
- function createHttpServer(tools, bridge, allowedTools, serverConfig) {
759
+ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
712
760
  const app = express();
713
761
  let mcpServerActive = false;
714
762
  let lastMCPActivity = 0;
@@ -738,7 +786,49 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
738
786
  const isPluginConnected = () => {
739
787
  return bridge.getInstances().length > 0;
740
788
  };
741
- app.use(cors());
789
+ const allowedOrigins = new Set(security?.allowedOrigins ?? []);
790
+ app.use((req, res, next) => {
791
+ const origin = req.headers.origin;
792
+ if (typeof origin !== "string" || origin === "") {
793
+ next();
794
+ return;
795
+ }
796
+ if (!allowedOrigins.has(origin)) {
797
+ res.status(403).json({
798
+ error: "forbidden_origin",
799
+ message: `Cross-origin requests are not allowed from ${origin}. Set ROBLOX_STUDIO_ALLOWED_ORIGINS to allowlist specific origins.`
800
+ });
801
+ return;
802
+ }
803
+ res.setHeader("Access-Control-Allow-Origin", origin);
804
+ res.setHeader("Vary", "Origin");
805
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
806
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-MCP-Auth, Mcp-Protocol-Version");
807
+ if (req.method === "OPTIONS") {
808
+ res.status(204).end();
809
+ return;
810
+ }
811
+ next();
812
+ });
813
+ const authToken = security?.authToken;
814
+ const authRequired = (path5) => path5 === "/mcp" || path5.startsWith("/mcp/") || path5 === "/proxy" || path5 === "/instances" || path5 === "/unregister-instance-id";
815
+ app.use((req, res, next) => {
816
+ if (!authToken || !authRequired(req.path)) {
817
+ next();
818
+ return;
819
+ }
820
+ const headerToken = req.headers["x-mcp-auth"];
821
+ const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice("Bearer ".length) : void 0;
822
+ const provided = typeof headerToken === "string" && headerToken !== "" ? headerToken : bearer;
823
+ if (provided !== void 0 && tokensMatch(provided, authToken)) {
824
+ next();
825
+ return;
826
+ }
827
+ res.status(401).json({
828
+ error: "unauthorized",
829
+ message: 'Missing or invalid auth token. Send it as "X-MCP-Auth: <token>" or "Authorization: Bearer <token>". ' + (security?.authTokenHint ?? "The token is in ~/.robloxstudio-mcp/auth-token (or ROBLOX_STUDIO_AUTH_TOKEN).")
830
+ });
831
+ });
742
832
  app.use(express.json({ limit: "50mb" }));
743
833
  app.use(express.urlencoded({ limit: "50mb", extended: true }));
744
834
  app.get("/health", (req, res) => {
@@ -1108,6 +1198,7 @@ var init_http_server = __esm({
1108
1198
  "use strict";
1109
1199
  init_bridge_service();
1110
1200
  init_mcp_compat();
1201
+ init_auth();
1111
1202
  TOOL_HANDLERS = {
1112
1203
  get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
1113
1204
  get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
@@ -1275,8 +1366,844 @@ var init_studio_client = __esm({
1275
1366
  }
1276
1367
  });
1277
1368
 
1369
+ // ../core/dist/tools/build-interpreter.js
1370
+ import * as acorn from "acorn";
1371
+ function isPlainObject(value) {
1372
+ if (value === null || typeof value !== "object")
1373
+ return false;
1374
+ const proto = Object.getPrototypeOf(value);
1375
+ return proto === Object.prototype || proto === null;
1376
+ }
1377
+ function runRestrictedScript(code, globals, options) {
1378
+ const timeoutMs = options?.timeoutMs ?? 1e4;
1379
+ const maxOps = options?.maxOps ?? 2e7;
1380
+ const deadline = Date.now() + timeoutMs;
1381
+ let ops = 0;
1382
+ let ast;
1383
+ try {
1384
+ ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
1385
+ } catch (err) {
1386
+ throw new Error(`Syntax error: ${err.message}`);
1387
+ }
1388
+ const globalScope = new Scope(void 0, true);
1389
+ for (const [name, value] of Object.entries(globals)) {
1390
+ globalScope.declare(name, value, "const");
1391
+ }
1392
+ function tick() {
1393
+ ops++;
1394
+ if (ops > maxOps) {
1395
+ throw new ScriptTimeoutError(`Operation budget exceeded (${maxOps} ops)`);
1396
+ }
1397
+ if ((ops & 8191) === 0 && Date.now() > deadline) {
1398
+ throw new ScriptTimeoutError(`Execution timed out after ${timeoutMs}ms`);
1399
+ }
1400
+ }
1401
+ function assertPropAllowed(prop) {
1402
+ const name = typeof prop === "symbol" ? String(prop) : String(prop);
1403
+ if (FORBIDDEN_PROPS.has(name)) {
1404
+ throw new Error(`Access to property "${name}" is not allowed`);
1405
+ }
1406
+ return name;
1407
+ }
1408
+ function getMember(obj, prop) {
1409
+ if (obj === null || obj === void 0) {
1410
+ throw new Error(`Cannot read property "${String(prop)}" of ${obj}`);
1411
+ }
1412
+ const name = assertPropAllowed(prop);
1413
+ if (Array.isArray(obj)) {
1414
+ if (name === "length")
1415
+ return obj.length;
1416
+ const idx = Number(name);
1417
+ if (Number.isInteger(idx) && idx >= 0)
1418
+ return obj[idx];
1419
+ if (ARRAY_MEMBERS.has(name))
1420
+ return obj[name];
1421
+ throw new Error(`Array method "${name}" is not allowed`);
1422
+ }
1423
+ if (typeof obj === "string") {
1424
+ if (name === "length")
1425
+ return obj.length;
1426
+ const idx = Number(name);
1427
+ if (Number.isInteger(idx) && idx >= 0)
1428
+ return obj[idx];
1429
+ if (STRING_MEMBERS.has(name))
1430
+ return obj[name];
1431
+ throw new Error(`String method "${name}" is not allowed`);
1432
+ }
1433
+ if (typeof obj === "number") {
1434
+ if (NUMBER_MEMBERS.has(name))
1435
+ return obj[name];
1436
+ throw new Error(`Number method "${name}" is not allowed`);
1437
+ }
1438
+ if (isPlainObject(obj)) {
1439
+ if (Object.prototype.hasOwnProperty.call(obj, name))
1440
+ return obj[name];
1441
+ return void 0;
1442
+ }
1443
+ throw new Error(`Property access on this value type is not allowed`);
1444
+ }
1445
+ function setMember(obj, prop, value) {
1446
+ if (obj === null || obj === void 0) {
1447
+ throw new Error(`Cannot set property "${String(prop)}" of ${obj}`);
1448
+ }
1449
+ const name = assertPropAllowed(prop);
1450
+ if (Object.isFrozen(obj)) {
1451
+ throw new Error(`Cannot assign to property "${name}" of a frozen object`);
1452
+ }
1453
+ if (Array.isArray(obj)) {
1454
+ const idx = Number(name);
1455
+ if (Number.isInteger(idx) && idx >= 0 || name === "length") {
1456
+ obj[name] = value;
1457
+ return;
1458
+ }
1459
+ throw new Error(`Cannot set property "${name}" on an array`);
1460
+ }
1461
+ if (isPlainObject(obj)) {
1462
+ obj[name] = value;
1463
+ return;
1464
+ }
1465
+ throw new Error(`Property assignment on this value type is not allowed`);
1466
+ }
1467
+ function callFunction(fn, thisArg, args, desc) {
1468
+ if (typeof fn !== "function") {
1469
+ throw new Error(`${desc} is not a function`);
1470
+ }
1471
+ return fn.apply(thisArg, args);
1472
+ }
1473
+ function bindPattern(pattern, value, scope, kind) {
1474
+ switch (pattern.type) {
1475
+ case "Identifier":
1476
+ scope.declare(pattern.name, value, kind);
1477
+ return;
1478
+ case "AssignmentPattern":
1479
+ bindPattern(pattern.left, value === void 0 ? evaluate(pattern.right, scope) : value, scope, kind);
1480
+ return;
1481
+ case "ArrayPattern": {
1482
+ if (value === null || value === void 0)
1483
+ throw new Error("Cannot destructure null/undefined");
1484
+ let i = 0;
1485
+ for (const el of pattern.elements) {
1486
+ if (el === null) {
1487
+ i++;
1488
+ continue;
1489
+ }
1490
+ if (el.type === "RestElement") {
1491
+ bindPattern(el.argument, Array.isArray(value) ? value.slice(i) : [], scope, kind);
1492
+ break;
1493
+ }
1494
+ bindPattern(el, getMember(value, i), scope, kind);
1495
+ i++;
1496
+ }
1497
+ return;
1498
+ }
1499
+ case "ObjectPattern": {
1500
+ if (value === null || value === void 0)
1501
+ throw new Error("Cannot destructure null/undefined");
1502
+ const used = /* @__PURE__ */ new Set();
1503
+ for (const propNode of pattern.properties) {
1504
+ if (propNode.type === "RestElement") {
1505
+ const rest = {};
1506
+ if (isPlainObject(value)) {
1507
+ for (const k of Object.keys(value)) {
1508
+ if (!used.has(k) && !FORBIDDEN_PROPS.has(k))
1509
+ rest[k] = value[k];
1510
+ }
1511
+ }
1512
+ bindPattern(propNode.argument, rest, scope, kind);
1513
+ continue;
1514
+ }
1515
+ const key = propNode.computed ? String(evaluate(propNode.key, scope)) : propNode.key.type === "Identifier" ? propNode.key.name : String(propNode.key.value);
1516
+ used.add(key);
1517
+ bindPattern(propNode.value, getMember(value, key), scope, kind);
1518
+ }
1519
+ return;
1520
+ }
1521
+ default:
1522
+ throw new Error(`Unsupported binding pattern: ${pattern.type}`);
1523
+ }
1524
+ }
1525
+ function makeFunction(node, closureScope) {
1526
+ return (...args) => {
1527
+ tick();
1528
+ const fnScope = new Scope(closureScope, true);
1529
+ node.params.forEach((param, i) => {
1530
+ if (param.type === "RestElement") {
1531
+ bindPattern(param.argument, args.slice(i), fnScope, "let");
1532
+ } else {
1533
+ bindPattern(param, args[i], fnScope, "let");
1534
+ }
1535
+ });
1536
+ try {
1537
+ if (node.body.type === "BlockStatement") {
1538
+ executeBlock(node.body.body, new Scope(fnScope));
1539
+ return void 0;
1540
+ }
1541
+ return evaluate(node.body, fnScope);
1542
+ } catch (signal) {
1543
+ if (signal instanceof ReturnSignal)
1544
+ return signal.value;
1545
+ throw signal;
1546
+ }
1547
+ };
1548
+ }
1549
+ function hoistFunctions(statements, scope) {
1550
+ for (const stmt of statements) {
1551
+ if (stmt.type === "FunctionDeclaration" && stmt.id) {
1552
+ if (stmt.async || stmt.generator)
1553
+ throw new Error("async/generator functions are not supported");
1554
+ scope.declare(stmt.id.name, makeFunction(stmt, scope), "let");
1555
+ }
1556
+ }
1557
+ }
1558
+ function executeBlock(statements, scope) {
1559
+ hoistFunctions(statements, scope);
1560
+ for (const stmt of statements) {
1561
+ execute(stmt, scope);
1562
+ }
1563
+ }
1564
+ function execute(node, scope) {
1565
+ tick();
1566
+ switch (node.type) {
1567
+ case "ExpressionStatement":
1568
+ evaluate(node.expression, scope);
1569
+ return;
1570
+ case "VariableDeclaration":
1571
+ for (const decl of node.declarations) {
1572
+ const value = decl.init ? evaluate(decl.init, scope) : void 0;
1573
+ bindPattern(decl.id, value, scope, node.kind);
1574
+ }
1575
+ return;
1576
+ case "FunctionDeclaration":
1577
+ return;
1578
+ // hoisted by executeBlock
1579
+ case "BlockStatement":
1580
+ executeBlock(node.body, new Scope(scope));
1581
+ return;
1582
+ case "EmptyStatement":
1583
+ return;
1584
+ case "IfStatement":
1585
+ if (evaluate(node.test, scope)) {
1586
+ execute(node.consequent, scope);
1587
+ } else if (node.alternate) {
1588
+ execute(node.alternate, scope);
1589
+ }
1590
+ return;
1591
+ case "ForStatement": {
1592
+ const forScope = new Scope(scope);
1593
+ if (node.init) {
1594
+ if (node.init.type === "VariableDeclaration")
1595
+ execute(node.init, forScope);
1596
+ else
1597
+ evaluate(node.init, forScope);
1598
+ }
1599
+ while (node.test === null || evaluate(node.test, forScope)) {
1600
+ tick();
1601
+ try {
1602
+ execute(node.body, new Scope(forScope));
1603
+ } catch (signal) {
1604
+ if (signal instanceof BreakSignal)
1605
+ break;
1606
+ if (!(signal instanceof ContinueSignal))
1607
+ throw signal;
1608
+ }
1609
+ if (node.update)
1610
+ evaluate(node.update, forScope);
1611
+ }
1612
+ return;
1613
+ }
1614
+ case "ForOfStatement": {
1615
+ const iterable = evaluate(node.right, scope);
1616
+ if (!Array.isArray(iterable) && typeof iterable !== "string") {
1617
+ throw new Error("for...of only supports arrays and strings");
1618
+ }
1619
+ for (const item of iterable) {
1620
+ tick();
1621
+ const iterScope = new Scope(scope);
1622
+ if (node.left.type === "VariableDeclaration") {
1623
+ bindPattern(node.left.declarations[0].id, item, iterScope, node.left.kind);
1624
+ } else {
1625
+ assignTo(node.left, item, iterScope);
1626
+ }
1627
+ try {
1628
+ execute(node.body, iterScope);
1629
+ } catch (signal) {
1630
+ if (signal instanceof BreakSignal)
1631
+ break;
1632
+ if (!(signal instanceof ContinueSignal))
1633
+ throw signal;
1634
+ }
1635
+ }
1636
+ return;
1637
+ }
1638
+ case "ForInStatement": {
1639
+ const obj = evaluate(node.right, scope);
1640
+ const keys = isPlainObject(obj) ? Object.keys(obj) : Array.isArray(obj) ? obj.map((_, i) => String(i)) : [];
1641
+ for (const key of keys) {
1642
+ tick();
1643
+ const iterScope = new Scope(scope);
1644
+ if (node.left.type === "VariableDeclaration") {
1645
+ bindPattern(node.left.declarations[0].id, key, iterScope, node.left.kind);
1646
+ } else {
1647
+ assignTo(node.left, key, iterScope);
1648
+ }
1649
+ try {
1650
+ execute(node.body, iterScope);
1651
+ } catch (signal) {
1652
+ if (signal instanceof BreakSignal)
1653
+ break;
1654
+ if (!(signal instanceof ContinueSignal))
1655
+ throw signal;
1656
+ }
1657
+ }
1658
+ return;
1659
+ }
1660
+ case "WhileStatement":
1661
+ while (evaluate(node.test, scope)) {
1662
+ tick();
1663
+ try {
1664
+ execute(node.body, new Scope(scope));
1665
+ } catch (signal) {
1666
+ if (signal instanceof BreakSignal)
1667
+ break;
1668
+ if (!(signal instanceof ContinueSignal))
1669
+ throw signal;
1670
+ }
1671
+ }
1672
+ return;
1673
+ case "DoWhileStatement":
1674
+ do {
1675
+ tick();
1676
+ try {
1677
+ execute(node.body, new Scope(scope));
1678
+ } catch (signal) {
1679
+ if (signal instanceof BreakSignal)
1680
+ break;
1681
+ if (!(signal instanceof ContinueSignal))
1682
+ throw signal;
1683
+ }
1684
+ } while (evaluate(node.test, scope));
1685
+ return;
1686
+ case "BreakStatement":
1687
+ throw new BreakSignal(node.label?.name);
1688
+ case "ContinueStatement":
1689
+ throw new ContinueSignal(node.label?.name);
1690
+ case "ReturnStatement":
1691
+ throw new ReturnSignal(node.argument ? evaluate(node.argument, scope) : void 0);
1692
+ case "ThrowStatement":
1693
+ throw evaluate(node.argument, scope);
1694
+ case "TryStatement": {
1695
+ try {
1696
+ execute(node.block, scope);
1697
+ } catch (err) {
1698
+ if (err instanceof BreakSignal || err instanceof ContinueSignal || err instanceof ReturnSignal)
1699
+ throw err;
1700
+ if (err instanceof ScriptTimeoutError)
1701
+ throw err;
1702
+ if (node.handler) {
1703
+ const catchScope = new Scope(scope);
1704
+ if (node.handler.param) {
1705
+ const message = err instanceof Error ? { message: err.message } : err;
1706
+ bindPattern(node.handler.param, message, catchScope, "let");
1707
+ }
1708
+ execute(node.handler.body, catchScope);
1709
+ }
1710
+ } finally {
1711
+ if (node.finalizer)
1712
+ execute(node.finalizer, scope);
1713
+ }
1714
+ return;
1715
+ }
1716
+ case "SwitchStatement": {
1717
+ const disc = evaluate(node.discriminant, scope);
1718
+ const switchScope = new Scope(scope);
1719
+ let matched = false;
1720
+ try {
1721
+ for (const caseNode of node.cases) {
1722
+ if (!matched) {
1723
+ if (caseNode.test === null)
1724
+ matched = true;
1725
+ else if (evaluate(caseNode.test, switchScope) === disc)
1726
+ matched = true;
1727
+ }
1728
+ if (matched) {
1729
+ for (const stmt of caseNode.consequent)
1730
+ execute(stmt, switchScope);
1731
+ }
1732
+ }
1733
+ } catch (signal) {
1734
+ if (!(signal instanceof BreakSignal))
1735
+ throw signal;
1736
+ }
1737
+ return;
1738
+ }
1739
+ case "LabeledStatement":
1740
+ try {
1741
+ execute(node.body, scope);
1742
+ } catch (signal) {
1743
+ if (signal instanceof BreakSignal && signal.label === node.label.name)
1744
+ return;
1745
+ throw signal;
1746
+ }
1747
+ return;
1748
+ default:
1749
+ throw new Error(`Unsupported statement: ${node.type}`);
1750
+ }
1751
+ }
1752
+ function assignTo(target, value, scope) {
1753
+ if (target.type === "Identifier") {
1754
+ scope.set(target.name, value);
1755
+ } else if (target.type === "MemberExpression") {
1756
+ const obj = evaluate(target.object, scope);
1757
+ const prop = target.computed ? evaluate(target.property, scope) : target.property.name;
1758
+ setMember(obj, prop, value);
1759
+ } else {
1760
+ throw new Error(`Unsupported assignment target: ${target.type}`);
1761
+ }
1762
+ }
1763
+ function evaluate(node, scope) {
1764
+ tick();
1765
+ switch (node.type) {
1766
+ case "Literal":
1767
+ if (node.regex)
1768
+ throw new Error("Regular expression literals are not supported");
1769
+ return node.value;
1770
+ case "TemplateLiteral": {
1771
+ let out = "";
1772
+ node.quasis.forEach((quasi, i) => {
1773
+ out += quasi.value.cooked ?? "";
1774
+ if (i < node.expressions.length)
1775
+ out += String(evaluate(node.expressions[i], scope));
1776
+ });
1777
+ return out;
1778
+ }
1779
+ case "Identifier":
1780
+ return scope.get(node.name);
1781
+ case "ArrayExpression": {
1782
+ const arr = [];
1783
+ for (const el of node.elements) {
1784
+ if (el === null) {
1785
+ arr.length++;
1786
+ continue;
1787
+ }
1788
+ if (el.type === "SpreadElement") {
1789
+ const spread = evaluate(el.argument, scope);
1790
+ if (!Array.isArray(spread) && typeof spread !== "string") {
1791
+ throw new Error("Spread only supports arrays and strings");
1792
+ }
1793
+ for (const item of spread)
1794
+ arr.push(item);
1795
+ } else {
1796
+ arr.push(evaluate(el, scope));
1797
+ }
1798
+ }
1799
+ return arr;
1800
+ }
1801
+ case "ObjectExpression": {
1802
+ const obj = {};
1803
+ for (const prop of node.properties) {
1804
+ if (prop.type === "SpreadElement") {
1805
+ const spread = evaluate(prop.argument, scope);
1806
+ if (isPlainObject(spread)) {
1807
+ for (const k of Object.keys(spread)) {
1808
+ if (!FORBIDDEN_PROPS.has(k))
1809
+ obj[k] = spread[k];
1810
+ }
1811
+ }
1812
+ continue;
1813
+ }
1814
+ if (prop.kind !== "init")
1815
+ throw new Error("Getters/setters are not supported");
1816
+ const key = prop.computed ? String(evaluate(prop.key, scope)) : prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
1817
+ assertPropAllowed(key);
1818
+ obj[key] = evaluate(prop.value, scope);
1819
+ }
1820
+ return obj;
1821
+ }
1822
+ case "ArrowFunctionExpression":
1823
+ case "FunctionExpression":
1824
+ if (node.async || node.generator)
1825
+ throw new Error("async/generator functions are not supported");
1826
+ return makeFunction(node, scope);
1827
+ case "UnaryExpression": {
1828
+ if (node.operator === "typeof" && node.argument.type === "Identifier" && !scope.has(node.argument.name)) {
1829
+ return "undefined";
1830
+ }
1831
+ if (node.operator === "delete") {
1832
+ if (node.argument.type !== "MemberExpression")
1833
+ return true;
1834
+ const obj = evaluate(node.argument.object, scope);
1835
+ const prop = node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name;
1836
+ const name = assertPropAllowed(prop);
1837
+ if (isPlainObject(obj) && !Object.isFrozen(obj)) {
1838
+ delete obj[name];
1839
+ return true;
1840
+ }
1841
+ throw new Error("delete is only allowed on plain objects");
1842
+ }
1843
+ const arg = evaluate(node.argument, scope);
1844
+ switch (node.operator) {
1845
+ case "-":
1846
+ return -arg;
1847
+ case "+":
1848
+ return +arg;
1849
+ case "!":
1850
+ return !arg;
1851
+ case "~":
1852
+ return ~arg;
1853
+ case "typeof":
1854
+ return typeof arg;
1855
+ case "void":
1856
+ return void 0;
1857
+ default:
1858
+ throw new Error(`Unsupported unary operator: ${node.operator}`);
1859
+ }
1860
+ }
1861
+ case "UpdateExpression": {
1862
+ const old = node.argument.type === "Identifier" ? scope.get(node.argument.name) : getMember(evaluate(node.argument.object, scope), node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name);
1863
+ const next = node.operator === "++" ? old + 1 : old - 1;
1864
+ assignTo(node.argument, next, scope);
1865
+ return node.prefix ? next : old;
1866
+ }
1867
+ case "BinaryExpression": {
1868
+ if (node.operator === "in") {
1869
+ const key = assertPropAllowed(evaluate(node.left, scope));
1870
+ const obj = evaluate(node.right, scope);
1871
+ if (isPlainObject(obj))
1872
+ return Object.prototype.hasOwnProperty.call(obj, key);
1873
+ if (Array.isArray(obj))
1874
+ return Number(key) >= 0 && Number(key) < obj.length;
1875
+ throw new Error('"in" is only supported on plain objects and arrays');
1876
+ }
1877
+ if (node.operator === "instanceof")
1878
+ throw new Error("instanceof is not supported");
1879
+ const l = evaluate(node.left, scope);
1880
+ const r = evaluate(node.right, scope);
1881
+ switch (node.operator) {
1882
+ case "+":
1883
+ return l + r;
1884
+ case "-":
1885
+ return l - r;
1886
+ case "*":
1887
+ return l * r;
1888
+ case "/":
1889
+ return l / r;
1890
+ case "%":
1891
+ return l % r;
1892
+ case "**":
1893
+ return l ** r;
1894
+ case "==":
1895
+ return l == r;
1896
+ // eslint-disable-line eqeqeq
1897
+ case "!=":
1898
+ return l != r;
1899
+ // eslint-disable-line eqeqeq
1900
+ case "===":
1901
+ return l === r;
1902
+ case "!==":
1903
+ return l !== r;
1904
+ case "<":
1905
+ return l < r;
1906
+ case "<=":
1907
+ return l <= r;
1908
+ case ">":
1909
+ return l > r;
1910
+ case ">=":
1911
+ return l >= r;
1912
+ case "&":
1913
+ return l & r;
1914
+ case "|":
1915
+ return l | r;
1916
+ case "^":
1917
+ return l ^ r;
1918
+ case "<<":
1919
+ return l << r;
1920
+ case ">>":
1921
+ return l >> r;
1922
+ case ">>>":
1923
+ return l >>> r;
1924
+ default:
1925
+ throw new Error(`Unsupported binary operator: ${node.operator}`);
1926
+ }
1927
+ }
1928
+ case "LogicalExpression": {
1929
+ const l = evaluate(node.left, scope);
1930
+ if (node.operator === "&&")
1931
+ return l ? evaluate(node.right, scope) : l;
1932
+ if (node.operator === "||")
1933
+ return l ? l : evaluate(node.right, scope);
1934
+ if (node.operator === "??")
1935
+ return l ?? evaluate(node.right, scope);
1936
+ throw new Error(`Unsupported logical operator: ${node.operator}`);
1937
+ }
1938
+ case "AssignmentExpression": {
1939
+ let value;
1940
+ if (node.operator === "=") {
1941
+ value = evaluate(node.right, scope);
1942
+ } else {
1943
+ const current = node.left.type === "Identifier" ? scope.get(node.left.name) : getMember(evaluate(node.left.object, scope), node.left.computed ? evaluate(node.left.property, scope) : node.left.property.name);
1944
+ const rhs = () => evaluate(node.right, scope);
1945
+ switch (node.operator) {
1946
+ case "+=":
1947
+ value = current + rhs();
1948
+ break;
1949
+ case "-=":
1950
+ value = current - rhs();
1951
+ break;
1952
+ case "*=":
1953
+ value = current * rhs();
1954
+ break;
1955
+ case "/=":
1956
+ value = current / rhs();
1957
+ break;
1958
+ case "%=":
1959
+ value = current % rhs();
1960
+ break;
1961
+ case "**=":
1962
+ value = current ** rhs();
1963
+ break;
1964
+ case "&=":
1965
+ value = current & rhs();
1966
+ break;
1967
+ case "|=":
1968
+ value = current | rhs();
1969
+ break;
1970
+ case "^=":
1971
+ value = current ^ rhs();
1972
+ break;
1973
+ case "<<=":
1974
+ value = current << rhs();
1975
+ break;
1976
+ case ">>=":
1977
+ value = current >> rhs();
1978
+ break;
1979
+ case ">>>=":
1980
+ value = current >>> rhs();
1981
+ break;
1982
+ case "&&=":
1983
+ value = current ? rhs() : current;
1984
+ break;
1985
+ case "||=":
1986
+ value = current ? current : rhs();
1987
+ break;
1988
+ case "??=":
1989
+ value = current ?? rhs();
1990
+ break;
1991
+ default:
1992
+ throw new Error(`Unsupported assignment operator: ${node.operator}`);
1993
+ }
1994
+ }
1995
+ assignTo(node.left, value, scope);
1996
+ return value;
1997
+ }
1998
+ case "ConditionalExpression":
1999
+ return evaluate(node.test, scope) ? evaluate(node.consequent, scope) : evaluate(node.alternate, scope);
2000
+ case "CallExpression": {
2001
+ const args = [];
2002
+ for (const argNode of node.arguments) {
2003
+ if (argNode.type === "SpreadElement") {
2004
+ const spread = evaluate(argNode.argument, scope);
2005
+ if (!Array.isArray(spread) && typeof spread !== "string") {
2006
+ throw new Error("Spread only supports arrays and strings");
2007
+ }
2008
+ for (const item of spread)
2009
+ args.push(item);
2010
+ } else {
2011
+ args.push(evaluate(argNode, scope));
2012
+ }
2013
+ }
2014
+ if (node.callee.type === "MemberExpression") {
2015
+ const obj = evaluate(node.callee.object, scope);
2016
+ if (node.optional && (obj === null || obj === void 0))
2017
+ return void 0;
2018
+ const prop = node.callee.computed ? evaluate(node.callee.property, scope) : node.callee.property.name;
2019
+ const method = getMember(obj, prop);
2020
+ if (node.callee.optional && (method === null || method === void 0))
2021
+ return void 0;
2022
+ return callFunction(method, obj, args, `${String(prop)}`);
2023
+ }
2024
+ const fn = evaluate(node.callee, scope);
2025
+ if (node.optional && (fn === null || fn === void 0))
2026
+ return void 0;
2027
+ const desc = node.callee.type === "Identifier" ? node.callee.name : "expression";
2028
+ return callFunction(fn, void 0, args, desc);
2029
+ }
2030
+ case "MemberExpression": {
2031
+ const obj = evaluate(node.object, scope);
2032
+ if (node.optional && (obj === null || obj === void 0))
2033
+ return void 0;
2034
+ const prop = node.computed ? evaluate(node.property, scope) : node.property.name;
2035
+ return getMember(obj, prop);
2036
+ }
2037
+ case "ChainExpression":
2038
+ return evaluate(node.expression, scope);
2039
+ case "SequenceExpression": {
2040
+ let result;
2041
+ for (const expr of node.expressions)
2042
+ result = evaluate(expr, scope);
2043
+ return result;
2044
+ }
2045
+ case "NewExpression":
2046
+ throw new Error('"new" is not supported in build code');
2047
+ case "ThisExpression":
2048
+ throw new Error('"this" is not supported in build code');
2049
+ case "AwaitExpression":
2050
+ throw new Error("await is not supported in build code");
2051
+ case "TaggedTemplateExpression":
2052
+ throw new Error("Tagged templates are not supported in build code");
2053
+ default:
2054
+ throw new Error(`Unsupported expression: ${node.type}`);
2055
+ }
2056
+ }
2057
+ executeBlock(ast.body, globalScope);
2058
+ }
2059
+ var ScriptTimeoutError, BreakSignal, ContinueSignal, ReturnSignal, FORBIDDEN_PROPS, ARRAY_MEMBERS, STRING_MEMBERS, NUMBER_MEMBERS, Scope;
2060
+ var init_build_interpreter = __esm({
2061
+ "../core/dist/tools/build-interpreter.js"() {
2062
+ "use strict";
2063
+ ScriptTimeoutError = class extends Error {
2064
+ constructor(message) {
2065
+ super(message);
2066
+ this.name = "ScriptTimeoutError";
2067
+ }
2068
+ };
2069
+ BreakSignal = class {
2070
+ label;
2071
+ constructor(label) {
2072
+ this.label = label;
2073
+ }
2074
+ };
2075
+ ContinueSignal = class {
2076
+ label;
2077
+ constructor(label) {
2078
+ this.label = label;
2079
+ }
2080
+ };
2081
+ ReturnSignal = class {
2082
+ value;
2083
+ constructor(value) {
2084
+ this.value = value;
2085
+ }
2086
+ };
2087
+ FORBIDDEN_PROPS = /* @__PURE__ */ new Set([
2088
+ "__proto__",
2089
+ "constructor",
2090
+ "prototype",
2091
+ "__defineGetter__",
2092
+ "__defineSetter__",
2093
+ "__lookupGetter__",
2094
+ "__lookupSetter__",
2095
+ "caller",
2096
+ "callee",
2097
+ "arguments",
2098
+ "bind",
2099
+ "call",
2100
+ "apply"
2101
+ ]);
2102
+ ARRAY_MEMBERS = /* @__PURE__ */ new Set([
2103
+ "length",
2104
+ "push",
2105
+ "pop",
2106
+ "shift",
2107
+ "unshift",
2108
+ "slice",
2109
+ "splice",
2110
+ "indexOf",
2111
+ "lastIndexOf",
2112
+ "includes",
2113
+ "join",
2114
+ "map",
2115
+ "filter",
2116
+ "forEach",
2117
+ "reduce",
2118
+ "reduceRight",
2119
+ "concat",
2120
+ "reverse",
2121
+ "sort",
2122
+ "find",
2123
+ "findIndex",
2124
+ "some",
2125
+ "every",
2126
+ "flat",
2127
+ "flatMap",
2128
+ "fill"
2129
+ ]);
2130
+ STRING_MEMBERS = /* @__PURE__ */ new Set([
2131
+ "length",
2132
+ "charAt",
2133
+ "charCodeAt",
2134
+ "codePointAt",
2135
+ "indexOf",
2136
+ "lastIndexOf",
2137
+ "includes",
2138
+ "startsWith",
2139
+ "endsWith",
2140
+ "slice",
2141
+ "substring",
2142
+ "toUpperCase",
2143
+ "toLowerCase",
2144
+ "trim",
2145
+ "trimStart",
2146
+ "trimEnd",
2147
+ "split",
2148
+ "repeat",
2149
+ "padStart",
2150
+ "padEnd",
2151
+ "concat",
2152
+ "at",
2153
+ "replace",
2154
+ "replaceAll"
2155
+ ]);
2156
+ NUMBER_MEMBERS = /* @__PURE__ */ new Set(["toFixed", "toPrecision", "toString"]);
2157
+ Scope = class {
2158
+ parent;
2159
+ isFunctionScope;
2160
+ vars = /* @__PURE__ */ new Map();
2161
+ consts = /* @__PURE__ */ new Set();
2162
+ constructor(parent, isFunctionScope = false) {
2163
+ this.parent = parent;
2164
+ this.isFunctionScope = isFunctionScope;
2165
+ }
2166
+ declare(name, value, kind) {
2167
+ if (kind === "var" && !this.isFunctionScope && this.parent) {
2168
+ this.parent.declare(name, value, kind);
2169
+ return;
2170
+ }
2171
+ this.vars.set(name, value);
2172
+ if (kind === "const")
2173
+ this.consts.add(name);
2174
+ }
2175
+ resolve(name) {
2176
+ if (this.vars.has(name))
2177
+ return this;
2178
+ return this.parent?.resolve(name);
2179
+ }
2180
+ globalScope() {
2181
+ return this.parent ? this.parent.globalScope() : this;
2182
+ }
2183
+ has(name) {
2184
+ return this.resolve(name) !== void 0;
2185
+ }
2186
+ get(name) {
2187
+ const s = this.resolve(name);
2188
+ if (!s)
2189
+ throw new Error(`${name} is not defined`);
2190
+ return s.vars.get(name);
2191
+ }
2192
+ set(name, value) {
2193
+ const s = this.resolve(name);
2194
+ if (!s) {
2195
+ this.globalScope().vars.set(name, value);
2196
+ return;
2197
+ }
2198
+ if (s.consts.has(name))
2199
+ throw new Error(`Assignment to constant variable "${name}"`);
2200
+ s.vars.set(name, value);
2201
+ }
2202
+ };
2203
+ }
2204
+ });
2205
+
1278
2206
  // ../core/dist/tools/build-executor.js
1279
- import * as vm from "vm";
1280
2207
  function createSeededRng(seed) {
1281
2208
  let s = seed;
1282
2209
  return () => {
@@ -1626,6 +2553,7 @@ function runBuildExecutor(code, palette, seed, options) {
1626
2553
  ]);
1627
2554
  }
1628
2555
  const rng = createSeededRng(seed ?? 42);
2556
+ const safeMath = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(Math)));
1629
2557
  const sandbox = {
1630
2558
  part: partFn,
1631
2559
  rpart: rpartFn,
@@ -1642,25 +2570,21 @@ function runBuildExecutor(code, palette, seed, options) {
1642
2570
  column: columnFn,
1643
2571
  pew: pewFn,
1644
2572
  fence: fenceFn,
1645
- Math,
2573
+ Math: safeMath,
1646
2574
  GRID_SIZE: 1,
1647
2575
  rng,
1648
- console: { log: () => {
2576
+ console: Object.freeze({ log: () => {
1649
2577
  }, warn: () => {
1650
2578
  }, error: () => {
1651
- } }
2579
+ } })
1652
2580
  };
1653
- const context = vm.createContext(sandbox, {
1654
- codeGeneration: { strings: false, wasm: false }
1655
- });
1656
- const script = new vm.Script(code, { filename: "build-generator.js" });
1657
2581
  try {
1658
- script.runInContext(context, { timeout });
2582
+ runRestrictedScript(code, sandbox, { timeoutMs: timeout });
1659
2583
  } catch (err) {
1660
- if (err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
2584
+ if (err instanceof ScriptTimeoutError) {
1661
2585
  throw new Error(`Build code execution timed out after ${timeout}ms`);
1662
2586
  }
1663
- throw new Error(`Build code execution error: ${err.message}`);
2587
+ throw new Error(`Build code execution error: ${err?.message ?? String(err)}`);
1664
2588
  }
1665
2589
  if (parts.length === 0) {
1666
2590
  throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
@@ -1672,6 +2596,7 @@ var DEFAULT_TIMEOUT, DEFAULT_MAX_PARTS, VALID_SHAPES;
1672
2596
  var init_build_executor = __esm({
1673
2597
  "../core/dist/tools/build-executor.js"() {
1674
2598
  "use strict";
2599
+ init_build_interpreter();
1675
2600
  DEFAULT_TIMEOUT = 1e4;
1676
2601
  DEFAULT_MAX_PARTS = 1e4;
1677
2602
  VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
@@ -2297,7 +3222,7 @@ var init_managed_instance_registry = __esm({
2297
3222
 
2298
3223
  // ../core/dist/studio-instance-manager.js
2299
3224
  import { execFileSync, spawn } from "child_process";
2300
- import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
3225
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
2301
3226
  import { randomUUID } from "crypto";
2302
3227
  import * as os2 from "os";
2303
3228
  import * as path2 from "path";
@@ -2312,14 +3237,14 @@ function isWsl() {
2312
3237
  if (process.platform !== "linux")
2313
3238
  return false;
2314
3239
  try {
2315
- return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
3240
+ return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
2316
3241
  } catch {
2317
3242
  return false;
2318
3243
  }
2319
3244
  }
2320
3245
  function powershell(script) {
2321
3246
  return run("powershell.exe", ["-NoProfile", "-Command", script], {
2322
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3247
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2323
3248
  });
2324
3249
  }
2325
3250
  function windowsLocalAppData() {
@@ -2329,7 +3254,7 @@ function windowsLocalAppData() {
2329
3254
  return void 0;
2330
3255
  try {
2331
3256
  return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
2332
- cwd: existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
3257
+ cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
2333
3258
  });
2334
3259
  } catch {
2335
3260
  return void 0;
@@ -2341,7 +3266,7 @@ function toWslPath(windowsPath) {
2341
3266
  return run("wslpath", ["-u", windowsPath]);
2342
3267
  }
2343
3268
  function toStudioLaunchArg(arg) {
2344
- if (!isWsl() || !path2.isAbsolute(arg) || !existsSync(arg))
3269
+ if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
2345
3270
  return arg;
2346
3271
  return run("wslpath", ["-w", arg]);
2347
3272
  }
@@ -2367,13 +3292,44 @@ function resolveBaseplateTemplatePath() {
2367
3292
  path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
2368
3293
  ];
2369
3294
  for (const candidate of candidates) {
2370
- if (existsSync(candidate))
3295
+ if (existsSync2(candidate))
2371
3296
  return candidate;
2372
3297
  }
2373
3298
  throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
2374
3299
  }
3300
+ function isProcessAlive(pid) {
3301
+ try {
3302
+ process.kill(pid, 0);
3303
+ return true;
3304
+ } catch (err) {
3305
+ return err.code === "EPERM";
3306
+ }
3307
+ }
3308
+ function sweepStaleBaseplateFiles() {
3309
+ let entries;
3310
+ try {
3311
+ entries = readdirSync2(BASEPLATE_TEMP_DIR);
3312
+ } catch {
3313
+ return;
3314
+ }
3315
+ const cutoff = Date.now() - STALE_BASEPLATE_MAX_AGE_MS;
3316
+ for (const entry of entries) {
3317
+ const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
3318
+ if (!match)
3319
+ continue;
3320
+ if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
3321
+ continue;
3322
+ const file = path2.join(BASEPLATE_TEMP_DIR, entry);
3323
+ try {
3324
+ if (statSync2(file).mtimeMs < cutoff)
3325
+ rmSync2(file, { force: true });
3326
+ } catch {
3327
+ }
3328
+ }
3329
+ }
2375
3330
  function createBaseplatePlaceFile() {
2376
- mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
3331
+ mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
3332
+ sweepStaleBaseplateFiles();
2377
3333
  const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
2378
3334
  copyFileSync(resolveBaseplateTemplatePath(), file);
2379
3335
  return file;
@@ -2387,8 +3343,12 @@ function cleanupManagedBaseplateFiles(record) {
2387
3343
  return;
2388
3344
  if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
2389
3345
  return;
2390
- rmSync2(record.localPlaceFile, { force: true });
2391
- rmSync2(`${record.localPlaceFile}.lock`, { force: true });
3346
+ for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
3347
+ try {
3348
+ rmSync2(file, { force: true });
3349
+ } catch {
3350
+ }
3351
+ }
2392
3352
  }
2393
3353
  function prepareStudioLaunchOptions(options) {
2394
3354
  if (options.source !== "baseplate" || options.localPlaceFile)
@@ -2409,10 +3369,10 @@ function resolveStudioExe() {
2409
3369
  }
2410
3370
  const localAppData = windowsLocalAppData();
2411
3371
  const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
2412
- if (!existsSync(root)) {
3372
+ if (!existsSync2(root)) {
2413
3373
  throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
2414
3374
  }
2415
- const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
3375
+ const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
2416
3376
  if (candidates.length === 0) {
2417
3377
  throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
2418
3378
  }
@@ -2447,7 +3407,7 @@ function listStudioProcesses() {
2447
3407
  function currentBootId() {
2448
3408
  if (process.platform === "linux") {
2449
3409
  try {
2450
- return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
3410
+ return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
2451
3411
  } catch {
2452
3412
  }
2453
3413
  }
@@ -2504,7 +3464,7 @@ function delay(ms) {
2504
3464
  function basenameAny(filePath) {
2505
3465
  return path2.basename(filePath.replace(/\\/g, "/"));
2506
3466
  }
2507
- var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
3467
+ var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
2508
3468
  var init_studio_instance_manager = __esm({
2509
3469
  "../core/dist/studio-instance-manager.js"() {
2510
3470
  "use strict";
@@ -2512,6 +3472,8 @@ var init_studio_instance_manager = __esm({
2512
3472
  BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
2513
3473
  BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
2514
3474
  BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
3475
+ STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3476
+ BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
2515
3477
  StudioInstanceManager = class {
2516
3478
  managedByInstanceId = /* @__PURE__ */ new Map();
2517
3479
  pending = /* @__PURE__ */ new Set();
@@ -2556,7 +3518,7 @@ var init_studio_instance_manager = __esm({
2556
3518
  const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
2557
3519
  const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
2558
3520
  const spawnOptions = {
2559
- cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
3521
+ cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
2560
3522
  detached: true,
2561
3523
  stdio: "ignore"
2562
3524
  };
@@ -8107,21 +9069,31 @@ var init_proxy_bridge_service = __esm({
8107
9069
  init_bridge_service();
8108
9070
  ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
8109
9071
  primaryBaseUrl;
9072
+ authToken;
8110
9073
  proxyInstanceId;
8111
9074
  proxyRequestTimeout = 3e4;
8112
9075
  cachedInstances = [];
8113
9076
  refreshTimer;
8114
9077
  static REFRESH_INTERVAL_MS = 1e3;
8115
- constructor(primaryBaseUrl) {
9078
+ constructor(primaryBaseUrl, authToken) {
8116
9079
  super();
8117
9080
  this.primaryBaseUrl = primaryBaseUrl;
9081
+ this.authToken = authToken;
8118
9082
  this.proxyInstanceId = uuidv42();
8119
9083
  this.refreshInstances();
8120
9084
  this.refreshTimer = setInterval(() => this.refreshInstances(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
8121
9085
  }
9086
+ authHeaders(extra) {
9087
+ const headers = { ...extra };
9088
+ if (this.authToken)
9089
+ headers["X-MCP-Auth"] = this.authToken;
9090
+ return headers;
9091
+ }
8122
9092
  async refreshInstances() {
8123
9093
  try {
8124
- const res = await fetch(`${this.primaryBaseUrl}/instances`);
9094
+ const res = await fetch(`${this.primaryBaseUrl}/instances`, {
9095
+ headers: this.authHeaders()
9096
+ });
8125
9097
  if (!res.ok)
8126
9098
  return;
8127
9099
  const body = await res.json();
@@ -8137,7 +9109,7 @@ var init_proxy_bridge_service = __esm({
8137
9109
  async unregisterInstanceIdEverywhere(instanceId) {
8138
9110
  const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
8139
9111
  method: "POST",
8140
- headers: { "Content-Type": "application/json" },
9112
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
8141
9113
  body: JSON.stringify({ instanceId })
8142
9114
  });
8143
9115
  if (!response.ok) {
@@ -8166,7 +9138,7 @@ var init_proxy_bridge_service = __esm({
8166
9138
  try {
8167
9139
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
8168
9140
  method: "POST",
8169
- headers: { "Content-Type": "application/json" },
9141
+ headers: this.authHeaders({ "Content-Type": "application/json" }),
8170
9142
  body: JSON.stringify({
8171
9143
  endpoint,
8172
9144
  data,
@@ -8211,6 +9183,7 @@ var init_server = __esm({
8211
9183
  "../core/dist/server.js"() {
8212
9184
  "use strict";
8213
9185
  init_http_server();
9186
+ init_auth();
8214
9187
  init_tools();
8215
9188
  init_bridge_service();
8216
9189
  init_proxy_bridge_service();
@@ -8280,14 +9253,28 @@ var init_server = __esm({
8280
9253
  }
8281
9254
  async run() {
8282
9255
  const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
8283
- const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
9256
+ const host = process.env.ROBLOX_STUDIO_HOST?.trim() || "127.0.0.1";
9257
+ if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
9258
+ console.error(`WARNING: ROBLOX_STUDIO_HOST=${host} exposes the MCP bridge beyond this machine. Anyone who can reach the port and knows the auth token can control Roblox Studio.`);
9259
+ }
9260
+ const auth = resolveAuthToken();
9261
+ const security = {
9262
+ authToken: auth.token,
9263
+ authTokenHint: auth.source === "env" ? "The token comes from ROBLOX_STUDIO_AUTH_TOKEN." : auth.filePath ? `The token is in ${auth.filePath}.` : void 0,
9264
+ allowedOrigins: (process.env.ROBLOX_STUDIO_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o !== "")
9265
+ };
9266
+ if (auth.source === "disabled") {
9267
+ console.error("WARNING: ROBLOX_STUDIO_NO_AUTH is set - tool endpoints accept unauthenticated requests.");
9268
+ } else if (auth.filePath) {
9269
+ console.error(`Auth token loaded from ${auth.filePath} (HTTP clients must send X-MCP-Auth or Authorization: Bearer)`);
9270
+ }
8284
9271
  let bridgeMode = "primary";
8285
9272
  let httpHandle;
8286
9273
  let primaryApp;
8287
9274
  let boundPort = 0;
8288
9275
  let promotionInterval;
8289
9276
  try {
8290
- primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
9277
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8291
9278
  const result = await listenWithRetry(primaryApp, host, basePort, 1);
8292
9279
  httpHandle = result.server;
8293
9280
  boundPort = result.port;
@@ -8296,7 +9283,7 @@ var init_server = __esm({
8296
9283
  } catch {
8297
9284
  bridgeMode = "proxy";
8298
9285
  primaryApp = void 0;
8299
- const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`);
9286
+ const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`, auth.token);
8300
9287
  this.bridge = proxyBridge;
8301
9288
  this.tools = new RobloxStudioTools(this.bridge);
8302
9289
  console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
@@ -8304,7 +9291,7 @@ var init_server = __esm({
8304
9291
  promotionInterval = setInterval(async () => {
8305
9292
  const candidateBridge = new BridgeService();
8306
9293
  const candidateTools = new RobloxStudioTools(candidateBridge);
8307
- const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
9294
+ const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config, security);
8308
9295
  try {
8309
9296
  const result = await listenWithRetry(candidateApp, host, basePort, 1);
8310
9297
  const oldBridge = this.bridge;
@@ -8329,7 +9316,7 @@ var init_server = __esm({
8329
9316
  let legacyHandle;
8330
9317
  let legacyApp;
8331
9318
  if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
8332
- legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
9319
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
8333
9320
  try {
8334
9321
  const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
8335
9322
  legacyHandle = result.server;
@@ -10186,7 +11173,7 @@ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders,
10186
11173
  EXAMPLE - compact cabin (17 lines):
10187
11174
  room(0,0,0,8,4,6,"a","b","a")
10188
11175
  roof(0,4,0,8,6,"gable","c")
10189
- wall(-4,0,-2,4,0,-2,4,1,"a")
11176
+ wall(-4,-2,4,-2,4,1,"a")
10190
11177
  part(0,2,3,3,3,0.3,"a","Block",0.4)
10191
11178
  row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
10192
11179
  column(-3,0,-2,4,0.5,"a","b")
@@ -11164,15 +12151,15 @@ part(0,2,0,2,1,1,"b")`,
11164
12151
  });
11165
12152
 
11166
12153
  // ../core/dist/install-plugin-helpers.js
11167
- import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
12154
+ import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync } from "fs";
11168
12155
  import { execSync } from "child_process";
11169
- import { join as join4 } from "path";
11170
- import { homedir as homedir4 } from "os";
12156
+ import { join as join5 } from "path";
12157
+ import { homedir as homedir5 } from "os";
11171
12158
  function isWSL() {
11172
12159
  if (process.platform !== "linux")
11173
12160
  return false;
11174
12161
  try {
11175
- const v = readFileSync5("/proc/version", "utf8");
12162
+ const v = readFileSync6("/proc/version", "utf8");
11176
12163
  return /microsoft|wsl/i.test(v);
11177
12164
  } catch {
11178
12165
  return false;
@@ -11190,7 +12177,7 @@ function getWindowsUserPluginsDir() {
11190
12177
  }).toString().trim();
11191
12178
  if (!linuxPath)
11192
12179
  return null;
11193
- return join4(linuxPath, "Roblox", "Plugins");
12180
+ return join5(linuxPath, "Roblox", "Plugins");
11194
12181
  } catch {
11195
12182
  return null;
11196
12183
  }
@@ -11199,7 +12186,7 @@ function getPluginsFolder() {
11199
12186
  if (process.env.MCP_PLUGINS_DIR)
11200
12187
  return process.env.MCP_PLUGINS_DIR;
11201
12188
  if (process.platform === "win32") {
11202
- return join4(process.env.LOCALAPPDATA || join4(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
12189
+ return join5(process.env.LOCALAPPDATA || join5(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
11203
12190
  }
11204
12191
  if (isWSL()) {
11205
12192
  const win = getWindowsUserPluginsDir();
@@ -11207,11 +12194,11 @@ function getPluginsFolder() {
11207
12194
  return win;
11208
12195
  console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
11209
12196
  }
11210
- return join4(homedir4(), "Documents", "Roblox", "Plugins");
12197
+ return join5(homedir5(), "Documents", "Roblox", "Plugins");
11211
12198
  }
11212
12199
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
11213
- const otherDest = join4(pluginsFolder, otherAssetName);
11214
- if (!existsSync4(otherDest))
12200
+ const otherDest = join5(pluginsFolder, otherAssetName);
12201
+ if (!existsSync5(otherDest))
11215
12202
  return;
11216
12203
  if (replace) {
11217
12204
  try {
@@ -11256,8 +12243,8 @@ __export(install_plugin_exports, {
11256
12243
  installBundledPlugin: () => installBundledPlugin,
11257
12244
  installPlugin: () => installPlugin
11258
12245
  });
11259
- import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
11260
- import { dirname as dirname3, join as join5 } from "path";
12246
+ import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
12247
+ import { dirname as dirname4, join as join6 } from "path";
11261
12248
  import { fileURLToPath } from "url";
11262
12249
  import { get } from "https";
11263
12250
  function httpsGet(url) {
@@ -11317,8 +12304,8 @@ function prepareInstall({
11317
12304
  warn
11318
12305
  }) {
11319
12306
  const pluginsFolder = getPluginsFolder();
11320
- if (!existsSync5(pluginsFolder)) {
11321
- mkdirSync4(pluginsFolder, { recursive: true });
12307
+ if (!existsSync6(pluginsFolder)) {
12308
+ mkdirSync5(pluginsFolder, { recursive: true });
11322
12309
  }
11323
12310
  handleVariantConflict({
11324
12311
  pluginsFolder,
@@ -11330,23 +12317,23 @@ function prepareInstall({
11330
12317
  return pluginsFolder;
11331
12318
  }
11332
12319
  function bundledAssetPath() {
11333
- const currentDir = dirname3(fileURLToPath(import.meta.url));
12320
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
11334
12321
  const candidates = [
11335
- join5(currentDir, "..", "studio-plugin", ASSET_NAME),
11336
- join5(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
12322
+ join6(currentDir, "..", "studio-plugin", ASSET_NAME),
12323
+ join6(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
11337
12324
  ];
11338
- return candidates.find((candidate) => existsSync5(candidate)) ?? null;
12325
+ return candidates.find((candidate) => existsSync6(candidate)) ?? null;
11339
12326
  }
11340
12327
  function packageVersion() {
11341
- const currentDir = dirname3(fileURLToPath(import.meta.url));
11342
- const pkg = JSON.parse(readFileSync6(join5(currentDir, "..", "package.json"), "utf8"));
12328
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
12329
+ const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
11343
12330
  if (!pkg.version) {
11344
12331
  throw new Error("Package version not found");
11345
12332
  }
11346
12333
  return pkg.version;
11347
12334
  }
11348
12335
  function bundledPluginVersion(source) {
11349
- const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
12336
+ const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
11350
12337
  return match ? match[1] : null;
11351
12338
  }
11352
12339
  function assertBundledPluginVersion(source) {
@@ -11359,9 +12346,9 @@ function assertBundledPluginVersion(source) {
11359
12346
  }
11360
12347
  }
11361
12348
  function filesMatch(a, b) {
11362
- if (!existsSync5(b)) return false;
11363
- const aBytes = readFileSync6(a);
11364
- const bBytes = readFileSync6(b);
12349
+ if (!existsSync6(b)) return false;
12350
+ const aBytes = readFileSync7(a);
12351
+ const bBytes = readFileSync7(b);
11365
12352
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
11366
12353
  }
11367
12354
  async function installBundledPlugin(options = {}) {
@@ -11374,7 +12361,7 @@ async function installBundledPlugin(options = {}) {
11374
12361
  }
11375
12362
  assertBundledPluginVersion(source);
11376
12363
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
11377
- const dest = join5(pluginsFolder, ASSET_NAME);
12364
+ const dest = join6(pluginsFolder, ASSET_NAME);
11378
12365
  if (filesMatch(source, dest)) return;
11379
12366
  copyFileSync2(source, dest);
11380
12367
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -11387,7 +12374,7 @@ async function installPlugin(options = {}) {
11387
12374
  const bundled = bundledAssetPath();
11388
12375
  if (bundled) {
11389
12376
  assertBundledPluginVersion(bundled);
11390
- const dest2 = join5(pluginsFolder, ASSET_NAME);
12377
+ const dest2 = join6(pluginsFolder, ASSET_NAME);
11391
12378
  if (filesMatch(bundled, dest2)) {
11392
12379
  log(`${ASSET_NAME} already installed.`);
11393
12380
  return;
@@ -11402,7 +12389,7 @@ async function installPlugin(options = {}) {
11402
12389
  if (!asset) {
11403
12390
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
11404
12391
  }
11405
- const dest = join5(pluginsFolder, ASSET_NAME);
12392
+ const dest = join6(pluginsFolder, ASSET_NAME);
11406
12393
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
11407
12394
  await download(asset.browser_download_url, dest);
11408
12395
  log(`Installed to ${dest}`);