@medialane/sdk 0.105.0 → 0.107.0
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.cjs +239 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +236 -28
- package/dist/index.js.map +1 -1
- package/dist/starknet/index.cjs +20 -0
- package/dist/starknet/index.cjs.map +1 -1
- package/dist/starknet/index.d.cts +8 -1
- package/dist/starknet/index.d.ts +8 -1
- package/dist/starknet/index.js +19 -1
- package/dist/starknet/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1377,8 +1377,19 @@ function createRateLimiter(windowMs, max) {
|
|
|
1377
1377
|
return true;
|
|
1378
1378
|
};
|
|
1379
1379
|
}
|
|
1380
|
+
var TRUSTED_APP_IP_HEADER = "x-medialane-client-ip";
|
|
1380
1381
|
function requestIp(req) {
|
|
1381
|
-
|
|
1382
|
+
const fromApp = req.headers.get(TRUSTED_APP_IP_HEADER)?.trim();
|
|
1383
|
+
if (fromApp) return fromApp;
|
|
1384
|
+
const fromEdge = req.headers.get("x-vercel-forwarded-for")?.split(",")[0]?.trim();
|
|
1385
|
+
if (fromEdge) return fromEdge;
|
|
1386
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
1387
|
+
if (forwarded) {
|
|
1388
|
+
const hops = forwarded.split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
1389
|
+
const nearest = hops[hops.length - 1];
|
|
1390
|
+
if (nearest) return nearest;
|
|
1391
|
+
}
|
|
1392
|
+
return "unknown";
|
|
1382
1393
|
}
|
|
1383
1394
|
|
|
1384
1395
|
// src/server/origin.ts
|
|
@@ -1467,43 +1478,135 @@ var ALLOWED_IMAGE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
1467
1478
|
]);
|
|
1468
1479
|
var MAX_IMAGE_REDIRECTS = 5;
|
|
1469
1480
|
var MAX_IMAGE_PROXY_BYTES = 15 * 1024 * 1024;
|
|
1481
|
+
function parseIpv4(ip) {
|
|
1482
|
+
const parts = ip.split(".");
|
|
1483
|
+
if (parts.length !== 4) return null;
|
|
1484
|
+
const bytes = [];
|
|
1485
|
+
for (const part of parts) {
|
|
1486
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
1487
|
+
const n = Number(part);
|
|
1488
|
+
if (n > 255) return null;
|
|
1489
|
+
bytes.push(n);
|
|
1490
|
+
}
|
|
1491
|
+
return bytes;
|
|
1492
|
+
}
|
|
1493
|
+
function isPrivateIpv4(bytes) {
|
|
1494
|
+
const a = bytes[0];
|
|
1495
|
+
const b = bytes[1];
|
|
1496
|
+
const c = bytes[2];
|
|
1497
|
+
if (a === 0) return true;
|
|
1498
|
+
if (a === 10) return true;
|
|
1499
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
1500
|
+
if (a === 127) return true;
|
|
1501
|
+
if (a === 169 && b === 254) return true;
|
|
1502
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1503
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
1504
|
+
if (a === 192 && b === 168) return true;
|
|
1505
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
1506
|
+
if (a >= 224) return true;
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
function expandIpv6(rawIp) {
|
|
1510
|
+
let ip = rawIp;
|
|
1511
|
+
let embeddedV4 = null;
|
|
1512
|
+
const v4Tail = ip.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1513
|
+
if (v4Tail && ip.includes(":")) {
|
|
1514
|
+
embeddedV4 = parseIpv4(v4Tail[1]);
|
|
1515
|
+
if (!embeddedV4) return null;
|
|
1516
|
+
ip = ip.slice(0, ip.length - v4Tail[1].length) + "0:0";
|
|
1517
|
+
}
|
|
1518
|
+
const sides = ip.split("::");
|
|
1519
|
+
if (sides.length > 2) return null;
|
|
1520
|
+
const head = sides[0] ? sides[0].split(":").filter(Boolean) : [];
|
|
1521
|
+
const tail = sides.length === 2 && sides[1] ? sides[1].split(":").filter(Boolean) : [];
|
|
1522
|
+
let groups;
|
|
1523
|
+
if (sides.length === 1) {
|
|
1524
|
+
groups = head;
|
|
1525
|
+
if (groups.length !== 8) return null;
|
|
1526
|
+
} else {
|
|
1527
|
+
const missing = 8 - head.length - tail.length;
|
|
1528
|
+
if (missing < 0) return null;
|
|
1529
|
+
groups = [...head, ...Array(missing).fill("0"), ...tail];
|
|
1530
|
+
}
|
|
1531
|
+
if (groups.length !== 8) return null;
|
|
1532
|
+
const bytes = [];
|
|
1533
|
+
for (const group of groups) {
|
|
1534
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return null;
|
|
1535
|
+
const n = parseInt(group, 16);
|
|
1536
|
+
bytes.push(n >> 8 & 255, n & 255);
|
|
1537
|
+
}
|
|
1538
|
+
if (embeddedV4) {
|
|
1539
|
+
bytes[12] = embeddedV4[0];
|
|
1540
|
+
bytes[13] = embeddedV4[1];
|
|
1541
|
+
bytes[14] = embeddedV4[2];
|
|
1542
|
+
bytes[15] = embeddedV4[3];
|
|
1543
|
+
}
|
|
1544
|
+
return bytes;
|
|
1545
|
+
}
|
|
1546
|
+
function isPrivateIpv6(bytes) {
|
|
1547
|
+
if (bytes.every((b) => b === 0)) return true;
|
|
1548
|
+
if (bytes.slice(0, 15).every((b) => b === 0) && bytes[15] === 1) return true;
|
|
1549
|
+
if ((bytes[0] & 254) === 252) return true;
|
|
1550
|
+
if (bytes[0] === 254 && (bytes[1] & 192) === 128) return true;
|
|
1551
|
+
if (bytes.slice(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255) {
|
|
1552
|
+
return isPrivateIpv4(bytes.slice(12));
|
|
1553
|
+
}
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
function parseNumber(part) {
|
|
1557
|
+
if (/^0x[0-9a-f]+$/i.test(part)) return parseInt(part.slice(2), 16);
|
|
1558
|
+
if (/^0[0-7]+$/.test(part)) return parseInt(part, 8);
|
|
1559
|
+
if (/^\d+$/.test(part)) return parseInt(part, 10);
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
function normalizeNumericHostname(host) {
|
|
1563
|
+
const parts = host.trim().split(".");
|
|
1564
|
+
if (parts.length < 1 || parts.length > 4) return null;
|
|
1565
|
+
const values = [];
|
|
1566
|
+
for (const part of parts) {
|
|
1567
|
+
const value = parseNumber(part);
|
|
1568
|
+
if (value === null || value < 0) return null;
|
|
1569
|
+
values.push(value);
|
|
1570
|
+
}
|
|
1571
|
+
const leading = values.slice(0, -1);
|
|
1572
|
+
if (leading.some((v) => v > 255)) return null;
|
|
1573
|
+
const tail = values[values.length - 1];
|
|
1574
|
+
const tailBytes = 4 - leading.length;
|
|
1575
|
+
if (tail > 2 ** (8 * tailBytes) - 1) return null;
|
|
1576
|
+
const bytes = [...leading];
|
|
1577
|
+
for (let i = tailBytes - 1; i >= 0; i--) bytes.push(tail >>> 8 * i & 255);
|
|
1578
|
+
return bytes.join(".");
|
|
1579
|
+
}
|
|
1580
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
1581
|
+
"localhost",
|
|
1582
|
+
"metadata.google.internal",
|
|
1583
|
+
"metadata.azure.internal"
|
|
1584
|
+
]);
|
|
1470
1585
|
function isPrivateHost(hostname) {
|
|
1471
|
-
const
|
|
1472
|
-
if (
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (n === 2130706433 || n === 0 || n >= 2886729728 && n <= 2887778303 || n >= 3232235520 && n <= 3232301055 || n >= 167772160 && n <= 184549375 || n >= 2851995648 && n <= 2852061183) return true;
|
|
1480
|
-
}
|
|
1481
|
-
if (/^0\d+\.\d+\.\d+\.\d+$/.test(h)) return true;
|
|
1482
|
-
if (h === "::1" || /^0*:0*:0*:0*:0*:0*:0*:0*1$/.test(h)) return true;
|
|
1483
|
-
const v4mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
1484
|
-
if (v4mapped) return isPrivateHost(v4mapped[1]);
|
|
1485
|
-
if (/^10\./.test(h)) return true;
|
|
1486
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
1487
|
-
if (/^192\.168\./.test(h)) return true;
|
|
1488
|
-
if (/^169\.254\./.test(h)) return true;
|
|
1489
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(h)) return true;
|
|
1490
|
-
if (/^fe80:/i.test(h)) return true;
|
|
1491
|
-
if (/^f[cd][0-9a-f]{2}:/i.test(h)) return true;
|
|
1492
|
-
if (h.endsWith(".local")) return true;
|
|
1493
|
-
if (h === "metadata.google.internal") return true;
|
|
1494
|
-
if (h === "metadata.azure.internal") return true;
|
|
1586
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1587
|
+
if (BLOCKED_HOSTNAMES.has(host)) return true;
|
|
1588
|
+
if (host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
1589
|
+
const candidate = normalizeNumericHostname(host) ?? host;
|
|
1590
|
+
const v4 = parseIpv4(candidate);
|
|
1591
|
+
if (v4) return isPrivateIpv4(v4);
|
|
1592
|
+
const v6 = expandIpv6(candidate);
|
|
1593
|
+
if (v6) return isPrivateIpv6(v6);
|
|
1495
1594
|
return false;
|
|
1496
1595
|
}
|
|
1497
|
-
function validateUrl(raw) {
|
|
1596
|
+
function validateUrl(raw, options = {}) {
|
|
1597
|
+
const requireHttps = options.requireHttps ?? true;
|
|
1498
1598
|
let parsed;
|
|
1499
1599
|
try {
|
|
1500
1600
|
parsed = new URL(raw);
|
|
1501
1601
|
} catch {
|
|
1502
1602
|
return { error: "Invalid url", status: 400 };
|
|
1503
1603
|
}
|
|
1504
|
-
if (parsed.protocol !== "https:") {
|
|
1604
|
+
if (requireHttps && parsed.protocol !== "https:") {
|
|
1505
1605
|
return { error: "Only https URLs allowed", status: 400 };
|
|
1506
1606
|
}
|
|
1607
|
+
if (!requireHttps && parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1608
|
+
return { error: "Only http and https URLs allowed", status: 400 };
|
|
1609
|
+
}
|
|
1507
1610
|
if (parsed.username || parsed.password) {
|
|
1508
1611
|
return { error: "URL credentials not allowed", status: 400 };
|
|
1509
1612
|
}
|
|
@@ -1512,6 +1615,111 @@ function validateUrl(raw) {
|
|
|
1512
1615
|
}
|
|
1513
1616
|
return { url: parsed };
|
|
1514
1617
|
}
|
|
1618
|
+
function isPrivateOrInsecureUrl(raw, requireHttps = true) {
|
|
1619
|
+
return "error" in validateUrl(raw, { requireHttps });
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/server/image-proxy.ts
|
|
1623
|
+
var DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; Medialane/1.0; +https://www.medialane.io)";
|
|
1624
|
+
function jsonError(message, status) {
|
|
1625
|
+
return Response.json({ error: message }, { status });
|
|
1626
|
+
}
|
|
1627
|
+
async function readBodyWithCap(res, maxBytes) {
|
|
1628
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
1629
|
+
if (declared > maxBytes) {
|
|
1630
|
+
return { ok: false, error: "Image too large", status: 413 };
|
|
1631
|
+
}
|
|
1632
|
+
const reader = res.body?.getReader();
|
|
1633
|
+
if (!reader) return { ok: false, error: "Empty response body", status: 502 };
|
|
1634
|
+
const chunks = [];
|
|
1635
|
+
let total = 0;
|
|
1636
|
+
for (; ; ) {
|
|
1637
|
+
const { done, value } = await reader.read();
|
|
1638
|
+
if (done) break;
|
|
1639
|
+
if (!value) continue;
|
|
1640
|
+
total += value.byteLength;
|
|
1641
|
+
if (total > maxBytes) {
|
|
1642
|
+
await reader.cancel().catch(() => {
|
|
1643
|
+
});
|
|
1644
|
+
return { ok: false, error: "Image too large", status: 413 };
|
|
1645
|
+
}
|
|
1646
|
+
chunks.push(value);
|
|
1647
|
+
}
|
|
1648
|
+
const body = new Uint8Array(total);
|
|
1649
|
+
let offset = 0;
|
|
1650
|
+
for (const chunk of chunks) {
|
|
1651
|
+
body.set(chunk, offset);
|
|
1652
|
+
offset += chunk.byteLength;
|
|
1653
|
+
}
|
|
1654
|
+
return { ok: true, body };
|
|
1655
|
+
}
|
|
1656
|
+
function createImageProxyHandler(config) {
|
|
1657
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
1658
|
+
const userAgent = config.userAgent ?? DEFAULT_USER_AGENT;
|
|
1659
|
+
async function resolvesToPrivateHost(hostname) {
|
|
1660
|
+
try {
|
|
1661
|
+
const addresses = await config.resolveHostname(hostname);
|
|
1662
|
+
if (addresses.length === 0) return true;
|
|
1663
|
+
return addresses.some((address) => isPrivateHost(address));
|
|
1664
|
+
} catch {
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function safeFetch(url, hopsLeft) {
|
|
1669
|
+
if (hopsLeft < 0) throw new Error("Too many redirects");
|
|
1670
|
+
if (await resolvesToPrivateHost(url.hostname)) {
|
|
1671
|
+
throw new Error("Blocked: hostname resolves to a private address");
|
|
1672
|
+
}
|
|
1673
|
+
const res = await doFetch(url.toString(), {
|
|
1674
|
+
redirect: "manual",
|
|
1675
|
+
headers: { "User-Agent": userAgent }
|
|
1676
|
+
});
|
|
1677
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1678
|
+
const location = res.headers.get("location");
|
|
1679
|
+
if (!location) throw new Error("Redirect with no Location header");
|
|
1680
|
+
const next = new URL(location, url);
|
|
1681
|
+
const validated = validateUrl(next.toString());
|
|
1682
|
+
if ("error" in validated) throw new Error(`Redirect blocked: ${validated.error}`);
|
|
1683
|
+
return safeFetch(validated.url, hopsLeft - 1);
|
|
1684
|
+
}
|
|
1685
|
+
return res;
|
|
1686
|
+
}
|
|
1687
|
+
return async function handleImageProxy(req) {
|
|
1688
|
+
if (!config.checkRateLimit(requestIp(req))) {
|
|
1689
|
+
return jsonError("Too many requests", 429);
|
|
1690
|
+
}
|
|
1691
|
+
const raw = new URL(req.url).searchParams.get("url");
|
|
1692
|
+
if (!raw) return jsonError("Missing url", 400);
|
|
1693
|
+
const validated = validateUrl(raw);
|
|
1694
|
+
if ("error" in validated) return jsonError(validated.error, validated.status);
|
|
1695
|
+
let upstream;
|
|
1696
|
+
try {
|
|
1697
|
+
upstream = await safeFetch(validated.url, MAX_IMAGE_REDIRECTS);
|
|
1698
|
+
} catch {
|
|
1699
|
+
return jsonError("Failed to fetch image", 502);
|
|
1700
|
+
}
|
|
1701
|
+
if (!upstream.ok) {
|
|
1702
|
+
return jsonError(`Upstream returned ${upstream.status}`, upstream.status);
|
|
1703
|
+
}
|
|
1704
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
1705
|
+
const baseType = contentType.split(";")[0].trim().toLowerCase();
|
|
1706
|
+
if (!ALLOWED_IMAGE_CONTENT_TYPES.has(baseType)) {
|
|
1707
|
+
return jsonError("Not an image", 400);
|
|
1708
|
+
}
|
|
1709
|
+
const capped = await readBodyWithCap(upstream, MAX_IMAGE_PROXY_BYTES);
|
|
1710
|
+
if (!capped.ok) return jsonError(capped.error, capped.status);
|
|
1711
|
+
return new Response(capped.body, {
|
|
1712
|
+
status: 200,
|
|
1713
|
+
headers: {
|
|
1714
|
+
"Content-Type": contentType,
|
|
1715
|
+
"X-Content-Type-Options": "nosniff",
|
|
1716
|
+
"Content-Security-Policy": "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox",
|
|
1717
|
+
"Cache-Control": "public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800",
|
|
1718
|
+
"Access-Control-Allow-Origin": "*"
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1515
1723
|
|
|
1516
1724
|
// src/server/backend-metadata.ts
|
|
1517
1725
|
function endpoint(config, path) {
|
|
@@ -1784,6 +1992,7 @@ exports.STARKNET_POP_COLLECTION_CLASS_HASH = STARKNET_POP_COLLECTION_CLASS_HASH;
|
|
|
1784
1992
|
exports.STARKNET_POP_FACTORY_CONTRACT = STARKNET_POP_FACTORY_CONTRACT;
|
|
1785
1993
|
exports.SUPPORTED_TOKENS = SUPPORTED_TOKENS;
|
|
1786
1994
|
exports.SUPPORTED_URL_CHAINS = SUPPORTED_URL_CHAINS;
|
|
1995
|
+
exports.TRUSTED_APP_IP_HEADER = TRUSTED_APP_IP_HEADER;
|
|
1787
1996
|
exports.assetHref = assetHref;
|
|
1788
1997
|
exports.buildAssetMetadata = buildAssetMetadata;
|
|
1789
1998
|
exports.chainFromSlug = chainFromSlug;
|
|
@@ -1792,6 +2001,7 @@ exports.coinHref = coinHref;
|
|
|
1792
2001
|
exports.collectionHref = collectionHref;
|
|
1793
2002
|
exports.createBackendProxyHandler = createBackendProxyHandler;
|
|
1794
2003
|
exports.createFailoverFetch = createFailoverFetch;
|
|
2004
|
+
exports.createImageProxyHandler = createImageProxyHandler;
|
|
1795
2005
|
exports.createRateLimiter = createRateLimiter;
|
|
1796
2006
|
exports.createRpcProxyHandler = createRpcProxyHandler;
|
|
1797
2007
|
exports.encodeU256 = encodeU256;
|
|
@@ -1808,6 +2018,7 @@ exports.getTokenBySymbol = getTokenBySymbol;
|
|
|
1808
2018
|
exports.hasCapability = hasCapability;
|
|
1809
2019
|
exports.isPolicyRefusal = isPolicyRefusal;
|
|
1810
2020
|
exports.isPrivateHost = isPrivateHost;
|
|
2021
|
+
exports.isPrivateOrInsecureUrl = isPrivateOrInsecureUrl;
|
|
1811
2022
|
exports.isSameOrigin = isSameOrigin;
|
|
1812
2023
|
exports.isServiceId = isServiceId;
|
|
1813
2024
|
exports.isTransientRpcError = isTransientRpcError;
|
|
@@ -1818,6 +2029,7 @@ exports.listServices = listServices;
|
|
|
1818
2029
|
exports.normalizeAddress = normalizeAddress;
|
|
1819
2030
|
exports.normalizeHash = normalizeHash;
|
|
1820
2031
|
exports.parseAmount = parseAmount;
|
|
2032
|
+
exports.readBodyWithCap = readBodyWithCap;
|
|
1821
2033
|
exports.requestIp = requestIp;
|
|
1822
2034
|
exports.resolveAppFeeConfig = resolveAppFeeConfig;
|
|
1823
2035
|
exports.resolveConfig = resolveConfig;
|