openmagic 0.43.1 → 0.43.2

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/README.md CHANGED
@@ -184,6 +184,7 @@ npx openmagic --port 3000
184
184
 
185
185
  - **Origin change** -- Your app runs on `:3000` but you access it via `:4567`. This can break OAuth redirect URIs, `localStorage` isolation, and Service Worker scope. Most dev setups work fine, but if your app checks `window.location.origin`, you may need to adjust your dev config.
186
186
  - **CSP via meta tags** -- OpenMagic strips CSP response headers so the toolbar script can load, but `<meta>` tag CSP can't be modified at the proxy level and may still block it.
187
+ - **Same-page trust boundary** -- The toolbar runs inside your proxied development page so it can inspect selected DOM elements. Do not use OpenMagic with untrusted third-party scripts running in that page.
187
188
  - **Not for production** -- This is a dev tool. Don't deploy the proxy to production.
188
189
 
189
190
  ---
package/dist/cli.js CHANGED
@@ -214,7 +214,7 @@ import {
214
214
  closeSync,
215
215
  renameSync as renameSync2
216
216
  } from "fs";
217
- import { join as join3, resolve, relative, dirname, extname } from "path";
217
+ import { join as join3, resolve, relative, dirname, extname, parse } from "path";
218
218
  import { tmpdir } from "os";
219
219
  import { createHash } from "crypto";
220
220
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
@@ -250,19 +250,41 @@ var IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([
250
250
  ]);
251
251
  function isPathSafe(filePath, roots) {
252
252
  const resolved = resolve(filePath);
253
- let real;
254
- try {
255
- real = realpathSync(resolved);
256
- } catch {
257
- real = resolved;
258
- }
259
253
  return roots.some((root) => {
260
254
  const resolvedRoot = resolve(root);
255
+ let realRoot;
256
+ try {
257
+ realRoot = realpathSync(resolvedRoot);
258
+ } catch {
259
+ return false;
260
+ }
261
261
  const rel = relative(resolvedRoot, resolved);
262
- const realRel = relative(resolvedRoot, real);
263
- return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\") && (!realRel.startsWith("..") && !realRel.startsWith("/") && !realRel.startsWith("\\"));
262
+ if (isOutsideRelative(rel)) return false;
263
+ const existingPath = nearestExistingPath(resolved);
264
+ if (!existingPath) return false;
265
+ let real;
266
+ try {
267
+ real = realpathSync(existingPath);
268
+ } catch {
269
+ return false;
270
+ }
271
+ const realRel = relative(realRoot, real);
272
+ return !isOutsideRelative(realRel);
264
273
  });
265
274
  }
275
+ function isOutsideRelative(relPath) {
276
+ return relPath === ".." || relPath.startsWith(`..${"/"}`) || relPath.startsWith(`..${"\\"}`) || relPath.startsWith("/") || relPath.startsWith("\\");
277
+ }
278
+ function nearestExistingPath(filePath) {
279
+ let current = resolve(filePath);
280
+ const root = parse(current).root;
281
+ while (!existsSync3(current)) {
282
+ const parent = dirname(current);
283
+ if (parent === current || current === root) return null;
284
+ current = parent;
285
+ }
286
+ return current;
287
+ }
266
288
  var fileMetadata = /* @__PURE__ */ new Map();
267
289
  function readFileSafe(filePath, roots) {
268
290
  if (!isPathSafe(filePath, roots)) {
@@ -346,6 +368,24 @@ function writeFileSafe(filePath, content, roots) {
346
368
  return { ok: false, error: `Failed to write file: ${e.message}` };
347
369
  }
348
370
  }
371
+ function deleteFileSafe(filePath, roots) {
372
+ if (!isPathSafe(filePath, roots)) {
373
+ return { ok: false, error: "Path is outside allowed roots" };
374
+ }
375
+ try {
376
+ if (!existsSync3(filePath)) {
377
+ return { ok: true };
378
+ }
379
+ const stat = lstatSync(filePath);
380
+ if (!stat.isFile()) {
381
+ return { ok: false, error: "Can only delete regular files" };
382
+ }
383
+ unlinkSync(filePath);
384
+ return { ok: true };
385
+ } catch (e) {
386
+ return { ok: false, error: `Failed to delete file: ${e.message}` };
387
+ }
388
+ }
349
389
  var MAX_LIST_ENTRIES = 2e3;
350
390
  function listFiles(rootPath, roots, maxDepth = 4) {
351
391
  if (!isPathSafe(rootPath, roots)) {
@@ -2127,7 +2167,7 @@ function attachOpenMagic(httpServer, roots) {
2127
2167
  if (!req.url?.startsWith("/__openmagic__/")) return false;
2128
2168
  const urlPath = req.url.split("?")[0];
2129
2169
  if (urlPath === "/__openmagic__/toolbar.js") {
2130
- serveToolbarBundle(res);
2170
+ serveToolbarBundle(res, getSessionToken());
2131
2171
  return true;
2132
2172
  }
2133
2173
  if (urlPath === "/__openmagic__/health") {
@@ -2144,7 +2184,7 @@ function attachOpenMagic(httpServer, roots) {
2144
2184
  const clientStates = /* @__PURE__ */ new WeakMap();
2145
2185
  wss.on("connection", (ws, req) => {
2146
2186
  const origin = req.headers.origin || "";
2147
- if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
2187
+ if (!isAllowedWsOrigin(origin)) {
2148
2188
  ws.close(4003, "Forbidden origin");
2149
2189
  return;
2150
2190
  }
@@ -2252,6 +2292,24 @@ async function handleMessage(ws, msg, state, roots) {
2252
2292
  }
2253
2293
  break;
2254
2294
  }
2295
+ case "fs.delete": {
2296
+ const payload = msg.payload;
2297
+ if (!payload?.path) {
2298
+ sendError(ws, "invalid_payload", "Missing path", msg.id);
2299
+ break;
2300
+ }
2301
+ const deleteResult = deleteFileSafe(payload.path, roots);
2302
+ if (!deleteResult.ok) {
2303
+ sendError(ws, "fs_error", deleteResult.error || "Delete failed", msg.id);
2304
+ } else {
2305
+ send(ws, {
2306
+ id: msg.id,
2307
+ type: "fs.deleted",
2308
+ payload: { path: payload.path, ok: true }
2309
+ });
2310
+ }
2311
+ break;
2312
+ }
2255
2313
  case "fs.undo": {
2256
2314
  const payload = msg.payload;
2257
2315
  if (!payload?.path) {
@@ -2406,6 +2464,15 @@ async function handleMessage(ws, msg, state, roots) {
2406
2464
  sendError(ws, "unknown_type", `Unknown message type: ${msg.type}`, msg.id);
2407
2465
  }
2408
2466
  }
2467
+ function isAllowedWsOrigin(origin) {
2468
+ if (!origin) return true;
2469
+ try {
2470
+ const parsed = new URL(origin);
2471
+ return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" || parsed.hostname === "[::1]";
2472
+ } catch {
2473
+ return false;
2474
+ }
2475
+ }
2409
2476
  function send(ws, msg) {
2410
2477
  if (ws.readyState === WebSocket.OPEN) {
2411
2478
  ws.send(JSON.stringify(msg));
@@ -2414,7 +2481,7 @@ function send(ws, msg) {
2414
2481
  function sendError(ws, code, message, id) {
2415
2482
  send(ws, { id: id || "error", type: "error", payload: { code, message } });
2416
2483
  }
2417
- function serveToolbarBundle(res) {
2484
+ function serveToolbarBundle(res, token) {
2418
2485
  const bundlePaths = [
2419
2486
  join4(__dirname, "toolbar", "index.global.js"),
2420
2487
  join4(__dirname, "..", "dist", "toolbar", "index.global.js")
@@ -2428,7 +2495,8 @@ function serveToolbarBundle(res) {
2428
2495
  "Access-Control-Allow-Origin": "*",
2429
2496
  "Cache-Control": "no-cache"
2430
2497
  });
2431
- res.end(content);
2498
+ res.end(`const __OPENMAGIC_TOKEN__=${JSON.stringify(token)};
2499
+ ${content}`);
2432
2500
  return;
2433
2501
  }
2434
2502
  } catch {
@@ -2439,7 +2507,8 @@ function serveToolbarBundle(res) {
2439
2507
  "Content-Type": "application/javascript",
2440
2508
  "Access-Control-Allow-Origin": "*"
2441
2509
  });
2442
- res.end(`(function(){var d=document.createElement("div");d.style.cssText="position:fixed;bottom:20px;right:20px;background:#1a1a2e;color:#e94560;padding:16px 24px;border-radius:12px;font-family:system-ui;font-size:14px;z-index:2147483647;box-shadow:0 4px 24px rgba(0,0,0,0.3);";d.textContent="OpenMagic: Toolbar bundle not found.";document.body.appendChild(d);})();`);
2510
+ res.end(`const __OPENMAGIC_TOKEN__=${JSON.stringify(token)};
2511
+ (function(){var d=document.createElement("div");d.style.cssText="position:fixed;bottom:20px;right:20px;background:#1a1a2e;color:#e94560;padding:16px 24px;border-radius:12px;font-family:system-ui;font-size:14px;z-index:2147483647;box-shadow:0 4px 24px rgba(0,0,0,0.3);";d.textContent="OpenMagic: Toolbar bundle not found.";document.body.appendChild(d);})();`);
2443
2512
  }
2444
2513
 
2445
2514
  // src/proxy.ts
@@ -2560,7 +2629,8 @@ ${toolbarScript}
2560
2629
  return server;
2561
2630
  }
2562
2631
  function buildInjectionScript(token) {
2563
- return `<script src="/__openmagic__/toolbar.js?v=${Date.now()}" data-openmagic="true" data-openmagic-token="${token}" defer></script>`;
2632
+ void token;
2633
+ return `<script src="/__openmagic__/toolbar.js?v=${Date.now()}" data-openmagic="true" defer></script>`;
2564
2634
  }
2565
2635
 
2566
2636
  // src/detect.ts
@@ -3479,10 +3549,28 @@ async function offerToStartDevServer(expectedPort) {
3479
3549
  const http = require("http");
3480
3550
  const fs = require("fs");
3481
3551
  const path = require("path");
3482
- const mimes = {".html":"text/html",".css":"text/css",".js":"application/javascript",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".svg":"image/svg+xml",".ico":"image/x-icon",".gif":"image/gif",".woff2":"font/woff2",".woff":"font/woff"};
3552
+ const root = path.resolve(${JSON.stringify(process.cwd())});
3553
+ const mimes = {".html":"text/html",".css":"text/css",".js":"application/javascript",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".svg":"image/svg+xml",".ico":"image/x-icon",".gif":"image/gif",".webp":"image/webp",".woff2":"font/woff2",".woff":"font/woff"};
3554
+ function resolveRequestPath(reqUrl) {
3555
+ let pathname;
3556
+ try {
3557
+ const rawPath = (reqUrl || "/").split(/[?#]/, 1)[0];
3558
+ const decodedRawPath = decodeURIComponent(rawPath);
3559
+ if (decodedRawPath.split(/[\\\\/]+/).includes("..")) return null;
3560
+ pathname = new URL(reqUrl || "/", "http://localhost").pathname;
3561
+ pathname = decodeURIComponent(pathname);
3562
+ } catch {
3563
+ return null;
3564
+ }
3565
+ if (pathname === "/") pathname = "/index.html";
3566
+ const candidate = path.resolve(root, "." + pathname);
3567
+ const rel = path.relative(root, candidate);
3568
+ if (rel === ".." || rel.startsWith("../") || rel.startsWith("..\\\\") || path.isAbsolute(rel)) return null;
3569
+ return candidate;
3570
+ }
3483
3571
  http.createServer((req, res) => {
3484
- let p = path.join(${JSON.stringify(process.cwd())}, req.url === "/" ? "/index.html" : req.url);
3485
- try { p = decodeURIComponent(p); } catch {}
3572
+ const p = resolveRequestPath(req.url);
3573
+ if (!p) { res.writeHead(403); res.end("Forbidden"); return; }
3486
3574
  fs.readFile(p, (err, data) => {
3487
3575
  if (err) { res.writeHead(404); res.end("Not found"); return; }
3488
3576
  const ext = path.extname(p).toLowerCase();