/* * Copyright 2024-2026 ReplyPier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The one way the widget talks to the page it is embedded in. * * These messages include the visitor's conversation token, so they are addressed to a * specific origin rather than broadcast with "*": if the embedding page is ever * replaced by another site's document, the browser drops the message instead of * handing that site a live token. * * The widget runs in a srcdoc iframe, which inherits the embedder's origin, so the * host origin is normally readable. When it genuinely cannot be determined we fall * back to "*" - a widget that cannot talk to its loader is broken, and that is worse * than the broadcast we have always done. */ const ANY_ORIGIN = '*' /** Origins a browser reports for an opaque (sandboxed, data:, srcdoc) document. */ const OPAQUE = new Set(['null', 'about:blank', '']) let resolved: string | null = null const usable = (origin: string | undefined | null): string | null => origin && !OPAQUE.has(origin) ? origin : null const originOf = (url: string | undefined | null): string | null => { if (!url) return null try { return usable(new URL(url).origin) } catch { return null } } /** * Origin of the page hosting the widget, or "*" when it cannot be established. * Resolved once: an embedding page navigating between routes keeps its origin. */ export function hostOrigin(): string { if (resolved) return resolved // Not framed at all - the dashboard preview renders the widget inline. if (window.parent === window) { resolved = usable(window.location.origin) || ANY_ORIGIN return resolved } // Same-origin parent: the srcdoc frame the embed loader creates. Reading its // location throws for a cross-origin embed, which the catch below handles. try { const parentOrigin = usable(window.parent.location.origin) if (parentOrigin) { resolved = parentOrigin return resolved } } catch { /* cross-origin parent - fall through */ } // Cross-origin embed: the browser still names our ancestors (not in Firefox), and // the referrer is the embedding document. try { const ancestors = window.location.ancestorOrigins const nearest = ancestors && ancestors.length ? usable(ancestors[0]) : null if (nearest) { resolved = nearest return resolved } } catch { /* not supported - fall through */ } resolved = originOf(document.referrer) || ANY_ORIGIN return resolved } /** Send a message to the embedding page, addressed to its origin. */ export function postToHost(message: Record): void { window.parent.postMessage(message, hostOrigin()) } /** Test seam: forget the cached origin. */ export function resetHostOrigin(): void { resolved = null }