@hoardodile/workbench 0.1.1 → 0.1.3
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 -1
- package/dist/assets/index-D_MtMbTH.js +77 -0
- package/dist/index.html +1 -1
- package/dist/mounts.mjs +85 -4
- package/package.json +6 -6
- package/dist/assets/index-DUdQ8cf0.js +0 -77
package/dist/index.html
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
})()
|
|
43
43
|
</script>
|
|
44
|
-
<script type="module" crossorigin src="/assets/index-
|
|
44
|
+
<script type="module" crossorigin src="/assets/index-D_MtMbTH.js"></script>
|
|
45
45
|
<link rel="stylesheet" crossorigin href="/assets/index-Bje_SHj7.css">
|
|
46
46
|
</head>
|
|
47
47
|
<body>
|
package/dist/mounts.mjs
CHANGED
|
@@ -489,9 +489,11 @@ const VAULT_DELETE_PATH = "/api/workbench/vault/delete"
|
|
|
489
489
|
* `nosniff`, `access-control-allow-origin: *` (opaque-origin iframe),
|
|
490
490
|
* HTML demoted to an attachment, `no-store` cache.
|
|
491
491
|
* - policy mirrors the server where it matters: http(s) only, no URL
|
|
492
|
-
* userinfo,
|
|
493
|
-
*
|
|
494
|
-
*
|
|
492
|
+
* userinfo, IP-literal public-address rule (unless
|
|
493
|
+
* `WORKBENCH_VAULT_ALLOW_PRIVATE=1`), ≤5 redirects, size cap
|
|
494
|
+
* (`WORKBENCH_VAULT_MAX_BYTES`, default 200 MiB), optional sha256 pin,
|
|
495
|
+
* atomic temp→rename write, dest confined to the plugin's vault
|
|
496
|
+
* directory.
|
|
495
497
|
*/
|
|
496
498
|
function pluginAssetsMount(vaultRoot) {
|
|
497
499
|
return async (req, res) => {
|
|
@@ -670,9 +672,10 @@ function validVaultDest(dest) {
|
|
|
670
672
|
)
|
|
671
673
|
}
|
|
672
674
|
|
|
673
|
-
/** Fetch with the dev-policy subset: http(s), no userinfo, ≤5 redirects, size cap. */
|
|
675
|
+
/** Fetch with the dev-policy subset: http(s), no userinfo, IP-literal public-address rule, ≤5 redirects, size cap. */
|
|
674
676
|
async function fetchWithPolicy(rawUrl, maxBytes) {
|
|
675
677
|
let url = new URL(rawUrl)
|
|
678
|
+
const allowPrivate = process.env.WORKBENCH_VAULT_ALLOW_PRIVATE === "1"
|
|
676
679
|
function vet(next) {
|
|
677
680
|
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
678
681
|
throw new Error(`unsupported scheme: ${next.protocol}`)
|
|
@@ -680,6 +683,14 @@ async function fetchWithPolicy(rawUrl, maxBytes) {
|
|
|
680
683
|
if (next.username.length > 0 || next.password.length > 0) {
|
|
681
684
|
throw new Error("URLs must not embed credentials")
|
|
682
685
|
}
|
|
686
|
+
if (!allowPrivate) {
|
|
687
|
+
const verdict = ipLiteralVerdict(next.hostname)
|
|
688
|
+
if (verdict === false) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`address is not public: ${next.hostname} — set WORKBENCH_VAULT_ALLOW_PRIVATE=1 to allow private ranges`,
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
683
694
|
return next
|
|
684
695
|
}
|
|
685
696
|
vet(url)
|
|
@@ -713,6 +724,76 @@ async function fetchWithPolicy(rawUrl, maxBytes) {
|
|
|
713
724
|
throw new Error("more than 5 redirects")
|
|
714
725
|
}
|
|
715
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Workbench dev mirror of the server's IP-literal public-address rule
|
|
729
|
+
* (see `packages/shared` `isPublicAddress`): private, loopback,
|
|
730
|
+
* link-local, CGNAT, documentation, benchmarking and multicast ranges
|
|
731
|
+
* are not globally routable. Returns `undefined` for hostnames — the dev
|
|
732
|
+
* tool deliberately does not resolve DNS per hop (fake-IP proxies and
|
|
733
|
+
* split-horizon DNS would make it unusable); the app's resolve-time
|
|
734
|
+
* pinning stays a server-side feature.
|
|
735
|
+
*/
|
|
736
|
+
function ipLiteralVerdict(address) {
|
|
737
|
+
if (!address.includes(":") && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) {
|
|
738
|
+
return undefined
|
|
739
|
+
}
|
|
740
|
+
// The URL hostname keeps the IPv6 brackets — strip them before any
|
|
741
|
+
// range matching.
|
|
742
|
+
if (address.startsWith("[") && address.endsWith("]")) {
|
|
743
|
+
address = address.slice(1, -1)
|
|
744
|
+
}
|
|
745
|
+
if (!address.includes(":")) {
|
|
746
|
+
const parts = address.split(".").map(Number)
|
|
747
|
+
const [a, b, c] = parts
|
|
748
|
+
if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
|
|
749
|
+
return undefined
|
|
750
|
+
}
|
|
751
|
+
if (a === 0 || a === 10 || a === 127) return false
|
|
752
|
+
if (a === 169 && b === 254) return false
|
|
753
|
+
if (a === 172 && b >= 16 && b <= 31) return false
|
|
754
|
+
if (a === 192 && b === 168) return false
|
|
755
|
+
if (a === 100 && b >= 64 && b <= 127) return false
|
|
756
|
+
if (a === 192 && b === 0 && c === 2) return false
|
|
757
|
+
if (a === 198 && (b === 18 || b === 19)) return false
|
|
758
|
+
if (a === 198 && b === 51 && c === 100) return false
|
|
759
|
+
if (a === 203 && b === 0 && c === 113) return false
|
|
760
|
+
if (a >= 224) return false
|
|
761
|
+
return true
|
|
762
|
+
}
|
|
763
|
+
const lower = address.split("%")[0].toLowerCase()
|
|
764
|
+
if (lower === "::" || lower === "::1") return false
|
|
765
|
+
if (lower.startsWith("::ffff:")) {
|
|
766
|
+
// The URL parser may normalize the dotted quad to hex groups
|
|
767
|
+
// (e.g. ::ffff:127.0.0.1 → [::ffff:7f00:1]) — decode the embedded
|
|
768
|
+
// IPv4 back out and re-check it.
|
|
769
|
+
const digits = lower
|
|
770
|
+
.slice("::ffff:".length)
|
|
771
|
+
.split(":")
|
|
772
|
+
.map((group) => group.padStart(4, "0"))
|
|
773
|
+
.join("")
|
|
774
|
+
.slice(-8)
|
|
775
|
+
if (/^[0-9a-f]{8}$/.test(digits)) {
|
|
776
|
+
const ipv4 = [0, 2, 4, 6]
|
|
777
|
+
.map((offset) => Number.parseInt(digits.slice(offset, offset + 2), 16))
|
|
778
|
+
.join(".")
|
|
779
|
+
return ipLiteralVerdict(ipv4)
|
|
780
|
+
}
|
|
781
|
+
return true
|
|
782
|
+
}
|
|
783
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return false
|
|
784
|
+
if (
|
|
785
|
+
lower.startsWith("fe8") ||
|
|
786
|
+
lower.startsWith("fe9") ||
|
|
787
|
+
lower.startsWith("fea") ||
|
|
788
|
+
lower.startsWith("feb")
|
|
789
|
+
) {
|
|
790
|
+
return false
|
|
791
|
+
}
|
|
792
|
+
if (lower.startsWith("2001:db8:")) return false
|
|
793
|
+
if (lower.startsWith("ff")) return false
|
|
794
|
+
return true
|
|
795
|
+
}
|
|
796
|
+
|
|
716
797
|
function sha256File(path) {
|
|
717
798
|
return createHash("sha256").update(readFileSync(path)).digest("hex")
|
|
718
799
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoardodile/workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Offline dev workbench for hoardodile content plugins.",
|
|
6
6
|
"keywords": [
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"vite": "^8.2.2",
|
|
51
51
|
"vitest": "^4.1.11",
|
|
52
52
|
"zod": "^4.4.3",
|
|
53
|
-
"@hoardodile/host-web": "0.1.
|
|
54
|
-
"@hoardodile/
|
|
55
|
-
"@hoardodile/
|
|
56
|
-
"@hoardodile/sdk-web": "0.1.
|
|
57
|
-
"@hoardodile/ui": "0.1.
|
|
53
|
+
"@hoardodile/host-web": "0.1.3",
|
|
54
|
+
"@hoardodile/i18n": "0.1.3",
|
|
55
|
+
"@hoardodile/sdk-types": "0.1.3",
|
|
56
|
+
"@hoardodile/sdk-web": "0.1.3",
|
|
57
|
+
"@hoardodile/ui": "0.1.3"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"dev": "node scripts/dev.mjs",
|