diff --git a/README.md b/README.md
index e3654d1..ff03381 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,26 @@ cd test && ./serve.sh
Open `http://localhost:8000/test/index.html` to test all embed types.
+## Custom Domains
+
+Add `data-custom-domain` to the Surface tag when an environment uses a verified
+custom domain:
+
+```html
+
+```
+
+The tag sends lead identification, journey tracking, external-form events, and
+open-trigger requests to `https://demo.example.com/api/v1`. It also trusts form
+iframe messages from `https://demo.example.com`. The value must be an HTTPS
+hostname or origin without a path, query string, credentials, or fragment. If
+the attribute is absent or invalid, the tag continues to use
+`https://forms.withsurface.com`.
+
## Embedding Types
- **Popup** -- modal overlay triggered by button click
diff --git a/src/conversions/conversion-listener.ts b/src/conversions/conversion-listener.ts
index 69723da..0f027a9 100644
--- a/src/conversions/conversion-listener.ts
+++ b/src/conversions/conversion-listener.ts
@@ -35,7 +35,7 @@ const isConversionMessage = (data: any): data is ConversionMessage =>
// Handles a `surface:conversion` message from a Surface form iframe: fires the
// pixel in this (parent) page, then acks so the iframe knows not to fall back to
// in-frame firing. The caller guarantees the origin is already trusted (checked
-// in the shared message listener against SURFACE_DOMAINS).
+// in the shared message listener against the runtime Surface-domain allowlist).
export const handleConversionMessage = (event: MessageEvent, log: Logger): void => {
const data = event.data;
if (!isConversionMessage(data)) return;
diff --git a/src/external-form/external-form.ts b/src/external-form/external-form.ts
index b9eb68a..4178ea0 100644
--- a/src/external-form/external-form.ts
+++ b/src/external-form/external-form.ts
@@ -1,9 +1,9 @@
-import { EXTERNAL_FORM_API } from "../constants";
import { isDebugMode } from "../utils/debug";
import { sendBeacon } from "../utils/beacon";
import { getSiteIdFromScript } from "../lead/site-id";
import { attachFormHandlers } from "./form-handlers";
import type { ExternalFormProps } from "../types";
+import { getSurfaceRuntimeConfig } from "../runtime-config";
export class SurfaceExternalForm {
initialRenderTime: Date;
@@ -27,7 +27,7 @@ export class SurfaceExternalForm {
this.formStarted = {};
this.config = {
- serverBaseUrl: props?.serverBaseUrl || EXTERNAL_FORM_API,
+ serverBaseUrl: props?.serverBaseUrl || getSurfaceRuntimeConfig().apiBaseUrl,
debugMode: isDebugMode(),
};
diff --git a/src/index.ts b/src/index.ts
index 8e58896..c330749 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,13 +10,15 @@ import { SurfaceExternalForm } from "./external-form/external-form";
import { SurfaceEmbed } from "./embed/embed";
import { resolveOpenTriggersOnLoad } from "./open-triggers/open-triggers";
import { initReview } from "./review/review";
+import { initializeSurfaceRuntimeConfig } from "./runtime-config";
const scriptTag = document.currentScript as HTMLScriptElement;
+const runtimeConfig = initializeSurfaceRuntimeConfig(scriptTag);
const environmentId = getSiteIdFromScript(scriptTag);
setEnvironmentId(environmentId);
// Create singleton store
-const SurfaceTagStore = new SurfaceStore(environmentId);
+const SurfaceTagStore = new SurfaceStore(environmentId, runtimeConfig);
// Expose public API on window (backwards compatible)
const w = window as unknown as Record;
@@ -30,7 +32,7 @@ w.SurfaceGetSiteIdFromScript = getSiteIdFromScript;
// Auto-open a form when the host URL carries a configured `?=true` param.
// Fire-and-forget; only touches the network when params are present.
-void resolveOpenTriggersOnLoad(environmentId);
+void resolveOpenTriggersOnLoad(environmentId, runtimeConfig);
// Surface CMS review bridge. Inert unless the page is loaded inside the CMS
// review iframe (?surface_review= token) — adds no listeners otherwise.
diff --git a/src/lead/identify.ts b/src/lead/identify.ts
index 9ba1aa2..b2b4c28 100644
--- a/src/lead/identify.ts
+++ b/src/lead/identify.ts
@@ -1,6 +1,10 @@
-import { LEAD_DATA_TTL, LEAD_IDENTIFY_API } from "../constants";
+import { LEAD_DATA_TTL } from "../constants";
import { getBrowserFingerprint } from "./fingerprint";
import type { LeadData } from "../types";
+import {
+ getSurfaceRuntimeConfig,
+ type SurfaceRuntimeConfig,
+} from "../runtime-config";
let environmentId: string | null = null;
let identifyInProgress = false;
@@ -49,7 +53,8 @@ export function getLeadDataWithTTL(): LeadData | null {
}
export async function identifyLead(
- envId: string
+ envId: string,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
): Promise {
if (identifyInProgress) {
return waitForCachedData();
@@ -66,7 +71,7 @@ export async function identifyLead(
const fingerprint = await getBrowserFingerprint(envId);
const parentUrl = new URL(window.location.href);
- const response = await fetch(LEAD_IDENTIFY_API, {
+ const response = await fetch(config.leadIdentifyApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
diff --git a/src/open-triggers/open-triggers.ts b/src/open-triggers/open-triggers.ts
index de3f800..3bf620a 100644
--- a/src/open-triggers/open-triggers.ts
+++ b/src/open-triggers/open-triggers.ts
@@ -1,7 +1,10 @@
-import { EXTERNAL_FORM_API } from "../constants";
import { SurfaceEmbed } from "../embed/embed";
import { openTriggerOverlay } from "./open-trigger-overlay";
import { OpenTriggerEntry, OpenTriggersMap, pickOpenTrigger } from "./resolve";
+import {
+ getSurfaceRuntimeConfig,
+ type SurfaceRuntimeConfig,
+} from "../runtime-config";
const SESSION_PREFIX = "surface_open_triggers:";
// Self-healing cache: re-fetch the map after this long so a slug retargeted/disabled
@@ -30,12 +33,15 @@ interface OverridableWindow {
* present as `?=true`. Opens a form even if it isn't already embedded on the
* page. No params → zero network. Always fails safe (never breaks the host page).
*/
-export async function resolveOpenTriggersOnLoad(environmentId: string | null): Promise {
+export async function resolveOpenTriggersOnLoad(
+ environmentId: string | null,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
+): Promise {
try {
if (!environmentId) return;
if (!window.location.search) return;
- const map = await fetchOpenTriggersMap(environmentId);
+ const map = await fetchOpenTriggersMap(environmentId, config);
const entry = pickOpenTrigger(window.location.search, map);
if (!entry) return;
@@ -45,13 +51,16 @@ export async function resolveOpenTriggersOnLoad(environmentId: string | null): P
}
}
-async function fetchOpenTriggersMap(environmentId: string): Promise {
+async function fetchOpenTriggersMap(
+ environmentId: string,
+ config: SurfaceRuntimeConfig
+): Promise {
const w = window as unknown as OverridableWindow;
// Test/escape hatch: a directly-injected map bypasses the network entirely.
if (w.__SURFACE_OPEN_TRIGGERS_MAP) return w.__SURFACE_OPEN_TRIGGERS_MAP;
- const sessionKey = SESSION_PREFIX + environmentId;
+ const sessionKey = `${SESSION_PREFIX}${config.apiBaseUrl}:${environmentId}`;
try {
const cached = sessionStorage.getItem(sessionKey);
if (cached) {
@@ -64,7 +73,7 @@ async function fetchOpenTriggersMap(environmentId: string): Promise {
- if (!event.origin || !(SURFACE_DOMAINS as readonly string[]).includes(event.origin)) {
+ const surfaceDomains = store.surfaceDomains ?? SURFACE_DOMAINS;
+ if (!event.origin || !surfaceDomains.includes(event.origin)) {
return;
}
@@ -19,7 +20,10 @@ export function initializeMessageListener(store: SurfaceStore): void {
const envId = getEnvironmentId();
if (envId) {
- identifyLead(envId)
+ const identify = store.config?.customOrigin
+ ? identifyLead(envId, store.config)
+ : identifyLead(envId);
+ identify
.then(() => store.sendPayloadToIframes("LEAD_DATA_UPDATE"))
.catch((e) => console.log("Failed identify", e));
} else {
diff --git a/src/store/store.ts b/src/store/store.ts
index acfe034..c8f7c9f 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -1,4 +1,4 @@
-import { SURFACE_DOMAINS, VALID_EMBED_TYPES } from "../constants";
+import { VALID_EMBED_TYPES } from "../constants";
import { isDebugMode } from "../utils/debug";
import { createLogger } from "../utils/logger";
import { parseCookies } from "../utils/cookies";
@@ -12,6 +12,10 @@ import {
clearUserJourney as clearJourney,
} from "./user-journey";
import type { Logger, StorePayload, PartialFilledData, LeadData } from "../types";
+import {
+ getSurfaceRuntimeConfig,
+ type SurfaceRuntimeConfig,
+} from "../runtime-config";
export class SurfaceStore {
windowUrl: string;
@@ -28,9 +32,13 @@ export class SurfaceStore {
userJourney: unknown[];
cachedIdentifyData: LeadData | null;
environmentId: string | null;
+ config: SurfaceRuntimeConfig;
log: Logger;
- constructor(environmentId: string | null = null) {
+ constructor(
+ environmentId: string | null = null,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
+ ) {
this.windowUrl = new URL(window.location.href).toString();
this.origin = new URL(window.location.href).origin.toString();
this.referrer = document.referrer || "";
@@ -40,7 +48,8 @@ export class SurfaceStore {
this.partialFilledData = {};
this.validEmbedTypes = VALID_EMBED_TYPES;
this.debugMode = isDebugMode();
- this.surfaceDomains = SURFACE_DOMAINS;
+ this.config = config;
+ this.surfaceDomains = config.surfaceDomains;
this.userJourneyId = null;
this.userJourney = [];
this.cachedIdentifyData = getLeadDataWithTTL();
@@ -63,7 +72,8 @@ export class SurfaceStore {
// The journey id resolves async — iframes that already received a
// STORE_UPDATE need a refresh to stitch this pageview.
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.setupRouteChangeDetection();
}
@@ -78,7 +88,10 @@ export class SurfaceStore {
if (!this.hasSurfaceIframe()) return;
this.sendPayloadToIframes("STORE_UPDATE");
if (this.environmentId) {
- identifyLead(this.environmentId)
+ const identify = this.config.customOrigin
+ ? identifyLead(this.environmentId, this.config)
+ : identifyLead(this.environmentId);
+ identify
.then(() => this.sendPayloadToIframes("LEAD_DATA_UPDATE"))
.catch((e) => this.log.error({ message: "Initial identify failed", error: e }));
} else if (getLeadDataWithTTL()) {
@@ -95,14 +108,18 @@ export class SurfaceStore {
}
private hasSurfaceIframe(): boolean {
- return Array.from(document.querySelectorAll("iframe")).some((iframe) =>
- SURFACE_DOMAINS.some((domain) => iframe.src.includes(domain))
- );
+ return Array.from(document.querySelectorAll("iframe")).some((iframe) => {
+ try {
+ return this.surfaceDomains.includes(new URL(iframe.src).origin);
+ } catch {
+ return false;
+ }
+ });
}
private isCurrentOriginSurfaceDomain(): boolean {
- const hostname = window.location?.hostname ?? "";
- return SURFACE_DOMAINS.some((url) => new URL(url).hostname === hostname);
+ const origin = window.location?.origin ?? "";
+ return this.surfaceDomains.includes(origin);
}
private setupRouteChangeDetection(): void {
@@ -120,7 +137,8 @@ export class SurfaceStore {
// A journey created/refreshed during the route change resolves after
// the push below — refresh iframes so they get the new id.
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.sendPayloadToIframes("STORE_UPDATE");
@@ -145,14 +163,17 @@ export class SurfaceStore {
const target = iframe || document.querySelector("#surface-iframe");
if (!target) return;
- SURFACE_DOMAINS.forEach((domain) => {
- if (target.src.includes(domain)) {
- target.contentWindow?.postMessage(
- { type, payload: this.getPayload(), sender: "surface_tag" },
- domain
- );
- }
- });
+ try {
+ const targetOrigin = new URL(target.src).origin;
+ if (!this.surfaceDomains.includes(targetOrigin)) return;
+
+ target.contentWindow?.postMessage(
+ { type, payload: this.getPayload(), sender: "surface_tag" },
+ targetOrigin
+ );
+ } catch {
+ // Ignore invalid iframe URLs.
+ }
}
getUrlParams(): Record {
diff --git a/src/store/user-journey.ts b/src/store/user-journey.ts
index 874c206..874590b 100644
--- a/src/store/user-journey.ts
+++ b/src/store/user-journey.ts
@@ -1,7 +1,6 @@
import {
SURFACE_USER_JOURNEY_COOKIE_NAME,
SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME,
- USER_JOURNEY_TRACKING_API,
RECENT_VISIT_COOKIE_MAX_AGE,
} from "../constants";
import { setCookie, getCookie, deleteCookie } from "../utils/cookies";
@@ -12,6 +11,10 @@ import {
getExistingJourneyId,
} from "./journey-cookies";
import type { LeadData, Logger, JourneyTrackEvent } from "../types";
+import {
+ getSurfaceRuntimeConfig,
+ type SurfaceRuntimeConfig,
+} from "../runtime-config";
function getBrowserReferrer(): string {
return typeof document === "undefined" ? "" : document.referrer || "";
@@ -52,7 +55,8 @@ export function initializeUserJourneyTracking(
environmentId: string | null,
log: Logger,
getJourneyId: () => string | null,
- setJourneyId: (id: string | null) => void
+ setJourneyId: (id: string | null) => void,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
): void {
try {
if (typeof window === "undefined") return;
@@ -73,7 +77,8 @@ export function initializeUserJourneyTracking(
createPageViewEvent(currentUrl, environmentId),
log,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl, {
@@ -92,7 +97,8 @@ export async function trackToRedis(
event: JourneyTrackEvent,
log: Logger,
getJourneyId: () => string | null,
- setJourneyId: (id: string | null) => void
+ setJourneyId: (id: string | null) => void,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
): Promise | null> {
try {
const journeyId = getJourneyId();
@@ -105,7 +111,7 @@ export async function trackToRedis(
const blob = new Blob([JSON.stringify(payload)], {
type: "application/json",
});
- const sent = navigator.sendBeacon(USER_JOURNEY_TRACKING_API, blob);
+ const sent = navigator.sendBeacon(config.userJourneyTrackingApi, blob);
if (sent) {
refreshJourneyCookie(journeyId);
log.info({ message: "Tracking sent via sendBeacon", response: { sent } });
@@ -114,7 +120,7 @@ export async function trackToRedis(
log.warn({ message: "sendBeacon failed, falling back to fetch" });
}
- const response = await fetch(USER_JOURNEY_TRACKING_API, {
+ const response = await fetch(config.userJourneyTrackingApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
@@ -145,7 +151,8 @@ export function updateUserJourneyOnRouteChange(
newUrl: string,
log: Logger,
getJourneyId: () => string | null,
- setJourneyId: (id: string | null) => void
+ setJourneyId: (id: string | null) => void,
+ config: SurfaceRuntimeConfig = getSurfaceRuntimeConfig()
): void {
try {
if (typeof window === "undefined") return;
@@ -162,7 +169,8 @@ export function updateUserJourneyOnRouteChange(
createPageViewEvent(currentUrl, environmentId),
log,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl, {
diff --git a/surface_embed_v1.js b/surface_embed_v1.js
index 3255785..076e3ac 100644
--- a/surface_embed_v1.js
+++ b/surface_embed_v1.js
@@ -71,6 +71,51 @@
return { ...fingerprint, id };
}
+ // src/runtime-config.ts
+ var CUSTOM_DOMAIN_ATTRIBUTE = "data-custom-domain";
+ var DEFAULT_SURFACE_RUNTIME_CONFIG = {
+ apiBaseUrl: EXTERNAL_FORM_API,
+ leadIdentifyApi: LEAD_IDENTIFY_API,
+ userJourneyTrackingApi: USER_JOURNEY_TRACKING_API,
+ surfaceDomains: SURFACE_DOMAINS,
+ customOrigin: null
+ };
+ var runtimeConfig = DEFAULT_SURFACE_RUNTIME_CONFIG;
+ function normalizeCustomOrigin(value) {
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+ try {
+ const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
+ return null;
+ }
+ return url.origin;
+ } catch {
+ return null;
+ }
+ }
+ function resolveSurfaceRuntimeConfig(scriptElement) {
+ const customOrigin = normalizeCustomOrigin(
+ scriptElement?.getAttribute(CUSTOM_DOMAIN_ATTRIBUTE) ?? ""
+ );
+ if (!customOrigin) return DEFAULT_SURFACE_RUNTIME_CONFIG;
+ const apiBaseUrl = `${customOrigin}/api/v1`;
+ return {
+ apiBaseUrl,
+ leadIdentifyApi: `${apiBaseUrl}/lead/identify`,
+ userJourneyTrackingApi: `${apiBaseUrl}/lead/track`,
+ surfaceDomains: Array.from(/* @__PURE__ */ new Set([...SURFACE_DOMAINS, customOrigin])),
+ customOrigin
+ };
+ }
+ function initializeSurfaceRuntimeConfig(scriptElement) {
+ runtimeConfig = resolveSurfaceRuntimeConfig(scriptElement);
+ return runtimeConfig;
+ }
+ function getSurfaceRuntimeConfig() {
+ return runtimeConfig;
+ }
+
// src/lead/identify.ts
var environmentId = null;
var identifyInProgress = false;
@@ -111,7 +156,7 @@
return null;
}
}
- async function identifyLead(envId) {
+ async function identifyLead(envId, config = getSurfaceRuntimeConfig()) {
if (identifyInProgress) {
return waitForCachedData();
}
@@ -123,7 +168,7 @@
try {
const fingerprint = await getBrowserFingerprint(envId);
const parentUrl = new URL(window.location.href);
- const response = await fetch(LEAD_IDENTIFY_API, {
+ const response = await fetch(config.leadIdentifyApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -441,7 +486,8 @@
// src/store/message-listener.ts
function initializeMessageListener(store) {
const handleMessage = (event) => {
- if (!event.origin || !SURFACE_DOMAINS.includes(event.origin)) {
+ const surfaceDomains = store.surfaceDomains ?? SURFACE_DOMAINS;
+ if (!event.origin || !surfaceDomains.includes(event.origin)) {
return;
}
if (event.data?.type === "surface:conversion") {
@@ -452,7 +498,8 @@
store.sendPayloadToIframes("STORE_UPDATE");
const envId = getEnvironmentId();
if (envId) {
- identifyLead(envId).then(() => store.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => console.log("Failed identify", e));
+ const identify = store.config?.customOrigin ? identifyLead(envId, store.config) : identifyLead(envId);
+ identify.then(() => store.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => console.log("Failed identify", e));
} else {
store.sendPayloadToIframes("LEAD_DATA_UPDATE");
}
@@ -514,7 +561,7 @@
}
};
}
- function initializeUserJourneyTracking(environmentId3, log2, getJourneyId, setJourneyId) {
+ function initializeUserJourneyTracking(environmentId3, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
if (typeof window === "undefined") return;
const existingId = getExistingJourneyId();
@@ -530,7 +577,8 @@
createPageViewEvent(currentUrl2, environmentId3),
log2,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl2, {
maxAge: RECENT_VISIT_COOKIE_MAX_AGE,
@@ -542,7 +590,7 @@
log2.error({ message: "Error initializing user journey tracking", error });
}
}
- async function trackToRedis(event, log2, getJourneyId, setJourneyId) {
+ async function trackToRedis(event, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
const journeyId = getJourneyId();
const payload = { ...event };
@@ -552,7 +600,7 @@
const blob = new Blob([JSON.stringify(payload)], {
type: "application/json"
});
- const sent = navigator.sendBeacon(USER_JOURNEY_TRACKING_API, blob);
+ const sent = navigator.sendBeacon(config.userJourneyTrackingApi, blob);
if (sent) {
refreshJourneyCookie(journeyId);
log2.info({ message: "Tracking sent via sendBeacon", response: { sent } });
@@ -560,7 +608,7 @@
}
log2.warn({ message: "sendBeacon failed, falling back to fetch" });
}
- const response = await fetch(USER_JOURNEY_TRACKING_API, {
+ const response = await fetch(config.userJourneyTrackingApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
@@ -581,7 +629,7 @@
return null;
}
}
- function updateUserJourneyOnRouteChange(environmentId3, newUrl, log2, getJourneyId, setJourneyId) {
+ function updateUserJourneyOnRouteChange(environmentId3, newUrl, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
if (typeof window === "undefined") return;
const currentUrl2 = newUrl || window.location.href;
@@ -594,7 +642,8 @@
createPageViewEvent(currentUrl2, environmentId3),
log2,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl2, {
maxAge: RECENT_VISIT_COOKIE_MAX_AGE,
@@ -616,7 +665,7 @@
// src/store/store.ts
var SurfaceStore = class {
- constructor(environmentId3 = null) {
+ constructor(environmentId3 = null, config = getSurfaceRuntimeConfig()) {
this.windowUrl = new URL(window.location.href).toString();
this.origin = new URL(window.location.href).origin.toString();
this.referrer = document.referrer || "";
@@ -626,7 +675,8 @@
this.partialFilledData = {};
this.validEmbedTypes = VALID_EMBED_TYPES;
this.debugMode = isDebugMode();
- this.surfaceDomains = SURFACE_DOMAINS;
+ this.config = config;
+ this.surfaceDomains = config.surfaceDomains;
this.userJourneyId = null;
this.userJourney = [];
this.cachedIdentifyData = getLeadDataWithTTL();
@@ -642,7 +692,8 @@
const resolved = !!id && id !== this.userJourneyId;
this.userJourneyId = id;
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.setupRouteChangeDetection();
}
@@ -650,7 +701,8 @@
if (!this.hasSurfaceIframe()) return;
this.sendPayloadToIframes("STORE_UPDATE");
if (this.environmentId) {
- identifyLead(this.environmentId).then(() => this.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => this.log.error({ message: "Initial identify failed", error: e }));
+ const identify = this.config.customOrigin ? identifyLead(this.environmentId, this.config) : identifyLead(this.environmentId);
+ identify.then(() => this.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => this.log.error({ message: "Initial identify failed", error: e }));
} else if (getLeadDataWithTTL()) {
this.sendPayloadToIframes("LEAD_DATA_UPDATE");
}
@@ -662,13 +714,17 @@
}
}
hasSurfaceIframe() {
- return Array.from(document.querySelectorAll("iframe")).some(
- (iframe) => SURFACE_DOMAINS.some((domain) => iframe.src.includes(domain))
- );
+ return Array.from(document.querySelectorAll("iframe")).some((iframe) => {
+ try {
+ return this.surfaceDomains.includes(new URL(iframe.src).origin);
+ } catch {
+ return false;
+ }
+ });
}
isCurrentOriginSurfaceDomain() {
- const hostname = window.location?.hostname ?? "";
- return SURFACE_DOMAINS.some((url) => new URL(url).hostname === hostname);
+ const origin = window.location?.origin ?? "";
+ return this.surfaceDomains.includes(origin);
}
setupRouteChangeDetection() {
onRouteChange((newUrl) => {
@@ -682,7 +738,8 @@
const resolved = !!id && id !== this.userJourneyId;
this.userJourneyId = id;
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.sendPayloadToIframes("STORE_UPDATE");
this.log.info({ message: "Route changed, updated journey", response: { url: newUrl } });
@@ -699,14 +756,15 @@
notifyIframe(iframe, type) {
const target = iframe || document.querySelector("#surface-iframe");
if (!target) return;
- SURFACE_DOMAINS.forEach((domain) => {
- if (target.src.includes(domain)) {
- target.contentWindow?.postMessage(
- { type, payload: this.getPayload(), sender: "surface_tag" },
- domain
- );
- }
- });
+ try {
+ const targetOrigin = new URL(target.src).origin;
+ if (!this.surfaceDomains.includes(targetOrigin)) return;
+ target.contentWindow?.postMessage(
+ { type, payload: this.getPayload(), sender: "surface_tag" },
+ targetOrigin
+ );
+ } catch {
+ }
}
getUrlParams() {
return getUrlParams();
@@ -815,7 +873,7 @@
this.formInitializationStatus = {};
this.formStarted = {};
this.config = {
- serverBaseUrl: props?.serverBaseUrl || EXTERNAL_FORM_API,
+ serverBaseUrl: props?.serverBaseUrl || getSurfaceRuntimeConfig().apiBaseUrl,
debugMode: isDebugMode()
};
this.environmentId = props?.siteId || getSiteIdFromScript(document.currentScript);
@@ -2134,21 +2192,21 @@
var CACHE_TTL_MS = 5 * 60 * 1e3;
var REUSE_POLL_INTERVAL_MS = 150;
var REUSE_POLL_MAX_TRIES = 12;
- async function resolveOpenTriggersOnLoad(environmentId3) {
+ async function resolveOpenTriggersOnLoad(environmentId3, config = getSurfaceRuntimeConfig()) {
try {
if (!environmentId3) return;
if (!window.location.search) return;
- const map = await fetchOpenTriggersMap(environmentId3);
+ const map = await fetchOpenTriggersMap(environmentId3, config);
const entry = pickOpenTrigger(window.location.search, map);
if (!entry) return;
openTriggerForm(entry);
} catch {
}
}
- async function fetchOpenTriggersMap(environmentId3) {
+ async function fetchOpenTriggersMap(environmentId3, config) {
const w3 = window;
if (w3.__SURFACE_OPEN_TRIGGERS_MAP) return w3.__SURFACE_OPEN_TRIGGERS_MAP;
- const sessionKey = SESSION_PREFIX + environmentId3;
+ const sessionKey = `${SESSION_PREFIX}${config.apiBaseUrl}:${environmentId3}`;
try {
const cached2 = sessionStorage.getItem(sessionKey);
if (cached2) {
@@ -2159,7 +2217,7 @@
}
} catch {
}
- const base = w3.__SURFACE_OPEN_TRIGGERS_BASE || EXTERNAL_FORM_API;
+ const base = w3.__SURFACE_OPEN_TRIGGERS_BASE || config.apiBaseUrl;
const response = await fetch(`${base}/environments/${encodeURIComponent(environmentId3)}/open-triggers`);
if (!response.ok) return null;
const json = await response.json();
@@ -2403,9 +2461,10 @@
// src/index.ts
var scriptTag = document.currentScript;
+ var runtimeConfig2 = initializeSurfaceRuntimeConfig(scriptTag);
var environmentId2 = getSiteIdFromScript(scriptTag);
setEnvironmentId(environmentId2);
- var SurfaceTagStore = new SurfaceStore(environmentId2);
+ var SurfaceTagStore = new SurfaceStore(environmentId2, runtimeConfig2);
var w2 = window;
w2.SurfaceEmbed = SurfaceEmbed;
w2.SurfaceExternalForm = SurfaceExternalForm;
@@ -2414,6 +2473,6 @@
w2.SurfaceSetLeadDataWithTTL = setLeadDataWithTTL;
w2.SurfaceGetLeadDataWithTTL = getLeadDataWithTTL;
w2.SurfaceGetSiteIdFromScript = getSiteIdFromScript;
- void resolveOpenTriggersOnLoad(environmentId2);
+ void resolveOpenTriggersOnLoad(environmentId2, runtimeConfig2);
initReview();
})();
diff --git a/surface_tag.js b/surface_tag.js
index 3255785..076e3ac 100644
--- a/surface_tag.js
+++ b/surface_tag.js
@@ -71,6 +71,51 @@
return { ...fingerprint, id };
}
+ // src/runtime-config.ts
+ var CUSTOM_DOMAIN_ATTRIBUTE = "data-custom-domain";
+ var DEFAULT_SURFACE_RUNTIME_CONFIG = {
+ apiBaseUrl: EXTERNAL_FORM_API,
+ leadIdentifyApi: LEAD_IDENTIFY_API,
+ userJourneyTrackingApi: USER_JOURNEY_TRACKING_API,
+ surfaceDomains: SURFACE_DOMAINS,
+ customOrigin: null
+ };
+ var runtimeConfig = DEFAULT_SURFACE_RUNTIME_CONFIG;
+ function normalizeCustomOrigin(value) {
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+ try {
+ const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
+ return null;
+ }
+ return url.origin;
+ } catch {
+ return null;
+ }
+ }
+ function resolveSurfaceRuntimeConfig(scriptElement) {
+ const customOrigin = normalizeCustomOrigin(
+ scriptElement?.getAttribute(CUSTOM_DOMAIN_ATTRIBUTE) ?? ""
+ );
+ if (!customOrigin) return DEFAULT_SURFACE_RUNTIME_CONFIG;
+ const apiBaseUrl = `${customOrigin}/api/v1`;
+ return {
+ apiBaseUrl,
+ leadIdentifyApi: `${apiBaseUrl}/lead/identify`,
+ userJourneyTrackingApi: `${apiBaseUrl}/lead/track`,
+ surfaceDomains: Array.from(/* @__PURE__ */ new Set([...SURFACE_DOMAINS, customOrigin])),
+ customOrigin
+ };
+ }
+ function initializeSurfaceRuntimeConfig(scriptElement) {
+ runtimeConfig = resolveSurfaceRuntimeConfig(scriptElement);
+ return runtimeConfig;
+ }
+ function getSurfaceRuntimeConfig() {
+ return runtimeConfig;
+ }
+
// src/lead/identify.ts
var environmentId = null;
var identifyInProgress = false;
@@ -111,7 +156,7 @@
return null;
}
}
- async function identifyLead(envId) {
+ async function identifyLead(envId, config = getSurfaceRuntimeConfig()) {
if (identifyInProgress) {
return waitForCachedData();
}
@@ -123,7 +168,7 @@
try {
const fingerprint = await getBrowserFingerprint(envId);
const parentUrl = new URL(window.location.href);
- const response = await fetch(LEAD_IDENTIFY_API, {
+ const response = await fetch(config.leadIdentifyApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -441,7 +486,8 @@
// src/store/message-listener.ts
function initializeMessageListener(store) {
const handleMessage = (event) => {
- if (!event.origin || !SURFACE_DOMAINS.includes(event.origin)) {
+ const surfaceDomains = store.surfaceDomains ?? SURFACE_DOMAINS;
+ if (!event.origin || !surfaceDomains.includes(event.origin)) {
return;
}
if (event.data?.type === "surface:conversion") {
@@ -452,7 +498,8 @@
store.sendPayloadToIframes("STORE_UPDATE");
const envId = getEnvironmentId();
if (envId) {
- identifyLead(envId).then(() => store.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => console.log("Failed identify", e));
+ const identify = store.config?.customOrigin ? identifyLead(envId, store.config) : identifyLead(envId);
+ identify.then(() => store.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => console.log("Failed identify", e));
} else {
store.sendPayloadToIframes("LEAD_DATA_UPDATE");
}
@@ -514,7 +561,7 @@
}
};
}
- function initializeUserJourneyTracking(environmentId3, log2, getJourneyId, setJourneyId) {
+ function initializeUserJourneyTracking(environmentId3, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
if (typeof window === "undefined") return;
const existingId = getExistingJourneyId();
@@ -530,7 +577,8 @@
createPageViewEvent(currentUrl2, environmentId3),
log2,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl2, {
maxAge: RECENT_VISIT_COOKIE_MAX_AGE,
@@ -542,7 +590,7 @@
log2.error({ message: "Error initializing user journey tracking", error });
}
}
- async function trackToRedis(event, log2, getJourneyId, setJourneyId) {
+ async function trackToRedis(event, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
const journeyId = getJourneyId();
const payload = { ...event };
@@ -552,7 +600,7 @@
const blob = new Blob([JSON.stringify(payload)], {
type: "application/json"
});
- const sent = navigator.sendBeacon(USER_JOURNEY_TRACKING_API, blob);
+ const sent = navigator.sendBeacon(config.userJourneyTrackingApi, blob);
if (sent) {
refreshJourneyCookie(journeyId);
log2.info({ message: "Tracking sent via sendBeacon", response: { sent } });
@@ -560,7 +608,7 @@
}
log2.warn({ message: "sendBeacon failed, falling back to fetch" });
}
- const response = await fetch(USER_JOURNEY_TRACKING_API, {
+ const response = await fetch(config.userJourneyTrackingApi, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
@@ -581,7 +629,7 @@
return null;
}
}
- function updateUserJourneyOnRouteChange(environmentId3, newUrl, log2, getJourneyId, setJourneyId) {
+ function updateUserJourneyOnRouteChange(environmentId3, newUrl, log2, getJourneyId, setJourneyId, config = getSurfaceRuntimeConfig()) {
try {
if (typeof window === "undefined") return;
const currentUrl2 = newUrl || window.location.href;
@@ -594,7 +642,8 @@
createPageViewEvent(currentUrl2, environmentId3),
log2,
getJourneyId,
- setJourneyId
+ setJourneyId,
+ config
);
setCookie(SURFACE_USER_JOURNEY_RECENT_VISIT_COOKIE_NAME, currentUrl2, {
maxAge: RECENT_VISIT_COOKIE_MAX_AGE,
@@ -616,7 +665,7 @@
// src/store/store.ts
var SurfaceStore = class {
- constructor(environmentId3 = null) {
+ constructor(environmentId3 = null, config = getSurfaceRuntimeConfig()) {
this.windowUrl = new URL(window.location.href).toString();
this.origin = new URL(window.location.href).origin.toString();
this.referrer = document.referrer || "";
@@ -626,7 +675,8 @@
this.partialFilledData = {};
this.validEmbedTypes = VALID_EMBED_TYPES;
this.debugMode = isDebugMode();
- this.surfaceDomains = SURFACE_DOMAINS;
+ this.config = config;
+ this.surfaceDomains = config.surfaceDomains;
this.userJourneyId = null;
this.userJourney = [];
this.cachedIdentifyData = getLeadDataWithTTL();
@@ -642,7 +692,8 @@
const resolved = !!id && id !== this.userJourneyId;
this.userJourneyId = id;
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.setupRouteChangeDetection();
}
@@ -650,7 +701,8 @@
if (!this.hasSurfaceIframe()) return;
this.sendPayloadToIframes("STORE_UPDATE");
if (this.environmentId) {
- identifyLead(this.environmentId).then(() => this.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => this.log.error({ message: "Initial identify failed", error: e }));
+ const identify = this.config.customOrigin ? identifyLead(this.environmentId, this.config) : identifyLead(this.environmentId);
+ identify.then(() => this.sendPayloadToIframes("LEAD_DATA_UPDATE")).catch((e) => this.log.error({ message: "Initial identify failed", error: e }));
} else if (getLeadDataWithTTL()) {
this.sendPayloadToIframes("LEAD_DATA_UPDATE");
}
@@ -662,13 +714,17 @@
}
}
hasSurfaceIframe() {
- return Array.from(document.querySelectorAll("iframe")).some(
- (iframe) => SURFACE_DOMAINS.some((domain) => iframe.src.includes(domain))
- );
+ return Array.from(document.querySelectorAll("iframe")).some((iframe) => {
+ try {
+ return this.surfaceDomains.includes(new URL(iframe.src).origin);
+ } catch {
+ return false;
+ }
+ });
}
isCurrentOriginSurfaceDomain() {
- const hostname = window.location?.hostname ?? "";
- return SURFACE_DOMAINS.some((url) => new URL(url).hostname === hostname);
+ const origin = window.location?.origin ?? "";
+ return this.surfaceDomains.includes(origin);
}
setupRouteChangeDetection() {
onRouteChange((newUrl) => {
@@ -682,7 +738,8 @@
const resolved = !!id && id !== this.userJourneyId;
this.userJourneyId = id;
if (resolved) this.sendPayloadToIframes("STORE_UPDATE");
- }
+ },
+ this.config
);
this.sendPayloadToIframes("STORE_UPDATE");
this.log.info({ message: "Route changed, updated journey", response: { url: newUrl } });
@@ -699,14 +756,15 @@
notifyIframe(iframe, type) {
const target = iframe || document.querySelector("#surface-iframe");
if (!target) return;
- SURFACE_DOMAINS.forEach((domain) => {
- if (target.src.includes(domain)) {
- target.contentWindow?.postMessage(
- { type, payload: this.getPayload(), sender: "surface_tag" },
- domain
- );
- }
- });
+ try {
+ const targetOrigin = new URL(target.src).origin;
+ if (!this.surfaceDomains.includes(targetOrigin)) return;
+ target.contentWindow?.postMessage(
+ { type, payload: this.getPayload(), sender: "surface_tag" },
+ targetOrigin
+ );
+ } catch {
+ }
}
getUrlParams() {
return getUrlParams();
@@ -815,7 +873,7 @@
this.formInitializationStatus = {};
this.formStarted = {};
this.config = {
- serverBaseUrl: props?.serverBaseUrl || EXTERNAL_FORM_API,
+ serverBaseUrl: props?.serverBaseUrl || getSurfaceRuntimeConfig().apiBaseUrl,
debugMode: isDebugMode()
};
this.environmentId = props?.siteId || getSiteIdFromScript(document.currentScript);
@@ -2134,21 +2192,21 @@
var CACHE_TTL_MS = 5 * 60 * 1e3;
var REUSE_POLL_INTERVAL_MS = 150;
var REUSE_POLL_MAX_TRIES = 12;
- async function resolveOpenTriggersOnLoad(environmentId3) {
+ async function resolveOpenTriggersOnLoad(environmentId3, config = getSurfaceRuntimeConfig()) {
try {
if (!environmentId3) return;
if (!window.location.search) return;
- const map = await fetchOpenTriggersMap(environmentId3);
+ const map = await fetchOpenTriggersMap(environmentId3, config);
const entry = pickOpenTrigger(window.location.search, map);
if (!entry) return;
openTriggerForm(entry);
} catch {
}
}
- async function fetchOpenTriggersMap(environmentId3) {
+ async function fetchOpenTriggersMap(environmentId3, config) {
const w3 = window;
if (w3.__SURFACE_OPEN_TRIGGERS_MAP) return w3.__SURFACE_OPEN_TRIGGERS_MAP;
- const sessionKey = SESSION_PREFIX + environmentId3;
+ const sessionKey = `${SESSION_PREFIX}${config.apiBaseUrl}:${environmentId3}`;
try {
const cached2 = sessionStorage.getItem(sessionKey);
if (cached2) {
@@ -2159,7 +2217,7 @@
}
} catch {
}
- const base = w3.__SURFACE_OPEN_TRIGGERS_BASE || EXTERNAL_FORM_API;
+ const base = w3.__SURFACE_OPEN_TRIGGERS_BASE || config.apiBaseUrl;
const response = await fetch(`${base}/environments/${encodeURIComponent(environmentId3)}/open-triggers`);
if (!response.ok) return null;
const json = await response.json();
@@ -2403,9 +2461,10 @@
// src/index.ts
var scriptTag = document.currentScript;
+ var runtimeConfig2 = initializeSurfaceRuntimeConfig(scriptTag);
var environmentId2 = getSiteIdFromScript(scriptTag);
setEnvironmentId(environmentId2);
- var SurfaceTagStore = new SurfaceStore(environmentId2);
+ var SurfaceTagStore = new SurfaceStore(environmentId2, runtimeConfig2);
var w2 = window;
w2.SurfaceEmbed = SurfaceEmbed;
w2.SurfaceExternalForm = SurfaceExternalForm;
@@ -2414,6 +2473,6 @@
w2.SurfaceSetLeadDataWithTTL = setLeadDataWithTTL;
w2.SurfaceGetLeadDataWithTTL = getLeadDataWithTTL;
w2.SurfaceGetSiteIdFromScript = getSiteIdFromScript;
- void resolveOpenTriggersOnLoad(environmentId2);
+ void resolveOpenTriggersOnLoad(environmentId2, runtimeConfig2);
initReview();
})();