@okostream/mcp 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-07-10
4
+
5
+ - Replaced hand-maintained remote-image IP ranges with a tested public-unicast policy covering IPv4, IPv6, mapped, reserved, documentation, benchmark, multicast, and private address classes.
6
+
3
7
  ## 0.5.0 - 2026-07-10
4
8
 
5
9
  - Isolated every API key behind a synthetic Firebase identity with immediate revocation across Firestore, main RTDB, and chat RTDB.
@@ -0,0 +1,18 @@
1
+ import ipaddr from "ipaddr.js";
2
+ export function normalizeIpLiteralHostname(hostname) {
3
+ return hostname.startsWith("[") && hostname.endsWith("]")
4
+ ? hostname.slice(1, -1)
5
+ : hostname;
6
+ }
7
+ export function ipLiteralFamily(address) {
8
+ const normalized = normalizeIpLiteralHostname(address);
9
+ if (!ipaddr.isValid(normalized))
10
+ return 0;
11
+ return ipaddr.parse(normalized).kind() === "ipv4" ? 4 : 6;
12
+ }
13
+ export function isPublicUnicastAddress(address) {
14
+ const normalized = normalizeIpLiteralHostname(address);
15
+ if (!ipaddr.isValid(normalized))
16
+ return false;
17
+ return ipaddr.parse(normalized).range() === "unicast";
18
+ }
package/dist/server.js CHANGED
@@ -23,7 +23,7 @@ if (process.argv.includes("--help")) {
23
23
  }
24
24
  const server = new McpServer({
25
25
  name: "oko-mcp-server",
26
- version: "0.5.0",
26
+ version: "0.5.1",
27
27
  }, {
28
28
  instructions: OKO_MCP_INSTRUCTIONS,
29
29
  });
package/dist/tools.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { stat, readFile, realpath } from "node:fs/promises";
3
- import { isIP } from "node:net";
4
3
  import { homedir } from "node:os";
5
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
6
5
  import { randomUUID } from "node:crypto";
@@ -12,6 +11,7 @@ import { get, endBefore, limitToLast, orderByKey, push, query as dbQuery, ref, r
12
11
  import { httpsCallable } from "firebase/functions";
13
12
  import { ensureAuth, currentScopes } from "./auth.js";
14
13
  import { env } from "./env.js";
14
+ import { ipLiteralFamily, isPublicUnicastAddress, normalizeIpLiteralHostname, } from "./networkPolicy.js";
15
15
  import { chatRtdb, firestore, functions, rtdb } from "./firebase.js";
16
16
  import { csvCell, requireConfirm, safeString, serialize, textResult } from "./utils.js";
17
17
  import { detectAndNormalize, extractIframeSrc } from "./videoUrl.js";
@@ -372,33 +372,6 @@ function isBlockedHostname(hostname) {
372
372
  const host = hostname.toLowerCase();
373
373
  return host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local");
374
374
  }
375
- function isBlockedIp(address) {
376
- const normalized = address.toLowerCase().startsWith("::ffff:")
377
- ? address.slice(7)
378
- : address;
379
- const version = isIP(normalized);
380
- if (version === 4) {
381
- const [a = -1, b = -1] = normalized.split(".").map((part) => Number(part));
382
- return a === 0
383
- || a === 10
384
- || a === 127
385
- || (a === 100 && b >= 64 && b <= 127)
386
- || (a === 169 && b === 254)
387
- || (a === 172 && b >= 16 && b <= 31)
388
- || (a === 192 && b === 168)
389
- || (a === 198 && (b === 18 || b === 19))
390
- || a >= 224;
391
- }
392
- if (version === 6) {
393
- return normalized === "::"
394
- || normalized === "::1"
395
- || normalized.startsWith("fc")
396
- || normalized.startsWith("fd")
397
- || normalized.startsWith("fe80:")
398
- || normalized.startsWith("ff");
399
- }
400
- return false;
401
- }
402
375
  async function resolveRemoteImageAddress(url) {
403
376
  if (url.protocol !== "https:") {
404
377
  throw new Error("Image URL must use https://.");
@@ -409,15 +382,17 @@ async function resolveRemoteImageAddress(url) {
409
382
  if (isBlockedHostname(url.hostname)) {
410
383
  throw new Error("Image URL host is not allowed. Use a public HTTPS image URL.");
411
384
  }
412
- if (isIP(url.hostname)) {
413
- if (isBlockedIp(url.hostname)) {
385
+ const literalAddress = normalizeIpLiteralHostname(url.hostname);
386
+ const literalFamily = ipLiteralFamily(literalAddress);
387
+ if (literalFamily !== 0) {
388
+ if (!isPublicUnicastAddress(literalAddress)) {
414
389
  throw new Error("Image URL points to a private/local IP address, which is not allowed.");
415
390
  }
416
- return { address: url.hostname, family: isIP(url.hostname) };
391
+ return { address: literalAddress, family: literalFamily };
417
392
  }
418
393
  const addresses = await lookup(url.hostname, { all: true });
419
- if (addresses.length === 0 || addresses.some((item) => isBlockedIp(item.address))) {
420
- throw new Error("Image URL resolves to a private/local IP address, which is not allowed.");
394
+ if (addresses.length === 0 || addresses.some((item) => !isPublicUnicastAddress(item.address))) {
395
+ throw new Error("Image URL resolves to a non-public IP address, which is not allowed.");
421
396
  }
422
397
  const selected = addresses[0];
423
398
  if (!selected || (selected.family !== 4 && selected.family !== 6)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "MCP server for managing OKO Stream webinars as an organizer.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -36,6 +36,7 @@
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "dotenv": "^17.2.3",
38
38
  "firebase": "^12.16.0",
39
+ "ipaddr.js": "2.4.0",
39
40
  "sharp": "^0.35.3",
40
41
  "undici": "7.28.0",
41
42
  "zod": "^3.25.76"