openmagic 0.43.0 → 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 +1 -0
- package/dist/cli.js +113 -25
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +34 -34
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +5 -3
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
|
-
|
|
263
|
-
|
|
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 (
|
|
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(
|
|
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(`
|
|
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
|
-
|
|
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
|
|
@@ -2862,7 +2932,7 @@ function checkDependenciesInstalled(cwd = process.cwd()) {
|
|
|
2862
2932
|
// src/cli.ts
|
|
2863
2933
|
import { createRequire as createRequire2 } from "module";
|
|
2864
2934
|
try {
|
|
2865
|
-
execSync2("ulimit -n 65536", { shell:
|
|
2935
|
+
execSync2("ulimit -n 65536", { shell: "/bin/sh", stdio: "ignore" });
|
|
2866
2936
|
} catch {
|
|
2867
2937
|
}
|
|
2868
2938
|
var origEmitWarning = process.emitWarning;
|
|
@@ -2973,13 +3043,13 @@ process.on("uncaughtException", (err) => {
|
|
|
2973
3043
|
process.exit(1);
|
|
2974
3044
|
});
|
|
2975
3045
|
try {
|
|
2976
|
-
const fdLimit = parseInt(execSync2("ulimit -n", { encoding: "utf-8", shell:
|
|
3046
|
+
const fdLimit = parseInt(execSync2("ulimit -n", { encoding: "utf-8", shell: "/bin/sh" }).trim(), 10);
|
|
2977
3047
|
if (fdLimit > 0 && fdLimit < 4096) {
|
|
2978
3048
|
try {
|
|
2979
|
-
execSync2("ulimit -n 65536", { shell:
|
|
3049
|
+
execSync2("ulimit -n 65536", { shell: "/bin/sh", stdio: "ignore" });
|
|
2980
3050
|
} catch {
|
|
2981
3051
|
}
|
|
2982
|
-
const newLimit = parseInt(execSync2("ulimit -n", { encoding: "utf-8", shell:
|
|
3052
|
+
const newLimit = parseInt(execSync2("ulimit -n", { encoding: "utf-8", shell: "/bin/sh" }).trim(), 10);
|
|
2983
3053
|
if (newLimit < 4096) {
|
|
2984
3054
|
writeLine();
|
|
2985
3055
|
printWarning(`File descriptor limit is ${fdLimit} (need 4096+).`);
|
|
@@ -3087,7 +3157,7 @@ function runCommand(cmd, args, cwd = process.cwd()) {
|
|
|
3087
3157
|
const child = spawn5(cmd, args, {
|
|
3088
3158
|
cwd,
|
|
3089
3159
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3090
|
-
shell:
|
|
3160
|
+
shell: "/bin/sh"
|
|
3091
3161
|
});
|
|
3092
3162
|
child.stdout?.on("data", (data) => {
|
|
3093
3163
|
const lines = data.toString().trim().split("\n");
|
|
@@ -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
|
|
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
|
-
|
|
3485
|
-
|
|
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();
|
|
@@ -3595,7 +3683,7 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3595
3683
|
const compat = checkNodeCompatibility(chosen.framework);
|
|
3596
3684
|
if (!compat.ok) {
|
|
3597
3685
|
writeLine();
|
|
3598
|
-
printError(compat.message);
|
|
3686
|
+
printError(compat.message || "Node.js version incompatible");
|
|
3599
3687
|
writeLine();
|
|
3600
3688
|
printInfo("Switch Node.js before running:");
|
|
3601
3689
|
printCommand("nvm use 20");
|
|
@@ -3712,7 +3800,7 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3712
3800
|
if (chosen?.framework) {
|
|
3713
3801
|
const compat2 = checkNodeCompatibility(chosen.framework);
|
|
3714
3802
|
if (!compat2.ok) {
|
|
3715
|
-
printWarning(compat2.message);
|
|
3803
|
+
printWarning(compat2.message || "Node.js version may be incompatible");
|
|
3716
3804
|
printDetail("Switch with:");
|
|
3717
3805
|
printCommand("nvm use 20");
|
|
3718
3806
|
writeLine();
|