@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/dist/index.html CHANGED
@@ -41,7 +41,7 @@
41
41
  }
42
42
  })()
43
43
  </script>
44
- <script type="module" crossorigin src="/assets/index-DUdQ8cf0.js"></script>
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, ≤5 redirects, size cap (`WORKBENCH_VAULT_MAX_BYTES`,
493
- * default 200 MiB), optional sha256 pin, atomic temp→rename write,
494
- * dest confined to the plugin's vault directory.
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.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.1",
54
- "@hoardodile/sdk-types": "0.1.1",
55
- "@hoardodile/i18n": "0.1.1",
56
- "@hoardodile/sdk-web": "0.1.1",
57
- "@hoardodile/ui": "0.1.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",