@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
JavaScript
// 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