@1e0zj/dsh-plugin-mall 0.1.18 → 0.2.1
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 +111 -5
- package/package.json +6 -2
- package/src/cli.js +1638 -0
- package/src/client.js +306 -67
- package/src/github.js +71 -3
- package/src/guard.js +2413 -0
- package/src/index.js +1761 -88
- package/src/installer.js +1585 -77
package/src/github.js
CHANGED
|
@@ -373,7 +373,7 @@ export function compareVersions(a, b) {
|
|
|
373
373
|
|
|
374
374
|
/** Cap on concurrent outbound requests — shared by verification and update checks. */
|
|
375
375
|
export const NETWORK_CONCURRENCY = 8;
|
|
376
|
-
// repo -> {kind, name, version, hostDeps, ts}
|
|
376
|
+
// repo -> {kind, name, version, hostDeps, manifest, ts}
|
|
377
377
|
const verifyCache = new Map();
|
|
378
378
|
// Verdicts expire. Caching a success for the process lifetime freezes the badge
|
|
379
379
|
// on whatever the repo looked like the first time it was seen: when dsh-TUI
|
|
@@ -442,6 +442,44 @@ async function fetchRawPackageJson(repo, signal, sources) {
|
|
|
442
442
|
throw lastError ?? new Error("no raw source reachable");
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Fetch an arbitrary repo file as text from the same raw sources. The path
|
|
447
|
+
* comes from a plugin manifest's `dsh.bundle.patch`, i.e. untrusted input:
|
|
448
|
+
* anything absolute, parent-traversing, or backslashed is rejected before a
|
|
449
|
+
* URL is ever built. Source templates carry `package.json` as their path; the
|
|
450
|
+
* tail is swapped for the requested file so custom `rawSources` keep working.
|
|
451
|
+
* Returns undefined when every reachable source 404s.
|
|
452
|
+
*/
|
|
453
|
+
export async function fetchRawFile(repo, filePath, { signal, sources } = {}) {
|
|
454
|
+
const parts = String(filePath ?? "").split("/").filter((part) => part.length > 0 && part !== ".");
|
|
455
|
+
if (parts.length === 0 || parts.some((part) => part === ".." || part.includes("\\") || part.includes(":"))) {
|
|
456
|
+
throw new Error(`unsafe repo file path: ${JSON.stringify(filePath)}`);
|
|
457
|
+
}
|
|
458
|
+
const tail = parts.map(encodeURIComponent).join("/");
|
|
459
|
+
let saw404 = false;
|
|
460
|
+
let lastError;
|
|
461
|
+
for (const template of rawSourcesOf(sources)) {
|
|
462
|
+
const base = template.replace("{repo}", repo);
|
|
463
|
+
const url = base.includes("{path}")
|
|
464
|
+
? base.replace("{path}", tail)
|
|
465
|
+
: base.replace(/package\.json$/, tail);
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch(url, {
|
|
468
|
+
headers: { "User-Agent": "dsh-plugin-mall", Accept: "text/plain, */*" },
|
|
469
|
+
signal,
|
|
470
|
+
});
|
|
471
|
+
if (response.status === 404) { saw404 = true; continue; }
|
|
472
|
+
if (!response.ok) throw new Error(`source returned ${response.status}`);
|
|
473
|
+
return await response.text();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error?.name === "AbortError") throw error;
|
|
476
|
+
lastError = error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (saw404) return undefined;
|
|
480
|
+
throw lastError ?? new Error("no raw source reachable");
|
|
481
|
+
}
|
|
482
|
+
|
|
445
483
|
/**
|
|
446
484
|
* Framework packages the host provides via the profiles/node_modules fallback.
|
|
447
485
|
* A plugin declaring any of these as `dependencies` (instead of
|
|
@@ -495,13 +533,13 @@ export async function verifyPlugins({ repos, signal, sources }) {
|
|
|
495
533
|
: typeof pkg.dsh?.bundle?.patch === "string" ? "bundle"
|
|
496
534
|
: pkg.dsh?.client !== undefined ? "client"
|
|
497
535
|
: "plain";
|
|
498
|
-
verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version, hostDeps: hostShadowDependencies(pkg), ts: Date.now() });
|
|
536
|
+
verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version, hostDeps: hostShadowDependencies(pkg), manifest: pkg, ts: Date.now() });
|
|
499
537
|
} catch (error) {
|
|
500
538
|
if (error?.name === "AbortError") throw error;
|
|
501
539
|
verifyCache.set(repo, { kind: "unknown", ts: Date.now() });
|
|
502
540
|
}
|
|
503
541
|
});
|
|
504
|
-
// 一律重建对象:内部的 ts 不该出现在发给浏览器的响应里。
|
|
542
|
+
// 一律重建对象:内部的 ts 与完整 manifest 不该出现在发给浏览器的响应里。
|
|
505
543
|
const results = {};
|
|
506
544
|
for (const repo of wanted) {
|
|
507
545
|
const cached = verifyCache.get(repo);
|
|
@@ -510,6 +548,15 @@ export async function verifyPlugins({ repos, signal, sources }) {
|
|
|
510
548
|
return { results };
|
|
511
549
|
}
|
|
512
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The cached full manifest for a repo already seen by verifyPlugins, or
|
|
553
|
+
* undefined. Server-side only — the browsing-time compat scan needs the whole
|
|
554
|
+
* package.json (peers, engines, dsh.conflicts), not just the badge metadata.
|
|
555
|
+
*/
|
|
556
|
+
export function cachedRepoManifest(repo) {
|
|
557
|
+
return verifyCache.get(String(repo ?? ""))?.manifest;
|
|
558
|
+
}
|
|
559
|
+
|
|
513
560
|
/**
|
|
514
561
|
* Fetch one repository's metadata plus its package.json (base64-decoded),
|
|
515
562
|
* which is what tells us whether it declares a dsh bundle patch.
|
|
@@ -612,6 +659,27 @@ if (process.argv[1]?.endsWith("github.js") && process.argv.includes("--self-test
|
|
|
612
659
|
const failed = runHostShadowFixtures();
|
|
613
660
|
console.log(`${HOST_SHADOW_FIXTURES.length - failed}/${HOST_SHADOW_FIXTURES.length} passed\n`);
|
|
614
661
|
if (failed > 0) process.exit(1);
|
|
662
|
+
// fetchRawFile 路径钳制:来自插件清单的 dsh.bundle.patch 是不可信输入,
|
|
663
|
+
// 越界/绝对/反斜杠路径必须在拼 URL 之前被拒绝(这些用例零网络)。
|
|
664
|
+
{
|
|
665
|
+
const deadSource = ["http://127.0.0.1:1/{repo}/package.json"];
|
|
666
|
+
let clampFailed = 0;
|
|
667
|
+
for (const bad of ["../secret", "a/../../b", "dir\\win.yml", "c:/abs.yml", ""]) {
|
|
668
|
+
let rejected = false;
|
|
669
|
+
try { await fetchRawFile("owner/repo", bad, { sources: deadSource }); } catch { rejected = true; }
|
|
670
|
+
if (!rejected) { clampFailed++; console.log(` FAIL 危险路径应拒绝: ${JSON.stringify(bad)}`); }
|
|
671
|
+
else console.log(` PASS 危险路径拒绝: ${JSON.stringify(bad)}`);
|
|
672
|
+
}
|
|
673
|
+
// 合法相对路径要通过钳制、真正走到网络(dead source 必失败,但报错不能是钳制错误)。
|
|
674
|
+
{
|
|
675
|
+
let reached = false;
|
|
676
|
+
try { await fetchRawFile("owner/repo", "subdir/patch file.yml", { sources: deadSource }); }
|
|
677
|
+
catch (error) { reached = !/unsafe repo file path/.test(error?.message ?? ""); }
|
|
678
|
+
if (!reached) { clampFailed++; console.log(" FAIL 合法路径应通过钳制并尝试网络"); }
|
|
679
|
+
else console.log(" PASS 合法路径通过钳制(网络不可达按预期失败)");
|
|
680
|
+
}
|
|
681
|
+
if (clampFailed > 0) process.exit(1);
|
|
682
|
+
}
|
|
615
683
|
if (process.argv.includes("--offline")) process.exit(0);
|
|
616
684
|
const apiBase = "https://api.github.com";
|
|
617
685
|
const result = await searchPlugins({ query: "", perPage: 3, apiBase });
|