@open-condo/miniapp-utils
Version:
A set of helper functions / components / hooks used to build new condo apps fast
1 lines • 89.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/helpers/analytics/instance.ts","../src/helpers/analytics/middlewares/grouping.ts","../src/helpers/analytics/middlewares/identity.ts","../src/helpers/apollo.ts","../src/helpers/proxying/utils.ts","../src/helpers/ip/utils.ts","../src/helpers/ip/ipv4.ts","../src/helpers/ip/ipv6.ts","../src/helpers/ip/index.ts","../src/helpers/proxying/proxy.ts","../src/helpers/sender.ts","../src/helpers/embeddingContext.tsx","../src/helpers/uuid.ts","../src/helpers/tracing.ts","../src/helpers/collections.ts","../src/helpers/cookies.ts","../src/helpers/environment.ts","../src/helpers/posthog.ts","../src/hooks/useSetPageActionsHandlers.ts","../src/hooks/useEffectOnce.ts","../src/hooks/useIntersectionObserver.ts","../src/hooks/usePrevious.ts"],"sourcesContent":["import { Analytics as DefaultAnalytics } from 'analytics'\n\nimport { GroupingMiddlewarePlugin, IdentityMiddlewarePlugin } from './middlewares'\n\nimport type { AnyPayload, AnalyticsConfig, AnalyticsInstanceWithGroups, PageData } from './types'\n\n/**\n * Type-safe wrapper on top of \"[analytics](https://www.npmjs.com/package/analytics)\" npm package\n * with support of group method (Posthog inspired) to group users into cohorts\n *\n * @example Init analytics in your app with type-guards\n * import { Analytics } from '@open-condo/miniapp-utils/helpers/analytics'\n * import { isDebug } from '@open-condo/miniapp-utils/helpers/environment'\n *\n * type MyAppEvents = {\n * 'order_create': { orderId: string, itemIds: Array<string> }\n * 'user_register': { userId: string, utmSource?: string }\n * }\n *\n * type MyUserData = {\n * 'name'?: string\n * 'age'?: number\n * }\n *\n * type MyAppGroups = 'organization' | 'country'\n *\n * export const analytics = new Analytics<MyAppEvents, MyUserData, MyAppGroups>({\n * app: appName,\n * version: revision,\n * debug: isDebug(),\n * })\n *\n * @example Use initialized analytics in your-app\n * import { Router } from 'next/router'\n * import { useEffect } from 'react'\n *\n * import { isValidCondoUIMessage } from '@open-condo/ui/events'\n *\n * import { analytics } from '@/domains/common/utils/analytics'\n * import { useAuth } from '@/domains/user/utils/auth'\n *\n * import type { FC } from 'react'\n *\n * export const ResidentAppEventsHandler: FC = () => {\n * const user = useAuth()\n * const { activeResident } = useActiveResident()\n *\n * // User change tracking\n * useEffect(() => {\n * if (user) {\n * analytics.identify(user.id, { name: user.name, type: user.type })\n * }\n * }, [user])\n *\n * // Page views tracking\n * useEffect(() => {\n * const handleRouteChange = () => analytics.pageView()\n * Router.events.on('routeChangeComplete', handleRouteChange)\n *\n * return () => {\n * Router.events.off('routeChangeComplete', handleRouteChange)\n * }\n * }, [])\n *\n * // Condo UI events tracking\n * useEffect(() => {\n * if (typeof window !== 'undefined') {\n * const handleMessage = async (e: MessageEvent) => {\n * if (isValidCondoUIMessage(e)) {\n * const { params: { event, ...eventData } } = e.data\n * await analytics.trackUntyped(event, eventData)\n * }\n *\n * }\n *\n * window.addEventListener('message', handleMessage)\n *\n * return () => {\n * window.removeEventListener('message', handleMessage)\n * }\n * }\n * }, [])\n *\n * return null\n * }\n */\nexport class Analytics<\n Events extends Record<string, AnyPayload> = Record<string, never>,\n UserData extends AnyPayload = Record<string, never>,\n GroupNames extends string = never,\n> {\n private readonly _analytics: AnalyticsInstanceWithGroups<GroupNames>\n private readonly _groups = new Set<GroupNames>()\n\n constructor (config: AnalyticsConfig) {\n this._analytics = DefaultAnalytics({\n ...config,\n plugins: [\n IdentityMiddlewarePlugin,\n GroupingMiddlewarePlugin,\n ...(config.plugins || []),\n ],\n }) as AnalyticsInstanceWithGroups<GroupNames>\n this._analytics.groups = this._groups\n }\n\n /**\n * Tracks type-safe business events. Recommended to use in most cases in app's codebase.\n * To add an event, modify \"Events\" generic.\n */\n async track<EventName extends Extract<keyof Events, string>>(eventName: EventName, eventData: Events[EventName]): Promise<void> {\n await this._analytics.track(eventName, eventData)\n }\n\n /**\n * Tracks untyped analytics events, used mainly for external sources (bridge / ui-kit / messages, etc.)\n * @deprecated It's not recommended to use this in your business logic, consider using typed \"track\" instead\n */\n async trackUntyped (eventName: string, eventData: AnyPayload): Promise<void> {\n await this._analytics.track(eventName, eventData)\n }\n\n /**\n * Tracks page changing in SPAs\n */\n async pageView (data?: PageData): Promise<void> {\n await this._analytics.page(data)\n }\n\n /**\n * Identifies user in analytics provider.\n * To specify all possible shape of user's data, modify \"UserData\" generic\n *\n * NOTE: Analytics plugins don't have a fixed behavior on how to handle consecutive identify calls.\n * Some of them affect only subsequent events, others affect all user events.\n * Therefore, it is not recommended to put cohort-specific data (organization, address, language, etc.) here.\n * Instead, use something like \"group\" method if your plugins supports it.\n */\n async identify<Key extends keyof UserData> (userId: string, userData?: Pick<UserData, Key>): Promise<void> {\n await this._analytics.identify(userId, userData)\n }\n\n /**\n * Resets analytics providers\n */\n async reset (): Promise<void> {\n for (const groupName of this._groups) {\n const groupKey = Analytics.getGroupKey(groupName)\n this._analytics.storage.removeItem(groupKey)\n }\n this._groups.clear()\n await this._analytics.reset()\n }\n\n static getGroupKey (groupName: string): string {\n return ['analytics', 'groups', groupName].join(':')\n }\n\n /**\n * Associates the user with a group, adding the attributes `groups.${groupName} = groupId`\n * to all subsequent analytic queries for the user\n * @example\n * analytics.setGroup('organization', organizationId)\n */\n setGroup (groupName: GroupNames, groupId: string): void {\n const groupKey = Analytics.getGroupKey(groupName)\n this._groups.add(groupName)\n this._analytics.storage.setItem(groupKey, groupId)\n }\n\n /**\n * Removes the current user from the group, stripping the “groups.${groupName}”\n * attribute from all subsequent eventualities\n * @example\n * deleteOrganization()\n * .then(() => analytics.removeGroup('organization'))\n */\n removeGroup (groupName: GroupNames): void {\n const groupKey = Analytics.getGroupKey(groupName)\n this._analytics.storage.removeItem(groupKey)\n this._groups.delete(groupName)\n }\n}\n","import { Analytics } from '../instance'\n\nimport type { AnalyticsPlugin, PluginTrackData } from '../types'\n\nfunction _addGroupingProperties (data: PluginTrackData): PluginTrackData {\n const { instance } = data\n\n for (const groupName of instance.groups) {\n const groupKey = Analytics.getGroupKey(groupName)\n const groupValue = instance.storage.getItem(groupKey)\n\n if (typeof groupValue === 'string') {\n const groupAttrName = `groups.${groupName}`\n data.payload.properties[groupAttrName] = groupValue\n }\n }\n return data\n}\n\nexport const GroupingMiddlewarePlugin: AnalyticsPlugin = {\n name: 'analytics-plugin-grouping',\n track: _addGroupingProperties,\n page: _addGroupingProperties,\n}","import type { AnalyticsPlugin, PluginTrackData } from '../types'\n\nconst IDENTITY_PROPERTIES = ['app', 'version']\n\nfunction _addIdentityProperties (data: PluginTrackData): PluginTrackData {\n const { instance } = data\n\n for (const contextPropertyName of IDENTITY_PROPERTIES) {\n const propertyValue = instance.getState(`context.${contextPropertyName}`)\n if (typeof propertyValue === 'string') {\n data.payload.properties[contextPropertyName] = propertyValue\n }\n }\n\n return data\n}\n\nexport const IdentityMiddlewarePlugin: AnalyticsPlugin = {\n name: 'analytics-plugin-identity',\n track: _addIdentityProperties,\n page: _addIdentityProperties,\n}","import { serialize as serializeCookie } from 'cookie'\nimport { setCookie, getCookies } from 'cookies-next'\n\nimport { getProxyHeadersForIp } from './proxying'\nimport { getRequestIp } from './proxying'\nimport {\n FINGERPRINT_ID_COOKIE_NAME,\n generateFingerprint,\n} from './sender'\nimport { getAppTracingHeaders } from './tracing'\n\nimport type { DefaultContext, RequestHandler } from '@apollo/client'\nimport type { IncomingMessage, ServerResponse } from 'http'\n\ntype Response = ServerResponse\n\ntype SSRContext = {\n headers: Record<string, string>\n defaultContext: DefaultContext\n}\n\nexport type TracingMiddlewareOptions = {\n serviceUrl: string\n codeVersion: string\n target?: string\n}\n\nexport type SSRProxyingMiddlewareOptions = {\n apiUrl: string\n proxyId?: string\n proxySecret?: string\n}\n\nexport function getTracingMiddleware (options: TracingMiddlewareOptions): RequestHandler {\n return function (operation, forward) {\n operation.setContext((previousContext: DefaultContext) => {\n const { headers: previousHeaders } = previousContext\n\n return {\n ...previousContext,\n headers: getAppTracingHeaders({\n ...options,\n previousHeaders,\n }),\n }\n })\n\n return forward(operation)\n }\n}\n\nexport function getSSRProxyingMiddleware ({ proxyId, proxySecret, apiUrl }: SSRProxyingMiddlewareOptions): RequestHandler {\n return function (operation, forward) {\n operation.setContext((previousContext: DefaultContext) => {\n if (typeof previousContext.clientIp !== 'string' || !proxyId || !proxySecret) return previousContext\n const proxyHeaders = getProxyHeadersForIp(\n 'POST',\n apiUrl,\n previousContext.clientIp,\n proxyId,\n proxySecret,\n )\n\n return {\n ...previousContext,\n headers: {\n ...previousContext.headers,\n ...proxyHeaders,\n },\n }\n })\n\n return forward(operation)\n }\n}\n\nexport function prepareSSRContext (req?: IncomingMessage, res?: Response): SSRContext {\n if (!req) {\n return {\n headers: {},\n defaultContext: {},\n }\n }\n\n const requestCookies = getCookies({ req, res })\n\n if (!requestCookies[FINGERPRINT_ID_COOKIE_NAME]) {\n const fingerprint = generateFingerprint()\n requestCookies[FINGERPRINT_ID_COOKIE_NAME] = fingerprint\n // NOTE: req and res are used to operate \"set-cookie\" headers\n setCookie(FINGERPRINT_ID_COOKIE_NAME, fingerprint, { req, res })\n }\n\n const cookieHeader = Object.entries(requestCookies)\n .map(([name, value]) => value ? serializeCookie(name, value) : null)\n .filter(Boolean)\n .join(';')\n\n const clientIp = getRequestIp(req, () => true)\n\n const headers: Record<string, string> = {\n 'cookie': cookieHeader,\n }\n\n if (req.headers['accept-language']) {\n headers['accept-language'] = req.headers['accept-language']\n }\n\n return {\n headers,\n defaultContext: {\n clientIp,\n },\n }\n}\n","import jwt from 'jsonwebtoken'\nimport proxyAddr from 'proxy-addr'\nimport { z } from 'zod'\n\nimport { isInSubnet } from '../ip'\n\nimport type { IncomingMessage } from 'http'\n\ntype ProxyConfig = {\n address: string | Array<string>\n secret: string\n}\n\ntype ProxyId = string\n\nexport type KnownProxies = Record<ProxyId, ProxyConfig>\n\nexport type TrustProxyFunction = (proxyAddr: string, idx: number) => boolean\n\nconst _ipSchema = z.union([z.ipv4(), z.ipv6()])\nconst _timeStampBasicRegexp = /^\\d+$/\n\nconst DEFAULT_PROXY_TIMEOUT_IN_MS = 5_000 // 5 sec to pass request is enough\nconst X_PROXY_ID_HEADER = 'x-proxy-id' as const\nconst X_PROXY_IP_HEADER = 'x-proxy-ip' as const\nconst X_PROXY_TIMESTAMP_HEADER = 'x-proxy-timestamp' as const\nconst X_PROXY_SIGNATURE_HEADER = 'x-proxy-signature' as const\n\nexport type ProxyHeaders = {\n [X_PROXY_ID_HEADER]: string\n [X_PROXY_IP_HEADER]: string\n [X_PROXY_TIMESTAMP_HEADER]: string\n [X_PROXY_SIGNATURE_HEADER]: string\n}\n\nfunction _getTimestampFromHeader (timestamp: string) {\n if (!_timeStampBasicRegexp.test(timestamp)) return Number.NaN\n return (new Date(parseInt(timestamp))).getTime()\n}\n\nfunction _isProxyIP (ip: string, proxyConfig: KnownProxies[string]) {\n const addresses = Array.isArray(proxyConfig.address) ? proxyConfig.address : [proxyConfig.address]\n const config = addresses.reduce((acc, addr) => {\n const isSubnet = addr.split('/').length > 1\n if (isSubnet) {\n acc.subnets.push(addr)\n } else {\n acc.ips.push(addr)\n }\n\n return acc\n }, { ips: [] as Array<string>, subnets: [] as Array<string> })\n\n if (config.ips.length && config.ips.includes(ip)) {\n return true\n }\n\n return !!(config.subnets.length && isInSubnet(ip, config.subnets))\n}\n\nexport function getRequestIp (req: IncomingMessage, trustProxyFn: TrustProxyFunction, knownProxies?: KnownProxies): string {\n // NOTE: That's what express does under the hood: https://github.com/expressjs/express/blob/4.x/lib/request.js#L349\n const originalIP = proxyAddr(req, trustProxyFn)\n\n if (!knownProxies) return originalIP\n\n const xProxyId = req.headers[X_PROXY_ID_HEADER]\n const xProxyIp = req.headers[X_PROXY_IP_HEADER]\n // NOTE: used to prevent relay attacks\n const xProxyTimestamp = req.headers[X_PROXY_TIMESTAMP_HEADER]\n const xProxySignature = req.headers[X_PROXY_SIGNATURE_HEADER]\n\n if (\n typeof xProxyId !== 'string' ||\n typeof xProxyIp !== 'string' ||\n typeof xProxyTimestamp !== 'string' ||\n typeof xProxySignature !== 'string'\n ) {\n return originalIP\n }\n\n // NOTE: validate, that x-proxy-ip is correct IP\n const { success: isValidIp } = _ipSchema.safeParse(xProxyIp)\n if (!isValidIp) {\n return originalIP\n }\n\n // NOTE: validate timestamp: it should less than now and no more than 5s less than now (recent enough)\n const timestamp = _getTimestampFromHeader(xProxyTimestamp)\n const now = Date.now()\n if (\n Number.isNaN(timestamp) ||\n timestamp > now ||\n now - timestamp > DEFAULT_PROXY_TIMEOUT_IN_MS\n ) {\n return originalIP\n }\n\n // NOTE: validate signature and proxy IP\n if (!Object.hasOwn(knownProxies, xProxyId)) {\n return originalIP\n }\n const proxyConfig = knownProxies[xProxyId]\n\n if (!proxyConfig || !_isProxyIP(originalIP, proxyConfig)) {\n return originalIP\n }\n\n try {\n // NOTE: config is passed from outside, where its obtained from .env, so its not hard-coded\n // nosemgrep: javascript.jsonwebtoken.security.jwt-hardcode.hardcoded-jwt-secret\n const jwtPayload = jwt.verify(xProxySignature, proxyConfig.secret, { algorithms: ['HS256'] })\n const expectedPayloadSchema = z.object({\n [X_PROXY_TIMESTAMP_HEADER]: z.literal(xProxyTimestamp),\n [X_PROXY_IP_HEADER]: z.literal(xProxyIp),\n [X_PROXY_ID_HEADER]: z.literal(xProxyId),\n method: z.literal(req.method),\n url: z.literal(req.url),\n })\n const { success: isMatchingSignature } = expectedPayloadSchema.safeParse(jwtPayload)\n\n return isMatchingSignature ? xProxyIp : originalIP\n } catch {\n return originalIP\n }\n}\n\nexport function getProxyHeadersForIp (method: string, url: string, ip: string, proxyId: string, secret: string): ProxyHeaders {\n const timestampString = String(Date.now())\n\n return {\n [X_PROXY_IP_HEADER]: ip,\n [X_PROXY_ID_HEADER]: proxyId,\n [X_PROXY_TIMESTAMP_HEADER]: timestampString,\n [X_PROXY_SIGNATURE_HEADER]: jwt.sign({\n [X_PROXY_IP_HEADER]: ip,\n [X_PROXY_ID_HEADER]: proxyId,\n [X_PROXY_TIMESTAMP_HEADER]: timestampString,\n method,\n url,\n }, secret, {\n expiresIn: Math.round(DEFAULT_PROXY_TIMEOUT_IN_MS / 1000),\n algorithm: 'HS256',\n }),\n }\n}\n\nexport function isRelativeUrl (url: string) {\n return url.startsWith('/')\n}\n\nexport function replaceUpstreamEndpoint ({\n endpoint,\n proxyPrefix,\n upstreamPrefix,\n upstreamOrigin,\n rewrites = {},\n}: {\n endpoint: string\n proxyPrefix: string\n upstreamPrefix: string\n upstreamOrigin: string\n rewrites?: Record<string, string>\n}) {\n const isRelativeLocation = isRelativeUrl(endpoint)\n const locationUrl = new URL(endpoint, 'https://_')\n\n let targetLocation\n\n const lookupUrl = new URL(endpoint, upstreamOrigin)\n lookupUrl.search = ''\n // First lookup relative location ('/some/path')\n if (isRelativeLocation || lookupUrl.origin === upstreamOrigin) {\n targetLocation ??= rewrites[lookupUrl.pathname]\n }\n\n // Then lookup absolute location ('https://upstreamhost.com/some/path')\n targetLocation ??= rewrites[lookupUrl.toString()]\n\n // If found lookup, perform smart replacement\n if (targetLocation) {\n const isRelativeTarget = isRelativeUrl(targetLocation)\n const targetUrl = new URL(targetLocation, upstreamOrigin)\n const targetSearchParams = new URLSearchParams(targetUrl.searchParams)\n // Replace target search params with location search params, then apply target search params on top\n if (!targetUrl.hash) {\n targetUrl.hash = locationUrl.hash\n }\n targetUrl.search = locationUrl.search\n for (const [name] of targetSearchParams.entries()) {\n targetUrl.searchParams.delete(name)\n }\n for (const [name, value] of targetSearchParams.entries()) {\n targetUrl.searchParams.append(name, value)\n }\n\n if (isRelativeTarget) {\n return targetUrl.pathname + targetUrl.search + targetUrl.hash\n } else {\n return targetUrl.toString()\n }\n }\n\n // If location is relative or has same as upstream domain, try to replace back upstreamPrefix with proxyPrefix\n if ((isRelativeLocation || locationUrl.origin === upstreamOrigin) && locationUrl.pathname.startsWith(upstreamPrefix)) {\n locationUrl.pathname = proxyPrefix + locationUrl.pathname.slice(upstreamPrefix.length)\n\n return locationUrl.pathname + locationUrl.search + locationUrl.hash\n }\n\n return endpoint\n}\n","// RegExp for testing if a string represents an IPv4 address\nconst v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'\nconst v4Str = `(${v4Seg}[.]){3}${v4Seg}`\nconst IPv4Reg = new RegExp(`^${v4Str}$`)\n\n// RegExp for testing if a string represents an IPv6 address\nconst v6Seg = '(?:[0-9a-fA-F]{1,4})'\nconst IPv6Reg = new RegExp(\n '^(' +\n `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +\n `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +\n `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` +\n `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` +\n `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` +\n `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` +\n `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` +\n `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +\n ')(%[0-9a-zA-Z]{1,})?$'\n)\n\n/**\n * Returns true if the string represents an IPv4 address. Matches Node.js net.isIPv4\n * functionality.\n */\nexport function isIPv4 (s: string) {\n return IPv4Reg.test(s)\n}\n\n/**\n * Returns true if the string represents an IPv6 address. Matches Node.js net.isIPv6\n * functionality.\n */\nexport function isIPv6 (s: string) {\n return IPv6Reg.test(s)\n}\n\nexport function isIP (s: string) {\n if (isIPv4(s)) return 4\n if (isIPv6(s)) return 6\n return 0\n}\n","import ipRanges from './ranges'\nimport * as util from './utils'\n\n/**\n * Given an IPv4 address, convert it to a 32-bit long integer.\n * @param ip the IPv4 address to expand\n * @throws if the string is not a valid IPv4 address\n */\nfunction ipv4ToLong (ip: string) {\n if (!util.isIPv4(ip)) {\n throw new Error(`not a valid IPv4 address: ${ip}`)\n }\n const octets = ip.split('.')\n return (\n ((parseInt(octets[0], 10) << 24) +\n (parseInt(octets[1], 10) << 16) +\n (parseInt(octets[2], 10) << 8) +\n parseInt(octets[3], 10)) >>>\n 0\n )\n}\n\n// this is the most optimised checker.\nfunction createLongChecker (subnet: string): (addressLong: number) => boolean {\n const [subnetAddress, prefixLengthString] = subnet.split('/')\n const prefixLength = parseInt(prefixLengthString, 10)\n if (!subnetAddress || !Number.isInteger(prefixLength)) {\n throw new Error(`not a valid IPv4 subnet: ${subnet}`)\n }\n\n if (prefixLength < 0 || prefixLength > 32) {\n throw new Error(`not a valid IPv4 prefix length: ${prefixLength} (from ${subnet})`)\n }\n\n const subnetLong = ipv4ToLong(subnetAddress)\n return addressLong => {\n if (prefixLength === 0) {\n return true\n }\n const subnetPrefix = subnetLong >> (32 - prefixLength)\n const addressPrefix = addressLong >> (32 - prefixLength)\n\n return subnetPrefix === addressPrefix\n }\n}\n\n/**\n * The functional version, creates a checking function that takes an IPv4 Address and\n * returns whether or not it is contained in (one of) the subnet(s).\n * @param subnetOrSubnets the IPv4 CIDR to test (or an array of them)\n * @throws if the subnet is not a valid IP addresses, or the CIDR prefix length\n * is not valid\n */\nexport function createChecker (\n subnetOrSubnets: string | string[]\n): (address: string) => boolean {\n if (Array.isArray(subnetOrSubnets)) {\n const checks = subnetOrSubnets.map(subnet => createLongChecker(subnet))\n return address => {\n const addressLong = ipv4ToLong(address)\n return checks.some(check => check(addressLong))\n }\n }\n const check = createLongChecker(subnetOrSubnets)\n return address => {\n const addressLong = ipv4ToLong(address)\n return check(addressLong)\n }\n}\n\n/**\n * Test if the given IPv4 address is contained in the specified subnet.\n * @param address the IPv4 address to check\n * @param subnetOrSubnets the IPv4 CIDR to test (or an array of them)\n * @throws if the address or subnet are not valid IP addresses, or the CIDR prefix length\n * is not valid\n */\nexport function isInSubnet (address: string, subnetOrSubnets: string | string[]): boolean {\n return createChecker(subnetOrSubnets)(address)\n}\n\n// cache these special subnet checkers\nconst specialNetsCache: Record<string, (address: string) => boolean> = {}\n\n/** Test if the given IP address is a private/internal IP address. */\nexport function isPrivate (address: string) {\n if (!('private' in specialNetsCache)) {\n specialNetsCache['private'] = createChecker(ipRanges.private.ipv4)\n }\n return specialNetsCache['private'](address)\n}\n\n/** Test if the given IP address is a localhost address. */\nexport function isLocalhost (address: string) {\n if (!('localhost' in specialNetsCache)) {\n specialNetsCache['localhost'] = createChecker(ipRanges.localhost.ipv4)\n }\n return specialNetsCache['localhost'](address)\n}\n\n/** Test if the given IP address is in a known reserved range and not a normal host IP */\nexport function isReserved (address: string) {\n if (!('reserved' in specialNetsCache)) {\n specialNetsCache['reserved'] = createChecker(ipRanges.reserved.ipv4)\n }\n return specialNetsCache['reserved'](address)\n}\n\n/**\n * Test if the given IP address is a special address of any kind (private, reserved,\n * localhost)\n */\nexport function isSpecial (address: string) {\n if (!('special' in specialNetsCache)) {\n specialNetsCache['special'] = createChecker([\n ...ipRanges.private.ipv4,\n ...ipRanges.localhost.ipv4,\n ...ipRanges.reserved.ipv4,\n ])\n }\n return specialNetsCache['special'](address)\n}","import ipRanges from './ranges'\nimport * as util from './utils'\n\n// Note: Profiling shows that on recent versions of Node, string.split(RegExp) is faster\n// than string.split(string).\nconst dot = /\\./\nconst mappedIpv4 = /^(.+:ffff:)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(?:%.+)?$/\nconst colon = /:/\nconst doubleColon = /::/\n\n/**\n * Given a mapped IPv4 address (e.g. ::ffff:203.0.113.38 or similar), convert it to the\n * equivalent standard IPv6 address.\n * @param ip the IPv4-to-IPv6 mapped address\n */\nfunction mappedIpv4ToIpv6 (ip: string) {\n const matches = ip.match(mappedIpv4)\n\n if (!matches || !util.isIPv4(matches[2])) {\n throw new Error(`not a mapped IPv4 address: ${ip}`)\n }\n\n // mapped IPv4 address\n const prefix = matches[1]\n const ipv4 = matches[2]\n\n const parts = ipv4.split(dot).map(x => parseInt(x, 10))\n\n const segment7 = ((parts[0] << 8) + parts[1]).toString(16)\n const segment8 = ((parts[2] << 8) + parts[3]).toString(16)\n\n return `${prefix}${segment7}:${segment8}`\n}\n\n/**\n * Given a mapped IPv4 address, return the bare IPv4 equivalent.\n */\nexport function extractMappedIpv4 (ip: string) {\n const matches = ip.match(mappedIpv4)\n\n if (!matches || !util.isIPv4(matches[2])) {\n throw new Error(`not a mapped IPv4 address: ${ip}`)\n }\n\n return matches[2]\n}\n\n/**\n * Given an IP address that may have double-colons, expand all segments and return them\n * as an array of 8 segments (16-bit words). As a peformance enhancement (indicated by\n * profiling), for any segment that was missing but should be a '0', returns undefined.\n * @param ip the IPv6 address to expand\n * @throws if the string is not a valid IPv6 address\n */\nfunction getIpv6Segments (ip: string): string[] {\n if (!util.isIPv6(ip)) {\n throw new Error(`not a valid IPv6 address: ${ip}`)\n }\n\n if (dot.test(ip)) {\n return getIpv6Segments(mappedIpv4ToIpv6(ip))\n }\n\n // break it into an array, including missing \"::\" segments\n const [beforeChunk, afterChunk] = ip.split(doubleColon)\n\n const beforeParts = (beforeChunk && beforeChunk.split(colon)) || []\n const afterParts = (afterChunk && afterChunk.split(colon)) || []\n const missingSegments = new Array<string>(8 - (beforeParts.length + afterParts.length))\n\n return beforeParts.concat(missingSegments, afterParts)\n}\n\n/**\n * Test if the given IPv6 address is contained in the specified subnet.\n * @param address the IPv6 address to check\n * @param subnetOrSubnets the IPv6 CIDR to test (or an array of them)\n * @throws if the address or subnet are not valid IP addresses, or the CIDR prefix length\n * is not valid\n */\nexport function isInSubnet (address: string, subnetOrSubnets: string | string[]): boolean {\n return createChecker(subnetOrSubnets)(address)\n}\n\n/**\n * Create a function to test if a given IPv6 address is contained in the specified subnet.\n * @param subnetOrSubnets the IPv6 CIDR to test (or an array of them)\n * @throws if the subnet(s) are not valid IP addresses, or the CIDR prefix lengths\n * are not valid\n */\nexport function createChecker (\n subnetOrSubnets: string | string[]\n): (address: string) => boolean {\n if (Array.isArray(subnetOrSubnets)) {\n const checks = subnetOrSubnets.map(subnet => createSegmentChecker(subnet))\n return address => {\n const segments = getIpv6Segments(address)\n return checks.some(check => check(segments))\n }\n }\n const check = createSegmentChecker(subnetOrSubnets)\n return address => {\n const segments = getIpv6Segments(address)\n return check(segments)\n }\n}\n\n// This creates the last function that works on the most deconstructed data\nfunction createSegmentChecker (subnet: string): (segments: string[]) => boolean {\n const [subnetAddress, prefixLengthString] = subnet.split('/')\n const prefixLength = parseInt(prefixLengthString, 10)\n\n if (!subnetAddress || !Number.isInteger(prefixLength)) {\n throw new Error(`not a valid IPv6 CIDR subnet: ${subnet}`)\n }\n\n if (prefixLength < 0 || prefixLength > 128) {\n throw new Error(`not a valid IPv6 prefix length: ${prefixLength} (from ${subnet})`)\n }\n\n // the next line throws if the address is not a valid IPv6 address\n const subnetSegments = getIpv6Segments(subnetAddress)\n\n return addressSegments => {\n for (let i = 0; i < 8; ++i) {\n const bitCount = Math.min(prefixLength - i * 16, 16)\n\n if (bitCount <= 0) {\n break\n }\n\n const subnetPrefix =\n ((subnetSegments[i] && parseInt(subnetSegments[i], 16)) || 0) >> (16 - bitCount)\n\n const addressPrefix =\n ((addressSegments[i] && parseInt(addressSegments[i], 16)) || 0) >>\n (16 - bitCount)\n\n if (subnetPrefix !== addressPrefix) {\n return false\n }\n }\n\n return true\n }\n}\n\n// cache these special subnet checkers\nconst specialNetsCache: Record<string, (address: string) => boolean> = {}\n\n/** Test if the given IP address is a private/internal IP address. */\nexport function isPrivate (address: string) {\n if (!('private' in specialNetsCache)) {\n specialNetsCache['private'] = createChecker(ipRanges.private.ipv6)\n }\n return specialNetsCache['private'](address)\n}\n\n/** Test if the given IP address is a localhost address. */\nexport function isLocalhost (address: string) {\n if (!('localhost' in specialNetsCache)) {\n specialNetsCache['localhost'] = createChecker(ipRanges.localhost.ipv6)\n }\n return specialNetsCache['localhost'](address)\n}\n\n/** Test if the given IP address is an IPv4 address mapped onto IPv6 */\nexport function isIPv4MappedAddress (address: string) {\n if (!('mapped' in specialNetsCache)) {\n specialNetsCache['mapped'] = createChecker('::ffff:0:0/96')\n }\n if (specialNetsCache['mapped'](address)) {\n const matches = address.match(mappedIpv4)\n return Boolean(matches && util.isIPv4(matches[2]))\n }\n return false\n}\n\n/** Test if the given IP address is in a known reserved range and not a normal host IP */\nexport function isReserved (address: string) {\n if (!('reserved' in specialNetsCache)) {\n specialNetsCache['reserved'] = createChecker(ipRanges.reserved.ipv6)\n }\n return specialNetsCache['reserved'](address)\n}\n\n/**\n * Test if the given IP address is a special address of any kind (private, reserved,\n * localhost)\n */\nexport function isSpecial (address: string) {\n if (!('special' in specialNetsCache)) {\n specialNetsCache['special'] = createChecker([\n ...ipRanges.private.ipv6,\n ...ipRanges.localhost.ipv6,\n ...ipRanges.reserved.ipv6,\n ])\n }\n return specialNetsCache['special'](address)\n}","import * as IPv4 from './ipv4'\nimport * as IPv6 from './ipv6'\nimport * as util from './utils'\n\nexport { isIP, isIPv4, isIPv6 } from './utils'\nexport { IPv4, IPv6 }\n\n/**\n * Test if the given IP address is contained in the specified subnet.\n * @param address the IPv4 or IPv6 address to check\n * @param subnetOrSubnets the IPv4 or IPv6 CIDR to test (or an array of them)\n * @throws if any of the address or subnet(s) are not valid IP addresses, or the CIDR\n * prefix length is not valid\n */\nexport function isInSubnet (address: string, subnetOrSubnets: string | string[]): boolean {\n return createChecker(subnetOrSubnets)(address)\n}\n/**\n * Create a function to test if the given IP address is contained in the specified subnet.\n * @param subnetOrSubnets the IPv4 or IPv6 CIDR to test (or an array of them)\n * @throws if any of the subnet(s) are not valid IP addresses, or the CIDR\n * prefix length is not valid\n */\nexport function createChecker (\n subnetOrSubnets: string | string[]\n): (address: string) => boolean {\n if (!Array.isArray(subnetOrSubnets)) {\n return createChecker([subnetOrSubnets])\n }\n\n const subnetsByVersion = subnetOrSubnets.reduce(\n (acc, subnet) => {\n const ip = subnet.split('/')[0];\n (acc[util.isIP(ip)] as string[]).push(subnet)\n return acc\n },\n { 0: [], 4: [], 6: [] }\n )\n\n if (subnetsByVersion[0].length !== 0) {\n throw new Error(`some subnets are not valid IP addresses: ${subnetsByVersion[0]}`)\n }\n\n const check4 = IPv4.createChecker(subnetsByVersion[4])\n const check6 = IPv6.createChecker(subnetsByVersion[6])\n\n return address => {\n if (!util.isIP(address)) {\n throw new Error(`not a valid IPv4 or IPv6 address: ${address}`)\n }\n\n // for mapped IPv4 addresses, compare against both IPv6 and IPv4 subnets\n if (util.isIPv6(address) && IPv6.isIPv4MappedAddress(address)) {\n return check6(address) || check4(IPv6.extractMappedIpv4(address))\n }\n\n if (util.isIPv6(address)) {\n return check6(address)\n } else {\n return check4(address)\n }\n }\n}\n\n/** Test if the given IP address is a private/internal IP address. */\nexport function isPrivate (address: string) {\n if (util.isIPv6(address)) {\n if (IPv6.isIPv4MappedAddress(address)) {\n return IPv4.isPrivate(IPv6.extractMappedIpv4(address))\n }\n return IPv6.isPrivate(address)\n } else {\n return IPv4.isPrivate(address)\n }\n}\n\n/** Test if the given IP address is a localhost address. */\nexport function isLocalhost (address: string) {\n if (util.isIPv6(address)) {\n if (IPv6.isIPv4MappedAddress(address)) {\n return IPv4.isLocalhost(IPv6.extractMappedIpv4(address))\n }\n return IPv6.isLocalhost(address)\n } else {\n return IPv4.isLocalhost(address)\n }\n}\n\n/** Test if the given IP address is an IPv4 address mapped onto IPv6 */\nexport function isIPv4MappedAddress (address: string) {\n if (util.isIPv6(address)) {\n return IPv6.isIPv4MappedAddress(address)\n } else {\n return false\n }\n}\n\n/** Test if the given IP address is in a known reserved range and not a normal host IP */\nexport function isReserved (address: string) {\n if (util.isIPv6(address)) {\n if (IPv6.isIPv4MappedAddress(address)) {\n return IPv4.isReserved(IPv6.extractMappedIpv4(address))\n }\n return IPv6.isReserved(address)\n } else {\n return IPv4.isReserved(address)\n }\n}\n\n/**\n * Test if the given IP address is a special address of any kind (private, reserved,\n * localhost)\n */\nexport function isSpecial (address: string) {\n if (util.isIPv6(address)) {\n if (IPv6.isIPv4MappedAddress(address)) {\n return IPv4.isSpecial(IPv6.extractMappedIpv4(address))\n }\n return IPv6.isSpecial(address)\n } else {\n return IPv4.isSpecial(address)\n }\n}\n\nexport const check = isInSubnet","import httpProxy from 'http-proxy'\n\nimport { getProxyHeadersForIp, getRequestIp, replaceUpstreamEndpoint } from './utils'\n\nimport type { KnownProxies, TrustProxyFunction } from './utils'\nimport type { IncomingMessage, ServerResponse } from 'http'\n\ntype IpProxyingOptions = {\n /** ID of the proxy to pass as x-proxy-id header */\n proxyId: string\n /** secret to sign x-proxy-signature header */\n proxySecret: string\n /** List of known proxies before current one from which IP can be extracted */\n knownProxies?: KnownProxies\n /**\n * Function to determine if a given IP address should be as x-forwarded-for header source.\n * Defaults to () => false, which means all IP addresses are trusted\n * */\n trustProxyFn?: TrustProxyFunction\n}\n\ntype LoggerType = {\n error: (data: unknown) => void\n}\n\ntype RelativeOrAbsoluteEndpoint = string\n\nexport type ProxyOptions = {\n /** Name of the proxy. Primarily used to set \"via\" header */\n name: string\n /** Proxy prefix which will be removed from request url */\n proxyPrefix: string\n /** Upstream host to proxy requests to */\n upstreamOrigin: string\n /** Upstream prefix to add to request url */\n upstreamPrefix: string\n /** IP proxying options, if specified, IP will be passed used signed x-proxy-id, x-proxy-ip, x-proxy-timestamp, x-proxy-signature headers */\n ipProxying?: IpProxyingOptions\n /** \n * Map of location header rewrites for redirects. \n * Key: upstream location, Value: rewritten location for client.\n * Used to rewrite Location headers in 3xx redirect responses.\n */\n locationRewrites?: Record<RelativeOrAbsoluteEndpoint, RelativeOrAbsoluteEndpoint>\n /** \n * Map of cookie path rewrites for Set-Cookie headers.\n * Key: upstream cookie path, Value: rewritten path for client.\n * Used to adjust cookie scope when proxying between different path prefixes.\n */\n cookiePathRewrites?: Record<RelativeOrAbsoluteEndpoint, RelativeOrAbsoluteEndpoint>\n /** \n * Logger instance for error reporting. Defaults to console if not provided.\n * Must implement an error method that accepts any data type.\n */\n logger?: LoggerType\n}\n\ntype ProxyHandler = (req: IncomingMessage, res: ServerResponse) => void\n\nexport function createProxy (options: ProxyOptions): ProxyHandler {\n const {\n name,\n proxyPrefix,\n upstreamOrigin,\n upstreamPrefix,\n ipProxying,\n locationRewrites,\n cookiePathRewrites,\n logger = console,\n } = options\n\n const proxy = httpProxy.createProxy({\n target: upstreamOrigin,\n changeOrigin: true,\n })\n\n const trustProxyFn = ipProxying?.trustProxyFn ?? (() => false)\n\n proxy.on('proxyReq', (proxyReq, req) => {\n if (req.url?.startsWith(proxyPrefix)) {\n proxyReq.path = upstreamPrefix + req.url.slice(proxyPrefix.length)\n }\n proxyReq.setHeader('via', name)\n\n if (req.url && req.method && ipProxying) {\n const ip = getRequestIp(req, trustProxyFn, ipProxying.knownProxies)\n const headers = getProxyHeadersForIp(req.method, proxyReq.path, ip, ipProxying.proxyId, ipProxying.proxySecret)\n for (const [headerName, headerValue] of Object.entries(headers)) {\n proxyReq.setHeader(headerName, headerValue)\n }\n }\n })\n proxy.on('proxyRes', (proxyRes, _req, _res) => {\n if (proxyRes.headers.location) {\n proxyRes.headers.location = replaceUpstreamEndpoint({\n endpoint: proxyRes.headers.location,\n proxyPrefix,\n upstreamPrefix,\n upstreamOrigin,\n rewrites: locationRewrites,\n })\n }\n\n // Handle Set-Cookie headers to rewrite cookie paths\n const setCookieHeaders = proxyRes.headers['set-cookie']\n if (setCookieHeaders) {\n proxyRes.headers['set-cookie'] = setCookieHeaders.map(cookieString => {\n return cookieString.replace(/;\\s*Path=([^;]+)/i, (match, pathValue) => {\n const rewrittenPath = replaceUpstreamEndpoint({\n endpoint: pathValue,\n proxyPrefix,\n upstreamPrefix,\n upstreamOrigin,\n rewrites: cookiePathRewrites,\n })\n return match.replace(pathValue, rewrittenPath)\n })\n })\n }\n })\n\n return function syncProxyHandler (req, res) {\n proxy.web(req, res, {}, (err) => {\n if (err) {\n // TODO: Add more complex loggers and standard error handling in next iterations\n logger.error({ msg: 'Proxy error', err })\n if (!res.headersSent) {\n res.writeHead(502, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ errors: [{ message: 'Proxy error' }] }))\n } else {\n res.end()\n }\n }\n })\n }\n}","import { getCookie, setCookie } from 'cookies-next'\n\nimport { getEmbeddingContext } from './embeddingContext'\nimport { generateUUIDv4 } from './uuid'\n\ntype SenderInfo = {\n dv: number\n fingerprint: string\n}\n\n/** Name of the cookie in which the fingerprint will be stored */\nexport const FINGERPRINT_ID_COOKIE_NAME = 'fingerprint'\n/** Default fingerprint length */\nexport const FINGERPRINT_ID_LENGTH = 32\n/** Fingerprint cookie should be persistent, so we are giving it a big maxAge */\nconst VERY_LONG_MAX_AGE_IN_SECONDS = Math.pow(2, 31) - 1 // Around 68 years in seconds\n\nfunction makeId (length: number): string {\n const croppedLength = Math.min(length, 32)\n\n return generateUUIDv4().replaceAll('-', '').substring(0, croppedLength)\n}\n\nexport function generateFingerprint (): string {\n return makeId(FINGERPRINT_ID_LENGTH)\n}\n\n/**\n * Creates a device fingerprint in the browser environment\n * that can be used to send mutations in open-condo applications,\n * uses cookies for storage between sessions.\n * Mostly used to generate the sender field in getClientSideSenderInfo.\n * So consider using it instead\n */\nexport function getClientSideFingerprint (): string {\n // NOTE: if we're inside another application, use its device id as fingerprint, but not persist it in cookie\n const embeddingContext = getEmbeddingContext()\n if (embeddingContext) {\n return embeddingContext.ctx.device.id\n }\n\n let fingerprint = getCookie(FINGERPRINT_ID_COOKIE_NAME)\n if (!fingerprint) {\n fingerprint = generateFingerprint()\n }\n // Since 2022 browsers maintain cookie for 400 days at max, so let's update cookie expiration time\n setCookie(FINGERPRINT_ID_COOKIE_NAME, fingerprint, {\n maxAge: VERY_LONG_MAX_AGE_IN_SECONDS, // no \"maxAge\" or \"expires\" means that cookie clears when session ends (f.e. when browser closes)\n })\n\n return fingerprint\n}\n\n/**\n * Creates a device fingerprint in the browser environment\n * that can be used to send mutations in open-condo applications.\n * Uses cookies for storage between sessions\n * @example\n * submitReadingsMutation({\n * variables: {\n * data: {\n * ...values,\n * dv: 1,\n * sender: getClientSideSenderInfo(),\n * meter: { connect: { id: meter.id } },\n * source: { connect: { id: METER_READING_MOBILE_APP_SOURCE_ID } },\n * },\n * },\n * })\n */\nexport function getClientSideSenderInfo (): SenderInfo {\n return {\n dv: 1,\n fingerprint: getClientSideFingerprint(),\n }\n}\n","import { deleteCookie, getCookie, setCookie } from 'cookies-next'\nimport React, { useEffect, useMemo, useState, createContext, useContext } from 'react'\nimport { z } from 'zod'\n\nimport { generateUUIDv4 } from './uuid'\n\nimport type { AppType, Optional } from './common/types'\nimport type { IncomingMessage, ServerResponse } from 'http'\n\nconst EMBEDDING_CONTEXT_COOKIE_NAME = 'embeddingContext'\nconst EMBEDDING_CONTEXT_QUERY_PARAM = 'embeddingContext'\nconst EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY = 'isEmbeddingContextProvider'\nconst EMBEDDING_CONTEXT_PROP_NAME = '__EMBEDDING_CONTEXT__'\nconst EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS = 2_000\n\nconst EMBEDDING_CONTEXT_SCHEMA = z.strictObject({\n dv: z.literal(1),\n app: z.strictObject({\n id: z.string(),\n version: z.string().optional(),\n build: z.string().optional(),\n }),\n platform: z.enum(['iOS', 'Android', 'web']),\n os: z.strictObject({\n name: z.string(),\n version: z.string().optional(),\n }).optional(),\n device: z.strictObject({\n id: z.string(),\n }),\n})\n\nconst EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA = z.strictObject({\n ctx: EMBEDDING_CONTEXT_SCHEMA,\n source: z.enum(['query', 'cookie']),\n})\n\nconst IS_PRIMARY_ALIVE_MESSAGE_SCHEMA = z.object({\n type: z.literal('EmbeddingContextPrimaryPolling'),\n data: z.strictObject({\n requestId: z.string(),\n }),\n})\n\nconst IS_PRIMARY_ALIVE_RESPONSE_SCHEMA = z.object({\n type: z.literal('EmbeddingContextPrimaryPollingResult'),\n data: z.strictObject({\n requestId: z.string(),\n isPrimary: z.boolean(),\n }),\n})\n\nexport type EmbeddingContext = z.infer<typeof EMBEDDING_CONTEXT_SCHEMA>\ntype EmbeddingContextWithSource = z.infer<typeof EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA>\ntype IsPrimaryAliveMessage = z.infer<typeof IS_PRIMARY_ALIVE_MESSAGE_SCHEMA>\ntype IsPrimaryAliveResponse = z.infer<typeof IS_PRIMARY_ALIVE_RESPONSE_SCHEMA>\n\nconst ReactEmbeddingContext = createContext<EmbeddingContext | null>(null)\n\nexport function useEmbeddingContext (): EmbeddingContext | null {\n return useContext(ReactEmbeddingContext)\n}\n\nfunction b64toContext (b64: string): EmbeddingContext | null {\n try {\n const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0))\n const decodedUTFString = new TextDecoder().decode(bytes)\n const parsedCtx = JSON.parse(decodedUTFString)\n return EMBEDDING_CONTEXT_SCHEMA.parse(parsedCtx)\n } catch {\n return null\n }\n}\n\nfunction contextToB64 (ctx: EmbeddingContext): string {\n const stringCtx = JSON.stringify(ctx)\n const bytes = new TextEncoder().encode(stringCtx)\n return btoa(String.fromCharCode(...bytes))\n}\n\nexport function getEmbeddingContext (req?: Optional<IncomingMessage>, res?: Optional<ServerResponse>): EmbeddingContextWithSource | null {\n // NOTE: context can be found in query for primary tab\n try {\n const queryParamValue = req\n ? new URL(req.url ?? '/', 'https://_').searchParams.get(EMBEDDING_CONTEXT_QUERY_PARAM)\n : new URLSearchParams(window.location.search).get(EMBEDDING_CONTEXT_QUERY_PARAM)\n if (queryParamValue) {\n const ctx = b64toContext(decodeURIComponent(queryParamValue))\n if (ctx) return { ctx, source: 'query' }\n }\n } catch {\n // NOTE: decodeURIComponent might throw on invalid input, ignore it as non-valid query-param\n }\n\n // NOTE: context can be found in cookie for secondary tabs\n const cookieValue = getCookie(EMBEDDING_CONTEXT_COOKIE_NAME, { req, res })\n if (cookieValue) {\n const ctx = b64toContext(cookieValue)\n if (ctx) return { ctx, source: 'cookie' }\n }\n\n return null\n}\n\nexport function withEmbeddingContext<\n PropsType extends Record<string, unknown>,\n ComponentType,\n RouterType,\n> (App: AppType<PropsType, ComponentType, RouterType>): AppType<PropsType, ComponentType, RouterType> {\n const WithEmbeddingContext: AppType<PropsType, ComponentType, RouterType> = (props) => {\n const { pageProps } = props\n\n const propsContextWithSource = useMemo(() => {\n const { success, data } = EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA.safeParse(pageProps[EMBEDDING_CONTEXT_PROP_NAME])\n if (!success) return null\n return data\n }, [pageProps])\n\n const [embeddingContext, setEmbeddingContext] = useState<EmbeddingContext | null>(propsContextWithSource?.ctx ?? null)\n const [isPrimaryTab, setIsPrimaryTab] = useState<boolean | null>(propsContextWithSource?.source === 'query' ? true : null)\n const [bcChannel, setBCChannel] = useState<BroadcastChannel | null>(null)\n\n useEffect(() => {\n // NOTE: if primary tab, save it in session storage, so it won't be lost on user navigation\n if (isPrimaryTab === true && typeof window !== 'undefined') {\n window.sessionStorage.setItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY, 'true')\n }\n // NOTE: restore primary tab status if it was lost on navigation\n if (isPrimaryTab === null && typeof window !== 'undefined') {\n setIsPrimaryTab(window.sessionStorage.getItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY) === 'true')\n }\n }, [isPrimaryTab])\n\n useEffect(() => {\n if (typeof window