@openlines/opl-pi-sht 0.2.7 → 0.2.8

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.
@@ -54,8 +54,120 @@ export function isPdfContentType(contentType: string): boolean {
54
54
  return contentType.toLowerCase().includes("application/pdf");
55
55
  }
56
56
 
57
- /** Normalize a URL, allowing only http/https (file:/data:/etc. are rejected). */
58
- export function assertHttpUrl(url: string): string {
57
+ export interface HttpUrlOptions {
58
+ /** Allow private, link-local, and reserved network hosts (default false). Loopback is always allowed; cloud metadata is always blocked. */
59
+ allowPrivateNetwork?: boolean;
60
+ }
61
+
62
+ /** Well-known cloud metadata hostnames that resolve to instance-credential endpoints. */
63
+ const BLOCKED_METADATA_HOSTS = new Set([
64
+ "metadata",
65
+ "metadata.google.internal",
66
+ "instance-data",
67
+ "instance-data.ec2.internal",
68
+ ]);
69
+
70
+ /** Cloud metadata IPv6 literals (e.g. AWS IMDS over IPv6). Always blocked. */
71
+ const BLOCKED_METADATA_V6 = new Set(["fd00:ec2::254"]);
72
+
73
+ type HostClass = "loopback" | "metadata" | "private" | "public";
74
+
75
+ /** Decode a bare unsigned 32-bit integer hostname (e.g. 2130706433) into octets. */
76
+ function ipv4FromInteger(n: number): number[] | null {
77
+ if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) return null;
78
+ return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
79
+ }
80
+
81
+ /** Parse a host into IPv4 octets (dotted-quad or integer form), or null when not an IPv4 literal. */
82
+ function ipv4Octets(host: string): number[] | null {
83
+ if (/^\d+$/.test(host)) return ipv4FromInteger(Number(host));
84
+ const octets = host.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : NaN));
85
+ if (octets.length !== 4 || octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return null;
86
+ return octets;
87
+ }
88
+
89
+ function isLoopbackIpv4(o: number[]): boolean {
90
+ return o[0] === 127;
91
+ }
92
+
93
+ function isMetadataIpv4(o: number[]): boolean {
94
+ return (
95
+ (o[0] === 169 && o[1] === 254 && o[2] === 169 && o[3] === 254) || // AWS/Azure/GCP/Oracle
96
+ (o[0] === 100 && o[1] === 100 && o[2] === 100 && o[3] === 200) // Alibaba
97
+ );
98
+ }
99
+
100
+ /** Non-globally-routable (blocked by default, opt-in via allowPrivateNetwork). */
101
+ function isPrivateIpv4(o: number[]): boolean {
102
+ const [a, b, c] = o;
103
+ return (
104
+ a === 0 || a === 10 || // 0.0.0.0/8, 10/8
105
+ (a === 100 && b >= 64 && b <= 127) || // 100.64/10 carrier-grade NAT
106
+ (a === 169 && b === 254) || // 169.254/16 link-local
107
+ (a === 172 && b >= 16 && b <= 31) || // 172.16/12
108
+ (a === 192 && b === 168) || // 192.168/16
109
+ (a === 192 && b === 0 && (c === 0 || c === 2)) || // 192.0.0/24, 192.0.2/24
110
+ (a === 198 && (b === 18 || b === 19)) || // 198.18/15 benchmark
111
+ (a === 198 && b === 51 && c === 100) || // 198.51.100/24
112
+ (a === 203 && b === 0 && c === 113) || // 203.0.113/24
113
+ a >= 224 // 224/4 multicast and 240/4 reserved
114
+ );
115
+ }
116
+
117
+ /** Decode IPv4 from an IPv4-mapped IPv6 literal (::ffff:a.b.c.d or ::ffff:hex:hex). */
118
+ function ipv4MappedOctets(ip: string): number[] | null {
119
+ const dotted = ip.match(/^::ffff:(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
120
+ if (dotted) return dotted.slice(1).map(Number);
121
+ const hex = ip.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
122
+ if (hex) {
123
+ const n = (parseInt(hex[1], 16) << 16) | parseInt(hex[2], 16);
124
+ return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
125
+ }
126
+ return null;
127
+ }
128
+
129
+ function classifyIpv4Octets(o: number[]): HostClass {
130
+ if (isLoopbackIpv4(o)) return "loopback";
131
+ if (isMetadataIpv4(o)) return "metadata";
132
+ if (isPrivateIpv4(o)) return "private";
133
+ return "public";
134
+ }
135
+
136
+ function classifyIpv6Host(host: string): HostClass {
137
+ const ip = host.replace(/^\[/, "").replace(/\]$/, "").replace(/%.*$/, "").toLowerCase();
138
+ if (BLOCKED_METADATA_V6.has(ip)) return "metadata";
139
+ if (ip === "::1") return "loopback";
140
+ const mapped = ipv4MappedOctets(ip);
141
+ if (mapped) return classifyIpv4Octets(mapped);
142
+ if (ip === "::") return "private"; // unspecified
143
+ if (/^f[cd]/.test(ip)) return "private"; // fc00::/7 unique-local
144
+ if (/^fe[89ab]/.test(ip)) return "private"; // fe80::/10 link-local
145
+ if (/^ff/.test(ip)) return "private"; // ff00::/8 multicast
146
+ return "public";
147
+ }
148
+
149
+ function classifyHost(host: string): HostClass {
150
+ const lowered = host.toLowerCase();
151
+ if (BLOCKED_METADATA_HOSTS.has(lowered)) return "metadata";
152
+ if (lowered === "localhost" || lowered.endsWith(".localhost")) return "loopback";
153
+ if (lowered.includes(":")) return classifyIpv6Host(lowered);
154
+ const octets = ipv4Octets(lowered);
155
+ return octets ? classifyIpv4Octets(octets) : "public";
156
+ }
157
+
158
+ function isBlockedNetworkHost(host: string, allowPrivateNetwork: boolean): boolean {
159
+ const cls = classifyHost(host);
160
+ if (cls === "metadata") return true; // never toggleable
161
+ if (cls === "loopback" || cls === "public") return false;
162
+ return !allowPrivateNetwork; // private / link-local / reserved
163
+ }
164
+
165
+ /**
166
+ * Normalize a URL, allowing only http/https. Loopback (localhost/127.0.0.0/8/::1) is
167
+ * allowed by default for local development; private/link-local/reserved ranges are blocked
168
+ * unless allowPrivateNetwork is set; cloud metadata is always blocked.
169
+ */
170
+ export function assertHttpUrl(url: string, opts: HttpUrlOptions = {}): string {
59
171
  let parsed: URL;
60
172
  try {
61
173
  parsed = new URL(url);
@@ -65,5 +177,8 @@ export function assertHttpUrl(url: string): string {
65
177
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
66
178
  throw new Error(`Unsupported protocol "${parsed.protocol}//" — only http/https`);
67
179
  }
180
+ if (isBlockedNetworkHost(parsed.hostname, opts.allowPrivateNetwork === true)) {
181
+ throw new Error(`Blocked network host "${parsed.hostname}" — private, link-local, reserved, and cloud-metadata URLs are not allowed (set allowPrivateNetwork to opt in to private ranges; metadata is always blocked)`);
182
+ }
68
183
  return parsed.href;
69
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlines/opl-pi-sht",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "opl-pi-sht (shouldHaveThat) Pi coding-agent extension kit: token-efficient context, tool and session safety, fingerprinted /init, model benchmarking, web access, and browser automation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,7 +47,7 @@
47
47
  "test:opl-init": "bun test tests/opl-init-crawl.test.mjs tests/opl-init-fingerprint.test.mjs tests/opl-init-packet.test.mjs && OPL_EXTENSION=opl-init bun test tests/extension-smoke.test.mjs",
48
48
  "test:opl-input": "bun test tests/opl-input-style.test.mjs && OPL_EXTENSION=opl-input bun test tests/extension-smoke.test.mjs",
49
49
  "test:opl-modes": "bun test tests/opl-modes-helpers.test.mjs tests/opl-modes-lifecycle.test.mjs && OPL_EXTENSION=opl-modes bun test tests/extension-smoke.test.mjs",
50
- "test:opl-questionnaire": "OPL_EXTENSION=opl-questionnaire bun test tests/extension-smoke.test.mjs",
50
+ "test:opl-questionnaire": "bun test tests/opl-questionnaire-helpers.test.mjs && OPL_EXTENSION=opl-questionnaire bun test tests/extension-smoke.test.mjs",
51
51
  "test:opl-todo": "bun test tests/opl-todo-config.test.mjs tests/opl-todo-render.test.mjs && OPL_EXTENSION=opl-todo bun test tests/extension-smoke.test.mjs",
52
52
  "test:opl-webaccess": "bun test tests/opl-webaccess-helpers.test.mjs && OPL_EXTENSION=opl-webaccess bun test tests/extension-smoke.test.mjs",
53
53
  "test:opl-simplebench": "bun test tests/opl-simplebench.test.ts && OPL_EXTENSION=opl-simplebench bun test tests/extension-smoke.test.mjs",