@open-condo/miniapp-utils
Version:
A set of helper functions / components / hooks used to build new condo apps fast
1 lines • 19.2 kB
Source Map (JSON)
{"version":3,"sources":["../../src/helpers/tracing.ts","../../src/helpers/sender.ts","../../src/helpers/embeddingContext.tsx","../../src/helpers/uuid.ts"],"sourcesContent":["import { parse as parseCookieString } from 'cookie'\n\nimport {\n FINGERPRINT_ID_COOKIE_NAME,\n getClientSideFingerprint,\n} from './sender'\nimport { generateUUIDv4 } from './uuid'\n\nexport type TracingOptions = {\n serviceUrl: string\n codeVersion: string\n target?: string\n previousHeaders?: Record<string, string>\n}\n\nconst SSR_DEFAULT_FINGERPRINT = 'webAppSSR'\nconst COOKIE_HEADER_NAME = 'cookie'\nconst REMOTE_APP_HEADER_NAME = 'x-remote-app'\nconst REMOTE_VERSION_HEADER_NAME = 'x-remote-version'\nconst REMOTE_CLIENT_HEADER_NAME = 'x-remote-client'\nconst REMOTE_ENV_HEADER_NAME = 'x-remote-env'\nconst TARGET_HEADER_NAME = 'x-target'\nconst START_REQUEST_ID_HEADER_NAME = 'x-start-request-id'\nconst PARENT_REQUEST_ID_HEADER_NAME = 'x-parent-request-id'\n\nfunction generateRequestId () {\n return `BR${generateUUIDv4().replaceAll('-', '')}`\n}\n\nexport function getAppTracingHeaders (options: TracingOptions) {\n const reqId = generateRequestId()\n\n const headers: Record<string, string> = {\n ...options.previousHeaders,\n [REMOTE_APP_HEADER_NAME]: options.serviceUrl,\n [REMOTE_VERSION_HEADER_NAME]: options.codeVersion,\n [PARENT_REQUEST_ID_HEADER_NAME]: reqId,\n [START_REQUEST_ID_HEADER_NAME]: reqId,\n }\n\n if (options.target) {\n headers[TARGET_HEADER_NAME] = options.target\n }\n\n headers[REMOTE_ENV_HEADER_NAME] = typeof document === 'undefined' ? 'SSR' : 'CSR'\n\n // NOTE: CSR\n if (typeof document !== 'undefined' && document.cookie) {\n headers[REMOTE_CLIENT_HEADER_NAME] = getClientSideFingerprint()\n } else if (headers[COOKIE_HEADER_NAME]) {\n const ssrCookies = parseCookieString(headers[COOKIE_HEADER_NAME])\n\n headers[REMOTE_CLIENT_HEADER_NAME] = ssrCookies[FINGERPRINT_ID_COOKIE_NAME] || SSR_DEFAULT_FINGERPRINT\n }\n\n return headers\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 === 'undefined' || !('BroadcastChannel' in window)) return\n\n const bc = new BroadcastChannel('embeddingContext')\n setBCChannel(bc)\n\n return () => {\n bc.close()\n setBCChannel(null)\n }\n }, [])\n\n // NOTE: Embedding context is shared between tabs by browser technology (cookies)\n // so each new page can obtain it in initial props in SSR and CSR\n // The problem is cookie might not be cleaned up on tab close, so we use 2 tricks:\n // 1. save primary tab status in session storage, so it won't be lost on user navigation\n // 2. use BroadcastChannel to poll if primary tab is still alive, if not, clean up cookie\n useEffect(() => {\n if (isPrimaryTab === null || !bcChannel) return\n\n if (isPrimaryTab) {\n const primaryListener = (e: MessageEvent) => {\n const { success, data } = IS_PRIMARY_ALIVE_MESSAGE_SCHEMA.safeParse(e.data)\n if (!success || !data?.data?.requestId) return\n\n const response: IsPrimaryAliveResponse = {\n type: 'EmbeddingContextPrimaryPollingResult',\n data: {\n isPrimary: true,\n requestId: data.data.requestId,\n },\n }\n\n bcChannel.postMessage(response)\n }\n\n bcChannel.addEventListener('message', primaryListener)\n\n return () => {\n bcChannel.removeEventListener('message', primaryListener)\n }\n }\n\n const requestId = generateUUIDv4()\n const timeout = setTimeout(() => {\n deleteCookie(EMBEDDING_CONTEXT_COOKIE_NAME)\n setEmbeddingContext(null)\n setIsPrimaryTab(false)\n }, EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS)\n\n const secondaryListener = (e: MessageEvent) => {\n const { success, data } = IS_PRIMARY_ALIVE_RESPONSE_SCHEMA.safeParse(e.data)\n if (!success || data?.data.requestId !== requestId) return\n\n clearTimeout(timeout)\n }\n\n bcChannel.addEventListener('message', secondaryListener)\n\n const pollMessage: IsPrimaryAliveMessage = {\n type: 'EmbeddingContextPrimaryPolling',\n data: {\n requestId,\n },\n }\n bcChannel.postMessage(pollMessage)\n\n return () => {\n bcChannel.removeEventListener('message', secondaryListener)\n clearTimeout(timeout)\n }\n\n }, [bcChannel, isPrimaryTab])\n\n return (\n <ReactEmbeddingContext.Provider value={embeddingContext}>\n <App {...props} />\n </ReactEmbeddingContext.Provider>\n )\n }\n\n const appGetInitialProps = App.getInitialProps\n if (appGetInitialProps) {\n WithEmbeddingContext.getInitialProps = async function (context) {\n const appProps = await appGetInitialProps(context)\n const { ctx } = context\n const embeddingContextWithSource = getEmbeddingContext(ctx.req, ctx.res)\n if (embeddingContextWithSource && embeddingContextWithSource.source === 'query') {\n // Save context in cookie for new tabs\n setCookie(EMBEDDING_CONTEXT_COOKIE_NAME, contextToB64(embeddingContextWithSource.ctx), {\n req: ctx.req,\n res: ctx.res,\n })\n }\n\n return {\n ...appProps,\n pageProps: {\n ...appProps.pageProps,\n [EMBEDDING_CONTEXT_PROP_NAME]: embeddingContextWithSource,\n },\n }\n }\n }\n\n return WithEmbeddingContext\n}","import { randomBytes } from 'crypto'\n\n/**\n * Generates v4 UUIDs in both browser and Node environments\n * @example\n * const uuid = generateUUIDv4()\n */\nexport function generateUUIDv4 (): string {\n let randomValues: Uint8Array\n\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n // Browser or Node.js (if Node 19+ supports crypto.randomUUID)\n return crypto.randomUUID()\n } else if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {\n // Browser environment\n randomValues = new Uint8Array(16)\n window.crypto.getRandomValues(randomValues)\n } else {\n // Node.js environment\n randomValues = randomBytes(16)\n }\n\n // Setting the version (4) and variant (RFC4122)\n randomValues[6] = (randomValues[6] & 0x0f) | 0x40 // version 4\n randomValues[8] = (randomValues[8] & 0x3f) | 0x80 // variant\n\n return [...randomValues]\n .map((value, index) => {\n const hex = value.toString(16).padStart(2, '0')\n if (index === 4 || index === 6 || index === 8 || index === 10) {\n return `-${hex}`\n }\n return hex\n })\n .join('')\n}\n"],"mappings":"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","names":["getCookie","setCookie","getCookie","setCookie"]}