UNPKG

@open-condo/miniapp-utils

Version:

A set of helper functions / components / hooks used to build new condo apps fast

397 lines (388 loc) 12.8 kB
// src/helpers/iframe.ts import { z } from "zod"; // src/helpers/collections.ts function nonNull(val) { return val !== null && val !== void 0; } // src/helpers/environment.ts function isDebug() { return process.env.NODE_ENV === "development"; } // src/helpers/ip/ranges.ts var ranges_default = { /** localhost IP ranges */ localhost: { /** the localhost address ranges for IPv4 */ ipv4: ["127.0.0.0/8"], /** the localhost address ranges for IPv6 */ ipv6: ["::1/128"] }, /** private IP ranges */ private: { /** private address ranges for IPv4 */ ipv4: [ "10.0.0.0/8", // RFC 1918 "172.16.0.0/12", // RFC 1918 "192.168.0.0/16" // RFC 1918 ], /** private address ranges for IPv6 */ ipv6: [ "fe80::/10", // link-local address "fc00::/7" // unique local address (ULA) ] }, /** reserved IP ranges */ reserved: { /** reserved address ranges for IPv4 */ ipv4: [ "0.0.0.0/8", // broadcast "this" "100.64.0.0/10", // carrier-grade NAT "169.254.0.0/16", // DHCP fallback "192.0.0.0/24", // IANA Special Purpose Address Registry "192.0.2.0/24", // TEST-NET-1 for documentation examples "192.88.99.0/24", // deprecated 6to4 anycast relays "198.18.0.0/15", // for testing inter-network comms between two subnets "198.51.100.0/24", // TEST-NET-2 for documentation examples "203.0.113.0/24", // TEST-NET-3 for documentation examples "224.0.0.0/4", // multicast "240.0.0.0/4", // reserved unspecified "255.255.255.255/32" // limited broadcast address ], /** reserved address ranges for IPv6 */ ipv6: [ "::/128", // unspecified address "64:ff9b::/96", // IPv4/IPv6 translation "100::/64", // discard prefix "2001::/32", // Teredo tunneling "2001:10::/28", // deprecated "2001:20::/28", // ORCHIDv2 "2001:db8::/32", // for documentation and examples "2002::/16", // 6to4 "ff00::/8" // multicast ] } }; // src/helpers/ip/utils.ts var v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"; var v4Str = `(${v4Seg}[.]){3}${v4Seg}`; var IPv4Reg = new RegExp(`^${v4Str}$`); var v6Seg = "(?:[0-9a-fA-F]{1,4})"; var IPv6Reg = new RegExp( `^((?:${v6Seg}:){7}(?:${v6Seg}|:)|(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:)))(%[0-9a-zA-Z]{1,})?$` ); function isIPv4(s) { return IPv4Reg.test(s); } function isIPv6(s) { return IPv6Reg.test(s); } function isIP(s) { if (isIPv4(s)) return 4; if (isIPv6(s)) return 6; return 0; } // src/helpers/ip/ipv4.ts function ipv4ToLong(ip) { if (!isIPv4(ip)) { throw new Error(`not a valid IPv4 address: ${ip}`); } const octets = ip.split("."); return (parseInt(octets[0], 10) << 24) + (parseInt(octets[1], 10) << 16) + (parseInt(octets[2], 10) << 8) + parseInt(octets[3], 10) >>> 0; } function createLongChecker(subnet) { const [subnetAddress, prefixLengthString] = subnet.split("/"); const prefixLength = parseInt(prefixLengthString, 10); if (!subnetAddress || !Number.isInteger(prefixLength)) { throw new Error(`not a valid IPv4 subnet: ${subnet}`); } if (prefixLength < 0 || prefixLength > 32) { throw new Error(`not a valid IPv4 prefix length: ${prefixLength} (from ${subnet})`); } const subnetLong = ipv4ToLong(subnetAddress); return (addressLong) => { if (prefixLength === 0) { return true; } const subnetPrefix = subnetLong >> 32 - prefixLength; const addressPrefix = addressLong >> 32 - prefixLength; return subnetPrefix === addressPrefix; }; } function createChecker(subnetOrSubnets) { if (Array.isArray(subnetOrSubnets)) { const checks = subnetOrSubnets.map((subnet) => createLongChecker(subnet)); return (address) => { const addressLong = ipv4ToLong(address); return checks.some((check2) => check2(addressLong)); }; } const check = createLongChecker(subnetOrSubnets); return (address) => { const addressLong = ipv4ToLong(address); return check(addressLong); }; } var specialNetsCache = {}; function isLocalhost(address) { if (!("localhost" in specialNetsCache)) { specialNetsCache["localhost"] = createChecker(ranges_default.localhost.ipv4); } return specialNetsCache["localhost"](address); } function isSpecial(address) { if (!("special" in specialNetsCache)) { specialNetsCache["special"] = createChecker([ ...ranges_default.private.ipv4, ...ranges_default.localhost.ipv4, ...ranges_default.reserved.ipv4 ]); } return specialNetsCache["special"](address); } // src/helpers/ip/ipv6.ts var dot = /\./; var mappedIpv4 = /^(.+:ffff:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:%.+)?$/; var colon = /:/; var doubleColon = /::/; function mappedIpv4ToIpv6(ip) { const matches = ip.match(mappedIpv4); if (!matches || !isIPv4(matches[2])) { throw new Error(`not a mapped IPv4 address: ${ip}`); } const prefix = matches[1]; const ipv4 = matches[2]; const parts = ipv4.split(dot).map((x) => parseInt(x, 10)); const segment7 = ((parts[0] << 8) + parts[1]).toString(16); const segment8 = ((parts[2] << 8) + parts[3]).toString(16); return `${prefix}${segment7}:${segment8}`; } function extractMappedIpv4(ip) { const matches = ip.match(mappedIpv4); if (!matches || !isIPv4(matches[2])) { throw new Error(`not a mapped IPv4 address: ${ip}`); } return matches[2]; } function getIpv6Segments(ip) { if (!isIPv6(ip)) { throw new Error(`not a valid IPv6 address: ${ip}`); } if (dot.test(ip)) { return getIpv6Segments(mappedIpv4ToIpv6(ip)); } const [beforeChunk, afterChunk] = ip.split(doubleColon); const beforeParts = beforeChunk && beforeChunk.split(colon) || []; const afterParts = afterChunk && afterChunk.split(colon) || []; const missingSegments = new Array(8 - (beforeParts.length + afterParts.length)); return beforeParts.concat(missingSegments, afterParts); } function createChecker2(subnetOrSubnets) { if (Array.isArray(subnetOrSubnets)) { const checks = subnetOrSubnets.map((subnet) => createSegmentChecker(subnet)); return (address) => { const segments = getIpv6Segments(address); return checks.some((check2) => check2(segments)); }; } const check = createSegmentChecker(subnetOrSubnets); return (address) => { const segments = getIpv6Segments(address); return check(segments); }; } function createSegmentChecker(subnet) { const [subnetAddress, prefixLengthString] = subnet.split("/"); const prefixLength = parseInt(prefixLengthString, 10); if (!subnetAddress || !Number.isInteger(prefixLength)) { throw new Error(`not a valid IPv6 CIDR subnet: ${subnet}`); } if (prefixLength < 0 || prefixLength > 128) { throw new Error(`not a valid IPv6 prefix length: ${prefixLength} (from ${subnet})`); } const subnetSegments = getIpv6Segments(subnetAddress); return (addressSegments) => { for (let i = 0; i < 8; ++i) { const bitCount = Math.min(prefixLength - i * 16, 16); if (bitCount <= 0) { break; } const subnetPrefix = (subnetSegments[i] && parseInt(subnetSegments[i], 16) || 0) >> 16 - bitCount; const addressPrefix = (addressSegments[i] && parseInt(addressSegments[i], 16) || 0) >> 16 - bitCount; if (subnetPrefix !== addressPrefix) { return false; } } return true; }; } var specialNetsCache2 = {}; function isLocalhost2(address) { if (!("localhost" in specialNetsCache2)) { specialNetsCache2["localhost"] = createChecker2(ranges_default.localhost.ipv6); } return specialNetsCache2["localhost"](address); } function isIPv4MappedAddress(address) { if (!("mapped" in specialNetsCache2)) { specialNetsCache2["mapped"] = createChecker2("::ffff:0:0/96"); } if (specialNetsCache2["mapped"](address)) { const matches = address.match(mappedIpv4); return Boolean(matches && isIPv4(matches[2])); } return false; } function isSpecial2(address) { if (!("special" in specialNetsCache2)) { specialNetsCache2["special"] = createChecker2([ ...ranges_default.private.ipv6, ...ranges_default.localhost.ipv6, ...ranges_default.reserved.ipv6 ]); } return specialNetsCache2["special"](address); } // src/helpers/ip/index.ts function isLocalhost3(address) { if (isIPv6(address)) { if (isIPv4MappedAddress(address)) { return isLocalhost(extractMappedIpv4(address)); } return isLocalhost2(address); } else { return isLocalhost(address); } } function isSpecial3(address) { if (isIPv6(address)) { if (isIPv4MappedAddress(address)) { return isSpecial(extractMappedIpv4(address)); } return isSpecial2(address); } else { return isSpecial(address); } } // src/helpers/urls.ts var REGEXP_ESCAPE_CHARS = /[\\^$.*+?()[\]{}|]/g; var WILDCARD_REGEXP_PART = "([a-zA-Z0-9-]{1,63})"; var WILDCARD_REGEXP_PART_ESCAPED = _escapeRegexp(WILDCARD_REGEXP_PART); function _escapeRegexp(source) { return source.replace(REGEXP_ESCAPE_CHARS, "\\$&"); } function getUrlMeta(url) { try { const u = new URL(url); const hostnameUnWrapped = u.hostname.startsWith("[") && u.hostname.endsWith("]") ? u.hostname.slice(1, -1) : u.hostname; u.isIP = isIP(hostnameUnWrapped) > 0; u.isSpecialIP = u.isIP && isSpecial3(hostnameUnWrapped); u.isLocalhost = u.hostname === "localhost" || u.isIP && isLocalhost3(hostnameUnWrapped); return u; } catch { return null; } } // src/helpers/iframe.ts var mappingRule = z.object({ from: z.string(), to: z.string() }); var domainsMapping = z.array(mappingRule); var IFRAME_PERMISSIONS_SCHEMA = z.object({ isFullscreenAllowed: z.boolean().nullish().transform((v) => v ?? false), isMicrophoneAllowed: z.boolean().nullish().transform((v) => v ?? false), isCameraAllowed: z.boolean().nullish().transform((v) => v ?? false), isSpeakerSelectionAllowed: z.boolean().nullish().transform((v) => v ?? false), isClipboardWriteAllowed: z.boolean().nullish().transform((v) => v ?? false) }).default({ isFullscreenAllowed: false, isMicrophoneAllowed: false, isCameraAllowed: false, isSpeakerSelectionAllowed: false, isClipboardWriteAllowed: false }); var IFRAME_METADATA_SCHEMA = z.object({ domainsMapping, permissions: IFRAME_PERMISSIONS_SCHEMA }); function buildAllowString(permissions) { return [ permissions.isFullscreenAllowed ? "fullscreen" : null, permissions.isMicrophoneAllowed ? "microphone" : null, permissions.isCameraAllowed ? "camera" : null, permissions.isSpeakerSelectionAllowed ? "speaker-selection" : null, permissions.isClipboardWriteAllowed ? "clipboard-write" : null ].filter(nonNull).join("; "); } function getAppEntrypoint({ appUrl, domainsMapping: domainsMapping2 }) { let isLocalhost4 = false; if (typeof window !== "undefined") { const hostAppMeta = getUrlMeta(window.location.href); isLocalhost4 = (hostAppMeta == null ? void 0 : hostAppMeta.isLocalhost) ?? false; } if (isDebug() || isLocalhost4) return appUrl; const originalUrl = new URL(appUrl); const fromDomain = originalUrl.origin; const mappingRule2 = domainsMapping2.find((rule) => rule.from === fromDomain); if (!mappingRule2) return appUrl; const toUrl = new URL(mappingRule2.to); toUrl.pathname = originalUrl.pathname; toUrl.search = originalUrl.search; toUrl.hash = originalUrl.hash; return toUrl.toString(); } function extractMiniappMetadata(app) { var _a; if (!app || !app.appUrl) return null; const domainsMapping2 = ((_a = app.domains) == null ? void 0 : _a.mapping) ?? []; const permissions = IFRAME_PERMISSIONS_SCHEMA.parse({ isFullscreenAllowed: app.isFullscreenAllowed, isMicrophoneAllowed: app.isMicrophoneAllowed, isCameraAllowed: app.isCameraAllowed, isSpeakerSelectionAllowed: app.isSpeakerSelectionAllowed }); return { id: app.id, name: app.name ?? null, entrypoint: getAppEntrypoint({ appUrl: app.appUrl, domainsMapping: domainsMapping2 }), domainsMapping: domainsMapping2, permissions }; } export { IFRAME_METADATA_SCHEMA, IFRAME_PERMISSIONS_SCHEMA, buildAllowString, extractMiniappMetadata, getAppEntrypoint }; //# sourceMappingURL=iframe.mjs.map