@kadanza/extension-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"extension-sdk.js","names":["#connected","#assertNotDestroyed","#configureAuthTokenAutoRefresh","#scheduleProactiveAuthTokenRefresh","#toHandshakePayload","#handshakePromise","#allowedOrigin","#tenantUrl","#unsubscribe","#onMessage","#pendingHandshake","#clearPending","#clearProactiveAuthTokenRefreshTimer","#destroyed","#authTokenAutoRefresh","#authTokenRefreshRetryUsed","#rejectPending","#pendingAuthTokenRefresh","#pendingPageSettingsUpdate","#loadPageSettingsHandlers","#authTokenRefreshHandlers","#authToken","#extensionDetails","#designTokens","#pageSettings","#assertConnected","#handleHandshakeAck","#handleAuthTokenRefresh","#handleLoadPageSettings","#handlePageSettingsUpdated","#resolvePending","#authTokenBufferMs","#parseExpiresAtMs","#authTokenRefreshTimer","#getProactiveRefreshDelayMs","#runProactiveAuthTokenRefresh","#handleProactiveAuthTokenRefreshFailure","#options","#destroyed","#started","#unsubscribe","#onMessage","#assertNotDestroyed","#post","#handleHandshakeInit","#handleAuthTokenRefresh","#handleUpdatePageSettings","#postPageSettingsUpdated"],"sources":["../src/api.ts","../src/events.ts","../src/messaging.ts","../src/origin.ts","../src/ExtensionSDK.ts","../src/hostMessaging.ts","../src/ExtensionSDKHost.ts","../src/hostUrl.ts","../src/index.ts"],"sourcesContent":["interface ApiRequestContext {\n baseUrl: string;\n tenantDomain: string;\n authTokenJwt: string;\n}\n\n/** Derives the Platform API origin from handshake `baseUrl`. */\nexport function deriveApiUrl(baseUrl: string): string {\n if (baseUrl.startsWith(\"/\")) {\n return baseUrl;\n }\n\n const url = new URL(baseUrl);\n url.hostname = `api.${url.hostname}`;\n return url.origin;\n}\n\nfunction resolveApiEndpoint(baseUrl: string, endpoint: string): string {\n if (!endpoint.startsWith(\"/\") || endpoint.startsWith(\"//\")) {\n throw new Error(\n 'API endpoint must be a root-relative path starting with a single \"/\".',\n );\n }\n\n return `${deriveApiUrl(baseUrl).replace(/\\/+$/, \"\")}${endpoint}`;\n}\n\nexport async function callApi<T>(\n endpoint: string,\n context: ApiRequestContext,\n options: RequestInit = {},\n): Promise<T> {\n const headers = new Headers(options.headers);\n\n if (!headers.has(\"Content-Type\")) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n\n headers.set(\"Authorization\", `Bearer ${context.authTokenJwt}`);\n headers.set(\"X-Tenant\", context.tenantDomain);\n\n const response = await fetch(resolveApiEndpoint(context.baseUrl, endpoint), {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const statusText = response.statusText ? ` ${response.statusText}` : \"\";\n throw new Error(`API call failed: ${response.status}${statusText}.`);\n }\n\n return (await response.json()) as T;\n}\n","/**\n * Wire event type strings for parent/child `postMessage` communication.\n *\n * Keys are SDK-facing names; values are the strings sent on the wire.\n * Comments mark message direction relative to the extension (child).\n */\nexport const CONNECTION_EVENTS = {\n /** Child → parent: start handshake. */\n handshakeInit: \"HANDSHAKE_INIT\",\n /** Parent → child: handshake success with auth and context. */\n handshakeAck: \"HANDSHAKE_ACK\",\n /** Child → parent: request a new auth token. */\n requestAuthTokenRefresh: \"REQUEST_TOKEN_REFRESH\",\n /** Parent → child: new auth token (reply or push). */\n authTokenRefresh: \"TOKEN_REFRESH\",\n /** Parent → child: open/load page settings UI with values. */\n loadPageSettings: \"LOAD_PAGE_SETTINGS\",\n /** Child → parent: persist updated page settings. */\n updatePageSettings: \"UPDATE_PAGE_SETTINGS\",\n /** Parent → child: result of a page settings update. */\n pageSettingsUpdated: \"PAGE_SETTINGS_UPDATED\",\n /** Child → parent: extension route changed. */\n navigationChange: \"NAVIGATION_CHANGE\",\n} as const;\n\n/** Union of all {@link CONNECTION_EVENTS} wire string values. */\nexport type ConnectionEvent =\n (typeof CONNECTION_EVENTS)[keyof typeof CONNECTION_EVENTS];\n","import type { ConnectionEvent } from \"./events\";\nimport type { ExtensionMessage } from \"./types\";\n\nexport type MessageHandler = (event: MessageEvent<ExtensionMessage>) => void;\n\nexport function postToParent(\n type: ConnectionEvent,\n allowedOrigin: string,\n payload?: unknown,\n): void {\n if (!window.parent || window.parent === window) {\n throw new Error(\"Extension is not running inside a parent frame.\");\n }\n\n const message: ExtensionMessage = payload === undefined ? { type } : { type, payload };\n window.parent.postMessage(message, allowedOrigin);\n}\n\nexport function subscribeToParentMessages(\n allowedOrigin: string,\n handler: MessageHandler,\n): () => void {\n const listener = (event: MessageEvent) => {\n if (event.origin !== allowedOrigin) {\n return;\n }\n\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n return;\n }\n\n handler(event as MessageEvent<ExtensionMessage>);\n };\n\n window.addEventListener(\"message\", listener);\n return () => window.removeEventListener(\"message\", listener);\n}\n","const LOCALHOST_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/** Thrown when `tenantUrl` is missing or not a safe postMessage origin. */\nexport class InvalidOriginError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidOriginError\";\n }\n}\n\n/**\n * Parses and validates a tenant URL into a postMessage-safe origin.\n * Allows https always; http only for localhost / 127.0.0.1 / [::1].\n *\n * @param tenantUrl - Absolute URL from the `tenantUrl` search param.\n * @returns Origin string suitable for `postMessage` targetOrigin.\n * @throws {InvalidOriginError} When missing, unparsable, or disallowed scheme/host.\n */\nexport function resolveAllowedOrigin(tenantUrl: string | null): string {\n if (!tenantUrl) {\n throw new InvalidOriginError(\n 'Missing \"tenantUrl\" search parameter required to establish parent origin.',\n );\n }\n\n let url: URL;\n try {\n url = new URL(tenantUrl);\n } catch {\n throw new InvalidOriginError(`Invalid tenantUrl: \"${tenantUrl}\".`);\n }\n\n const isLocalhost = LOCALHOST_HOSTS.has(url.hostname);\n const isHttps = url.protocol === \"https:\";\n const isLocalHttp = url.protocol === \"http:\" && isLocalhost;\n\n if (!isHttps && !isLocalHttp) {\n throw new InvalidOriginError(\n `tenantUrl must use https (or http on localhost). Received: \"${url.protocol}//${url.host}\".`,\n );\n }\n\n return url.origin;\n}\n\n/**\n * Reads the parent-supplied `tenantUrl` from a URL search string.\n *\n * The parent must pass `tenantUrl` on the iframe `src` so\n * {@link resolveAllowedOrigin} can whitelist the parent origin.\n *\n * @param search - Query string (defaults to `window.location.search`).\n */\nexport function readTenantUrlFromLocation(\n search: string = window.location.search,\n): string | null {\n return new URLSearchParams(search).get(\"tenantUrl\");\n}\n","import { callApi, deriveApiUrl } from \"./api\";\nimport { CONNECTION_EVENTS } from \"./events\";\nimport { postToParent, subscribeToParentMessages } from \"./messaging\";\nimport { readTenantUrlFromLocation, resolveAllowedOrigin } from \"./origin\";\nimport type {\n AuthToken,\n AuthTokenRefreshPayload,\n ConnectOptions,\n DesignTokens,\n ExtensionDetails,\n ExtensionMessage,\n HandshakePayload,\n NavigationChangePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n RequestOptions,\n UpdatePageSettingsPayload,\n} from \"./types\";\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_AUTH_TOKEN_BUFFER_MS = 120_000;\nconst AUTO_REFRESH_RETRY_DELAY_MS = 30_000;\n/** Largest delay `setTimeout` can reliably use in browsers. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Public contract for talking to the Kadanza parent frame over `postMessage`.\n *\n * Requires a `tenantUrl` search param on the extension URL so\n * {@link connect} can validate the parent origin.\n */\nexport interface IExtensionSDK {\n /**\n * Establishes the parent connection and waits for `HANDSHAKE_ACK`.\n *\n * Safe to call repeatedly (e.g. from a React `useEffect` with deps):\n * - Already connected: returns the cached handshake and re-applies\n * `authTokenAutoRefresh` / `authTokenBufferMs` without another init.\n * `timeoutMs` is ignored after connect.\n * - Handshake in progress: returns the same in-flight promise. Options from\n * the first connecting call apply until the handshake completes.\n *\n * Opt-in proactive refresh can be enabled with\n * `authTokenAutoRefresh: true` (optional `authTokenBufferMs`).\n *\n * Handshake completes when the parent sends `HANDSHAKE_ACK`. Context fields\n * (`authToken`, `extensionDetails`, `designTokens`, `pageSettings`) are\n * optional — omitted values are `null`. Use them only when the current host\n * surface provides them (e.g. `spaceId` / `pageId` on Experience Pages,\n * `designTokens` when a tenant is in context).\n *\n * @param options - Handshake timeout and optional auth-token auto-refresh.\n * @throws {InvalidOriginError} When `tenantUrl` is missing or invalid.\n * @throws When not embedded in a parent frame, destroyed, or the handshake\n * times out.\n */\n connect(options?: ConnectOptions): Promise<HandshakePayload>;\n\n /**\n * Tears down listeners, proactive refresh timers, and cached state.\n * Rejects any in-flight requests. The instance cannot be reused after destroy.\n * When created via {@link createExtensionSDK}, clears the singleton so a\n * later create call can return a fresh instance.\n */\n destroy(): void;\n\n /** Whether a successful handshake has completed and not been destroyed. */\n readonly isConnected: boolean;\n\n /**\n * Last auth token from handshake or refresh.\n * `null` until connected, or when the host omitted it.\n */\n getAuthToken(): AuthToken | null;\n\n /**\n * Extension context from handshake.\n * `null` until connected, or when the host omitted it.\n */\n getExtensionDetails(): ExtensionDetails | null;\n\n /**\n * Tenant branding from handshake.\n * `null` until connected, or when the host omitted it.\n */\n getDesignTokens(): DesignTokens | null;\n\n /**\n * Latest page settings from handshake or `LOAD_PAGE_SETTINGS`;\n * `null` until set.\n */\n getPageSettings(): PageSettings | null;\n\n /**\n * Validated parent `postMessage` origin from `tenantUrl`;\n * `null` until `connect` resolves it.\n */\n getAllowedOrigin(): string | null;\n\n /**\n * Raw `tenantUrl` search-param value used for the allowed origin;\n * `null` until `connect` reads it.\n */\n getTenantUrl(): string | null;\n\n /**\n * Platform API origin derived from handshake `baseUrl`.\n * `null` until connected, or when `extensionDetails.baseUrl` was omitted.\n */\n getApiUrl(): string | null;\n\n /**\n * Calls the Kadanza Platform API with the current connection credentials.\n *\n * The API origin is derived from the handshake `baseUrl`. Authorization and\n * tenant headers are managed by the SDK and cannot be overridden.\n *\n * @typeParam T - Expected JSON response body.\n * @param endpoint - Root-relative API path.\n * @param options - Standard fetch options.\n * @throws When not connected, handshake did not include `authToken` and\n * `extensionDetails` (`baseUrl`, `tenantDomain`), the endpoint is invalid,\n * the request fails, or the response is not successful JSON.\n */\n apiCall<T>(endpoint: string, options?: RequestInit): Promise<T>;\n\n /**\n * Asks the parent for a new auth token and waits for `TOKEN_REFRESH`.\n *\n * @param options - Optional request timeout (default 10s).\n * @throws When not connected, a refresh is already in progress,\n * or the request times out / returns an invalid payload.\n */\n emitRequestAuthTokenRefresh(\n options?: RequestOptions,\n ): Promise<AuthToken>;\n\n /**\n * Pushes page settings to the parent and waits for `PAGE_SETTINGS_UPDATED`.\n *\n * @param payload - Update payload containing the settings object.\n * @param options - Optional request timeout (default 10s).\n * @throws When not connected, an update is already in progress,\n * or the request times out / returns an invalid payload.\n */\n emitUpdatePageSettings(\n payload: UpdatePageSettingsPayload,\n options?: RequestOptions,\n ): Promise<PageSettingsUpdatedPayload>;\n\n /**\n * Registers a handler for parent-initiated `LOAD_PAGE_SETTINGS`.\n *\n * @returns Unsubscribe function.\n */\n onLoadPageSettings(handler: (settings: PageSettings) => void): () => void;\n\n /**\n * Registers a handler for auth-token updates (requested or parent-pushed).\n *\n * @returns Unsubscribe function.\n */\n onAuthTokenRefresh(handler: (authToken: AuthToken) => void): () => void;\n\n /**\n * Notifies the parent that the extension's route changed.\n *\n * Wire: `NAVIGATION_CHANGE` (fire-and-forget).\n *\n * @param payload - Navigation change payload (`path` within the extension).\n * @throws When not connected or destroyed.\n */\n emitNavigationChange(payload: NavigationChangePayload): void;\n}\n\ntype Pending<T> = {\n resolve: (value: T) => void;\n reject: (reason?: unknown) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\n/** Default {@link IExtensionSDK} implementation. Prefer {@link createExtensionSDK}. */\nexport class ExtensionSDK implements IExtensionSDK {\n #allowedOrigin: string | null = null;\n #tenantUrl: string | null = null;\n #unsubscribe: (() => void) | null = null;\n #connected = false;\n #destroyed = false;\n\n #authToken: AuthToken | null = null;\n #extensionDetails: ExtensionDetails | null = null;\n #designTokens: DesignTokens | null = null;\n #pageSettings: PageSettings | null = null;\n\n #authTokenAutoRefresh = false;\n #authTokenBufferMs = DEFAULT_AUTH_TOKEN_BUFFER_MS;\n #authTokenRefreshTimer: ReturnType<typeof setTimeout> | null = null;\n #authTokenRefreshRetryUsed = false;\n\n #pendingHandshake: Pending<HandshakePayload> | null = null;\n #handshakePromise: Promise<HandshakePayload> | null = null;\n #pendingAuthTokenRefresh: Pending<AuthToken> | null = null;\n #pendingPageSettingsUpdate: Pending<PageSettingsUpdatedPayload> | null = null;\n\n #loadPageSettingsHandlers = new Set<(settings: PageSettings) => void>();\n #authTokenRefreshHandlers = new Set<(authToken: AuthToken) => void>();\n\n get isConnected(): boolean {\n return this.#connected;\n }\n\n async connect(options: ConnectOptions = {}): Promise<HandshakePayload> {\n this.#assertNotDestroyed();\n\n if (this.#connected) {\n this.#configureAuthTokenAutoRefresh(options);\n this.#scheduleProactiveAuthTokenRefresh();\n return this.#toHandshakePayload();\n }\n\n if (this.#handshakePromise) {\n return this.#handshakePromise;\n }\n\n this.#configureAuthTokenAutoRefresh(options);\n\n const tenantUrl = readTenantUrlFromLocation();\n this.#allowedOrigin = resolveAllowedOrigin(tenantUrl);\n this.#tenantUrl = tenantUrl;\n\n this.#unsubscribe?.();\n this.#unsubscribe = subscribeToParentMessages(\n this.#allowedOrigin,\n (event) => this.#onMessage(event),\n );\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const handshakePromise = new Promise<HandshakePayload>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.#pendingHandshake = null;\n this.#handshakePromise = null;\n reject(new Error(`Handshake timed out after ${timeoutMs}ms.`));\n }, timeoutMs);\n\n this.#pendingHandshake = { resolve, reject, timer };\n });\n this.#handshakePromise = handshakePromise;\n\n try {\n postToParent(CONNECTION_EVENTS.handshakeInit, this.#allowedOrigin);\n } catch (error) {\n this.#clearPending(this.#pendingHandshake);\n this.#pendingHandshake = null;\n this.#handshakePromise = null;\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n this.#allowedOrigin = null;\n this.#tenantUrl = null;\n this.#clearProactiveAuthTokenRefreshTimer();\n throw error;\n }\n\n return handshakePromise;\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.#connected = false;\n\n this.#clearProactiveAuthTokenRefreshTimer();\n this.#authTokenAutoRefresh = false;\n this.#authTokenRefreshRetryUsed = false;\n\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n this.#allowedOrigin = null;\n this.#tenantUrl = null;\n\n this.#rejectPending(this.#pendingHandshake, new Error(\"SDK destroyed.\"));\n this.#rejectPending(this.#pendingAuthTokenRefresh, new Error(\"SDK destroyed.\"));\n this.#rejectPending(this.#pendingPageSettingsUpdate, new Error(\"SDK destroyed.\"));\n this.#pendingHandshake = null;\n this.#handshakePromise = null;\n this.#pendingAuthTokenRefresh = null;\n this.#pendingPageSettingsUpdate = null;\n\n this.#loadPageSettingsHandlers.clear();\n this.#authTokenRefreshHandlers.clear();\n\n this.#authToken = null;\n this.#extensionDetails = null;\n this.#designTokens = null;\n this.#pageSettings = null;\n }\n\n getAuthToken(): AuthToken | null {\n return this.#authToken;\n }\n\n getExtensionDetails(): ExtensionDetails | null {\n return this.#extensionDetails;\n }\n\n getDesignTokens(): DesignTokens | null {\n return this.#designTokens;\n }\n\n getPageSettings(): PageSettings | null {\n return this.#pageSettings;\n }\n\n getAllowedOrigin(): string | null {\n return this.#allowedOrigin;\n }\n\n getTenantUrl(): string | null {\n return this.#tenantUrl;\n }\n\n getApiUrl(): string | null {\n const baseUrl = this.#extensionDetails?.baseUrl;\n if (!baseUrl) {\n return null;\n }\n\n try {\n return deriveApiUrl(baseUrl);\n } catch {\n return null;\n }\n }\n\n async apiCall<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n this.#assertConnected();\n\n const baseUrl = this.#extensionDetails?.baseUrl;\n const tenantDomain = this.#extensionDetails?.tenantDomain;\n const authTokenJwt = this.#authToken?.jwt;\n\n if (!baseUrl || !tenantDomain || !authTokenJwt) {\n throw new Error(\n \"API calls require handshake authToken and extensionDetails (baseUrl, tenantDomain).\",\n );\n }\n\n return callApi<T>(\n endpoint,\n {\n baseUrl,\n tenantDomain,\n authTokenJwt,\n },\n options,\n );\n }\n\n async emitRequestAuthTokenRefresh(\n options: RequestOptions = {},\n ): Promise<AuthToken> {\n this.#assertConnected();\n\n if (this.#pendingAuthTokenRefresh) {\n throw new Error(\"Auth token refresh already in progress.\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const allowedOrigin = this.#allowedOrigin!;\n\n const promise = new Promise<AuthToken>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.#pendingAuthTokenRefresh = null;\n reject(new Error(`Auth token refresh timed out after ${timeoutMs}ms.`));\n }, timeoutMs);\n\n this.#pendingAuthTokenRefresh = { resolve, reject, timer };\n });\n\n postToParent(CONNECTION_EVENTS.requestAuthTokenRefresh, allowedOrigin);\n return promise;\n }\n\n async emitUpdatePageSettings(\n payload: UpdatePageSettingsPayload,\n options: RequestOptions = {},\n ): Promise<PageSettingsUpdatedPayload> {\n this.#assertConnected();\n\n if (this.#pendingPageSettingsUpdate) {\n throw new Error(\"Page settings update already in progress.\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const allowedOrigin = this.#allowedOrigin!;\n\n const promise = new Promise<PageSettingsUpdatedPayload>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.#pendingPageSettingsUpdate = null;\n reject(new Error(`Page settings update timed out after ${timeoutMs}ms.`));\n }, timeoutMs);\n\n this.#pendingPageSettingsUpdate = { resolve, reject, timer };\n });\n\n postToParent(CONNECTION_EVENTS.updatePageSettings, allowedOrigin, payload);\n return promise;\n }\n\n onLoadPageSettings(handler: (settings: PageSettings) => void): () => void {\n this.#loadPageSettingsHandlers.add(handler);\n return () => {\n this.#loadPageSettingsHandlers.delete(handler);\n };\n }\n\n onAuthTokenRefresh(handler: (authToken: AuthToken) => void): () => void {\n this.#authTokenRefreshHandlers.add(handler);\n return () => {\n this.#authTokenRefreshHandlers.delete(handler);\n };\n }\n\n emitNavigationChange(payload: NavigationChangePayload): void {\n this.#assertConnected();\n postToParent(\n CONNECTION_EVENTS.navigationChange,\n this.#allowedOrigin!,\n payload,\n );\n }\n\n #onMessage(event: MessageEvent<ExtensionMessage>): void {\n const { type, payload } = event.data;\n\n switch (type) {\n case CONNECTION_EVENTS.handshakeAck: {\n this.#handleHandshakeAck(payload as HandshakePayload | undefined);\n break;\n }\n case CONNECTION_EVENTS.authTokenRefresh: {\n this.#handleAuthTokenRefresh(payload as AuthTokenRefreshPayload | undefined);\n break;\n }\n case CONNECTION_EVENTS.loadPageSettings: {\n this.#handleLoadPageSettings(payload as PageSettings | undefined);\n break;\n }\n case CONNECTION_EVENTS.pageSettingsUpdated: {\n this.#handlePageSettingsUpdated(\n payload as PageSettingsUpdatedPayload | undefined,\n );\n break;\n }\n default: {\n break;\n }\n }\n }\n\n #toHandshakePayload(): HandshakePayload {\n return {\n authToken: this.#authToken,\n extensionDetails: this.#extensionDetails,\n designTokens: this.#designTokens,\n pageSettings: this.#pageSettings,\n };\n }\n\n #handleHandshakeAck(payload: HandshakePayload | undefined): void {\n this.#authToken = isPlainObject(payload?.authToken)\n ? (payload?.authToken as AuthToken)\n : null;\n this.#extensionDetails = isPlainObject(payload?.extensionDetails)\n ? (payload?.extensionDetails as ExtensionDetails)\n : null;\n this.#designTokens = isPlainObject(payload?.designTokens)\n ? (payload?.designTokens as DesignTokens)\n : null;\n this.#pageSettings = isPlainObject(payload?.pageSettings)\n ? (payload?.pageSettings as PageSettings)\n : null;\n this.#connected = true;\n\n const pending = this.#pendingHandshake;\n this.#pendingHandshake = null;\n this.#handshakePromise = null;\n this.#resolvePending(pending, this.#toHandshakePayload());\n\n this.#authTokenRefreshRetryUsed = false;\n this.#scheduleProactiveAuthTokenRefresh();\n }\n\n #handleAuthTokenRefresh(payload: AuthTokenRefreshPayload | undefined): void {\n if (!payload?.authToken) {\n this.#rejectPending(\n this.#pendingAuthTokenRefresh,\n new Error(\"Invalid TOKEN_REFRESH payload.\"),\n );\n this.#pendingAuthTokenRefresh = null;\n return;\n }\n\n this.#authToken = payload.authToken;\n\n const pending = this.#pendingAuthTokenRefresh;\n this.#pendingAuthTokenRefresh = null;\n this.#resolvePending(pending, this.#authToken);\n\n for (const handler of this.#authTokenRefreshHandlers) {\n handler(this.#authToken);\n }\n\n this.#authTokenRefreshRetryUsed = false;\n this.#scheduleProactiveAuthTokenRefresh();\n }\n\n #handleLoadPageSettings(payload: PageSettings | undefined): void {\n const settings = payload ?? {};\n this.#pageSettings = settings;\n\n for (const handler of this.#loadPageSettingsHandlers) {\n handler(settings);\n }\n }\n\n #handlePageSettingsUpdated(\n payload: PageSettingsUpdatedPayload | undefined,\n ): void {\n if (!payload || typeof payload.success !== \"boolean\") {\n this.#rejectPending(\n this.#pendingPageSettingsUpdate,\n new Error(\"Invalid PAGE_SETTINGS_UPDATED payload.\"),\n );\n this.#pendingPageSettingsUpdate = null;\n return;\n }\n\n const pending = this.#pendingPageSettingsUpdate;\n this.#pendingPageSettingsUpdate = null;\n this.#resolvePending(pending, payload);\n }\n\n #assertNotDestroyed(): void {\n if (this.#destroyed) {\n throw new Error(\"SDK has been destroyed.\");\n }\n }\n\n #assertConnected(): void {\n this.#assertNotDestroyed();\n if (!this.#connected || !this.#allowedOrigin) {\n throw new Error(\"SDK is not connected. Call connect() first.\");\n }\n }\n\n #configureAuthTokenAutoRefresh(options: ConnectOptions): void {\n if (options.authTokenBufferMs !== undefined) {\n if (\n !Number.isFinite(options.authTokenBufferMs) ||\n options.authTokenBufferMs < 0\n ) {\n throw new Error(\n \"authTokenBufferMs must be a finite non-negative number.\",\n );\n }\n }\n\n this.#authTokenAutoRefresh = options.authTokenAutoRefresh === true;\n this.#authTokenBufferMs =\n options.authTokenBufferMs ?? DEFAULT_AUTH_TOKEN_BUFFER_MS;\n this.#authTokenRefreshRetryUsed = false;\n this.#clearProactiveAuthTokenRefreshTimer();\n }\n\n #parseExpiresAtMs(authToken: AuthToken): number | null {\n const expiresSec = Number(authToken.expires);\n if (!Number.isFinite(expiresSec)) {\n return null;\n }\n return expiresSec * 1000;\n }\n\n #getProactiveRefreshDelayMs(authToken: AuthToken): number | null {\n const expiresAtMs = this.#parseExpiresAtMs(authToken);\n if (expiresAtMs === null) {\n return null;\n }\n return Math.max(0, expiresAtMs - this.#authTokenBufferMs - Date.now());\n }\n\n #clearProactiveAuthTokenRefreshTimer(): void {\n if (this.#authTokenRefreshTimer !== null) {\n clearTimeout(this.#authTokenRefreshTimer);\n this.#authTokenRefreshTimer = null;\n }\n }\n\n #scheduleProactiveAuthTokenRefresh(): void {\n this.#clearProactiveAuthTokenRefreshTimer();\n\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n !this.#authToken\n ) {\n return;\n }\n\n const delayMs = this.#getProactiveRefreshDelayMs(this.#authToken);\n if (delayMs === null) {\n return;\n }\n\n const clampedDelay = Math.min(delayMs, MAX_TIMEOUT_MS);\n this.#authTokenRefreshTimer = setTimeout(() => {\n this.#authTokenRefreshTimer = null;\n void this.#runProactiveAuthTokenRefresh();\n }, clampedDelay);\n }\n\n async #runProactiveAuthTokenRefresh(): Promise<void> {\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n !this.#authToken\n ) {\n return;\n }\n\n const authTokenAtRequest = this.#authToken;\n const delayMs = this.#getProactiveRefreshDelayMs(authTokenAtRequest);\n if (delayMs === null) {\n return;\n }\n\n if (delayMs > 0) {\n this.#scheduleProactiveAuthTokenRefresh();\n return;\n }\n\n if (this.#pendingAuthTokenRefresh) {\n this.#handleProactiveAuthTokenRefreshFailure(authTokenAtRequest);\n return;\n }\n\n try {\n await this.emitRequestAuthTokenRefresh();\n } catch {\n this.#handleProactiveAuthTokenRefreshFailure(authTokenAtRequest);\n }\n }\n\n #handleProactiveAuthTokenRefreshFailure(authTokenAtRequest: AuthToken): void {\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n this.#authToken?.jwt !== authTokenAtRequest.jwt\n ) {\n return;\n }\n\n if (this.#authTokenRefreshRetryUsed) {\n return;\n }\n\n const expiresAtMs = this.#parseExpiresAtMs(authTokenAtRequest);\n if (expiresAtMs === null) {\n return;\n }\n\n const remainingMs = expiresAtMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n this.#authTokenRefreshRetryUsed = true;\n this.#clearProactiveAuthTokenRefreshTimer();\n\n const retryDelay = Math.min(AUTO_REFRESH_RETRY_DELAY_MS, remainingMs);\n this.#authTokenRefreshTimer = setTimeout(() => {\n this.#authTokenRefreshTimer = null;\n void this.#runProactiveAuthTokenRefresh();\n }, retryDelay);\n }\n\n #clearPending(pending: { timer: ReturnType<typeof setTimeout> } | null): void {\n if (pending) {\n clearTimeout(pending.timer);\n }\n }\n\n #resolvePending<T>(pending: Pending<T> | null, value: T): void {\n if (!pending) {\n return;\n }\n clearTimeout(pending.timer);\n pending.resolve(value);\n }\n\n #rejectPending(\n pending: { timer: ReturnType<typeof setTimeout>; reject: (reason?: unknown) => void } | null,\n reason: unknown,\n ): void {\n if (!pending) {\n return;\n }\n clearTimeout(pending.timer);\n pending.reject(reason);\n }\n}\n","import type { ConnectionEvent } from \"./events\";\nimport type { ExtensionMessage } from \"./types\";\n\nexport type HostMessageHandler = (\n event: MessageEvent<ExtensionMessage>,\n) => void;\n\n/**\n * Posts a typed extension message to an embedded child frame.\n *\n * @param contentWindow - The iframe's `contentWindow`.\n * @param origin - Target origin for `postMessage` (extension origin).\n * @param type - Wire event type from {@link CONNECTION_EVENTS}.\n * @param payload - Optional message payload.\n */\nexport function postToChild(\n contentWindow: Window,\n origin: string,\n type: ConnectionEvent,\n payload?: unknown,\n): void {\n const message: ExtensionMessage =\n payload === undefined ? { type } : { type, payload };\n contentWindow.postMessage(message, origin);\n}\n\n/**\n * Subscribes to `message` events from a child frame at `origin`.\n *\n * Ignores events from other origins and payloads without a `type` field.\n *\n * @returns Unsubscribe function.\n */\nexport function subscribeToChildMessages(\n origin: string,\n handler: HostMessageHandler,\n): () => void {\n const listener = (event: MessageEvent) => {\n if (event.origin !== origin) {\n return;\n }\n\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n return;\n }\n\n handler(event as MessageEvent<ExtensionMessage>);\n };\n\n window.addEventListener(\"message\", listener);\n return () => window.removeEventListener(\"message\", listener);\n}\n","import { CONNECTION_EVENTS } from \"./events\";\nimport { postToChild, subscribeToChildMessages } from \"./hostMessaging\";\nimport type {\n AuthToken,\n ExtensionMessage,\n HandshakePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n UpdatePageSettingsPayload,\n} from \"./types\";\n\n/**\n * Options for {@link ExtensionSDKHost}.\n *\n * Platform code supplies token minting and page-settings persistence via\n * callbacks; the host owns only the postMessage protocol.\n */\nexport interface ExtensionSDKHostOptions {\n /** Returns the iframe `contentWindow`, or `null` if not ready. */\n getContentWindow: () => Window | null;\n /** Extension origin used as `postMessage` targetOrigin / source filter. */\n origin: string;\n /**\n * Builds the handshake payload when the child sends `HANDSHAKE_INIT`.\n * Context fields are optional; send `designTokens` whenever a tenant is in\n * context. `spaceId` / `pageId` apply to Experience Pages only.\n */\n resolveHandshakePayload: () =>\n Partial<HandshakePayload> | Promise<Partial<HandshakePayload>>;\n /**\n * Resolves a fresh auth token when the child sends `REQUEST_TOKEN_REFRESH`.\n */\n resolveAuthToken: () => AuthToken | Promise<AuthToken>;\n /**\n * Persists page settings from `UPDATE_PAGE_SETTINGS`.\n * Return `true` on success; the host emits `PAGE_SETTINGS_UPDATED`.\n */\n onUpdatePageSettings?: (\n settings: PageSettings,\n ) => boolean | Promise<boolean>;\n}\n\n/**\n * Parent-frame counterpart to {@link IExtensionSDK}.\n *\n * Listens for child messages and replies with the shared wire protocol.\n * Create one instance per iframe.\n */\nexport interface IExtensionSDKHost {\n /**\n * Attaches the child message listener. Safe to call once; no-ops if already\n * started or destroyed.\n */\n start(): void;\n\n /**\n * Tears down the listener. The instance cannot be restarted after destroy.\n */\n destroy(): void;\n\n /**\n * Asks the child to open page settings UI with the given values.\n *\n * Wire: `LOAD_PAGE_SETTINGS` (fire-and-forget).\n */\n emitLoadPageSettings(settings: PageSettings | null): void;\n}\n\n/** Default {@link IExtensionSDKHost} implementation. */\nexport class ExtensionSDKHost implements IExtensionSDKHost {\n #options: ExtensionSDKHostOptions;\n #unsubscribe: (() => void) | null = null;\n #started = false;\n #destroyed = false;\n\n constructor(options: ExtensionSDKHostOptions) {\n this.#options = options;\n }\n\n start(): void {\n if (this.#destroyed || this.#started) {\n return;\n }\n\n this.#unsubscribe = subscribeToChildMessages(\n this.#options.origin,\n (event) => {\n void this.#onMessage(event);\n },\n );\n this.#started = true;\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.#started = false;\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n }\n\n emitLoadPageSettings(settings: PageSettings | null): void {\n this.#assertNotDestroyed();\n this.#post(CONNECTION_EVENTS.loadPageSettings, settings);\n }\n\n async #onMessage(event: MessageEvent<ExtensionMessage>): Promise<void> {\n if (this.#destroyed) {\n return;\n }\n\n const { type, payload } = event.data;\n\n switch (type) {\n case CONNECTION_EVENTS.handshakeInit: {\n await this.#handleHandshakeInit();\n break;\n }\n case CONNECTION_EVENTS.requestAuthTokenRefresh: {\n await this.#handleAuthTokenRefresh();\n break;\n }\n case CONNECTION_EVENTS.updatePageSettings: {\n await this.#handleUpdatePageSettings(\n payload as UpdatePageSettingsPayload | undefined,\n );\n break;\n }\n default: {\n break;\n }\n }\n }\n\n async #handleHandshakeInit(): Promise<void> {\n try {\n const handshakePayload = await this.#options.resolveHandshakePayload();\n if (this.#destroyed) {\n return;\n }\n this.#post(CONNECTION_EVENTS.handshakeAck, handshakePayload);\n } catch {\n // Platform owns error UI / retry; do not reply with a partial ACK.\n }\n }\n\n async #handleAuthTokenRefresh(): Promise<void> {\n try {\n const authToken = await this.#options.resolveAuthToken();\n if (this.#destroyed) {\n return;\n }\n this.#post(CONNECTION_EVENTS.authTokenRefresh, { authToken });\n } catch {\n // Platform owns error UI / retry; do not reply with an invalid token.\n }\n }\n\n async #handleUpdatePageSettings(\n payload: UpdatePageSettingsPayload | undefined,\n ): Promise<void> {\n const settings = payload?.settings;\n if (!settings || typeof settings !== \"object\") {\n this.#postPageSettingsUpdated(false);\n return;\n }\n\n const handler = this.#options.onUpdatePageSettings;\n if (!handler) {\n this.#postPageSettingsUpdated(false);\n return;\n }\n\n try {\n const success = await handler(settings);\n if (this.#destroyed) {\n return;\n }\n this.#postPageSettingsUpdated(Boolean(success));\n } catch {\n if (this.#destroyed) {\n return;\n }\n this.#postPageSettingsUpdated(false);\n }\n }\n\n #postPageSettingsUpdated(success: boolean): void {\n const payload: PageSettingsUpdatedPayload = { success };\n this.#post(CONNECTION_EVENTS.pageSettingsUpdated, payload);\n }\n\n #post(type: (typeof CONNECTION_EVENTS)[keyof typeof CONNECTION_EVENTS], payload?: unknown): void {\n const contentWindow = this.#options.getContentWindow();\n if (!contentWindow) {\n return;\n }\n postToChild(contentWindow, this.#options.origin, type, payload);\n }\n\n #assertNotDestroyed(): void {\n if (this.#destroyed) {\n throw new Error(\"ExtensionSDKHost has been destroyed.\");\n }\n }\n}\n","/**\n * Returns whether `urlRaw` is a valid HTTPS extension URL.\n *\n * When `checkOrigin` is provided, the URL's origin must also pass that predicate\n * (e.g. hostname ends with `.kadanza.app`).\n */\nexport function isValidExtensionUrl(\n urlRaw: string | null | undefined,\n checkOrigin?: (origin: string) => boolean,\n): boolean {\n if (typeof urlRaw !== \"string\") {\n return false;\n }\n\n try {\n const url = new URL(urlRaw);\n const allowedProtocol = url.protocol === \"https:\";\n const allowedOrigin = checkOrigin ? checkOrigin(url.origin) : true;\n return allowedProtocol && allowedOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * Clones `urlRaw` and sets the `tenantUrl` search param (parent origin).\n *\n * @param urlRaw - Absolute extension URL string.\n * @param tenantUrl - Parent origin to embed (defaults to `window.location.origin`).\n * @returns Enriched `URL`, or `null` when `urlRaw` is missing or unparsable.\n */\nexport function enrichExtensionUrl(\n urlRaw: string | null | undefined,\n tenantUrl: string = window.location.origin,\n): URL | null {\n if (typeof urlRaw !== \"string\") {\n return null;\n }\n\n try {\n const enrichedUrl = new URL(urlRaw);\n\n if (typeof tenantUrl === \"string\") {\n enrichedUrl.searchParams.set(\"tenantUrl\", tenantUrl);\n }\n\n return enrichedUrl;\n } catch {\n return null;\n }\n}\n","import { ExtensionSDK, type IExtensionSDK } from \"./ExtensionSDK\";\n\nexport { ExtensionSDK, type IExtensionSDK } from \"./ExtensionSDK\";\nexport {\n ExtensionSDKHost,\n type ExtensionSDKHostOptions,\n type IExtensionSDKHost,\n} from \"./ExtensionSDKHost\";\nexport { CONNECTION_EVENTS, type ConnectionEvent } from \"./events\";\nexport {\n postToChild,\n subscribeToChildMessages,\n type HostMessageHandler,\n} from \"./hostMessaging\";\nexport { enrichExtensionUrl, isValidExtensionUrl } from \"./hostUrl\";\nexport {\n InvalidOriginError,\n readTenantUrlFromLocation,\n resolveAllowedOrigin,\n} from \"./origin\";\nexport type {\n AuthToken,\n AuthTokenRefreshPayload,\n ConnectOptions,\n DesignTokens,\n ExtensionDetails,\n ExtensionMessage,\n HandshakePayload,\n NavigationChangePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n RequestOptions,\n UpdatePageSettingsPayload,\n} from \"./types\";\n\nlet instance: IExtensionSDK | null = null;\nlet hasWarnedDuplicateCreate = false;\n\n/** Clears the factory singleton when the managed instance is destroyed. */\nclass ManagedExtensionSDK extends ExtensionSDK {\n destroy(): void {\n super.destroy();\n if (instance === this) {\n instance = null;\n hasWarnedDuplicateCreate = false;\n }\n }\n}\n\n/**\n * Returns the shared {@link IExtensionSDK} for the current extension frame.\n *\n * Creates the instance on first call. Later calls return the same instance and\n * log a console warning — use the SDK once per app. After {@link IExtensionSDK.destroy},\n * the next call creates a fresh instance.\n */\nexport const createExtensionSDK = (): IExtensionSDK => {\n if (instance) {\n if (!hasWarnedDuplicateCreate) {\n console.warn(\n `[${__PACKAGE_NAME__}] createExtensionSDK() was called more than once. ` +\n \"The SDK is a singleton — reuse the same instance for the whole app.\",\n );\n hasWarnedDuplicateCreate = true;\n }\n return instance;\n }\n\n instance = new ManagedExtensionSDK();\n return instance;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"extension-sdk.js","names":["#onSettle","#resolve","#reject","#timer","#settled","#settle","#connected","#assertNotDestroyed","#configureAuthTokenAutoRefresh","#scheduleProactiveAuthTokenRefresh","#toHandshakePayload","#handshakePromise","#allowedOrigin","#tenantUrl","#unsubscribe","#onMessage","#pendingHandshake","#clearProactiveAuthTokenRefreshTimer","#destroyed","#authTokenAutoRefresh","#authTokenRefreshRetryUsed","#pendingAuthTokenRefresh","#pendingPageSettingsUpdate","#loadPageSettingsHandlers","#navigateHandlers","#authTokenRefreshHandlers","#authToken","#extensionDetails","#designTokens","#pageSettings","#assertConnected","#handleHandshakeAck","#handleAuthTokenRefresh","#handleLoadPageSettings","#handleRequestNavigationChange","#handlePageSettingsUpdated","#authTokenBufferMs","#parseExpiresAtMs","#authTokenRefreshTimer","#getProactiveRefreshDelayMs","#runProactiveAuthTokenRefresh","#handleProactiveAuthTokenRefreshFailure","#options","#destroyed","#started","#unsubscribe","#onMessage","#routingType","#pendingNavigationChange","#assertNotDestroyed","#post","#handleHandshakeInit","#handleAuthTokenRefresh","#handleUpdatePageSettings","#handleNavigationChange","#postPageSettingsUpdated"],"sources":["../src/api.ts","../src/events.ts","../src/messaging.ts","../src/origin.ts","../src/PendingRequest.ts","../src/types.ts","../src/ExtensionSDK.ts","../src/hostMessaging.ts","../src/ExtensionSDKHost.ts","../src/hostUrl.ts","../src/index.ts"],"sourcesContent":["interface ApiRequestContext {\n baseUrl: string;\n tenantDomain: string;\n authTokenJwt: string;\n}\n\n/** Derives the Platform API origin from handshake `baseUrl`. */\nexport function deriveApiUrl(baseUrl: string): string {\n if (baseUrl.startsWith(\"/\")) {\n return baseUrl;\n }\n\n const url = new URL(baseUrl);\n url.hostname = `api.${url.hostname}`;\n return url.origin;\n}\n\nfunction resolveApiEndpoint(baseUrl: string, endpoint: string): string {\n if (!endpoint.startsWith(\"/\") || endpoint.startsWith(\"//\")) {\n throw new Error(\n 'API endpoint must be a root-relative path starting with a single \"/\".',\n );\n }\n\n return `${deriveApiUrl(baseUrl).replace(/\\/+$/, \"\")}${endpoint}`;\n}\n\nexport async function callApi<T>(\n endpoint: string,\n context: ApiRequestContext,\n options: RequestInit = {},\n): Promise<T> {\n const headers = new Headers(options.headers);\n\n if (!headers.has(\"Content-Type\")) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n\n headers.set(\"Authorization\", `Bearer ${context.authTokenJwt}`);\n headers.set(\"X-Tenant\", context.tenantDomain);\n\n const response = await fetch(resolveApiEndpoint(context.baseUrl, endpoint), {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const statusText = response.statusText ? ` ${response.statusText}` : \"\";\n throw new Error(`API call failed: ${response.status}${statusText}.`);\n }\n\n return (await response.json()) as T;\n}\n","/**\n * Wire event type strings for parent/child `postMessage` communication.\n *\n * Keys are SDK-facing names; values are the strings sent on the wire.\n * Comments mark message direction relative to the extension (child).\n */\nexport const CONNECTION_EVENTS = {\n /** Child → parent: start handshake. */\n handshakeInit: \"HANDSHAKE_INIT\",\n /** Parent → child: handshake success with auth and context. */\n handshakeAck: \"HANDSHAKE_ACK\",\n /** Child → parent: request a new auth token. */\n requestAuthTokenRefresh: \"REQUEST_TOKEN_REFRESH\",\n /** Parent → child: new auth token (reply or push). */\n authTokenRefresh: \"TOKEN_REFRESH\",\n /** Parent → child: open/load page settings UI with values. */\n loadPageSettings: \"LOAD_PAGE_SETTINGS\",\n /** Child → parent: persist updated page settings. */\n updatePageSettings: \"UPDATE_PAGE_SETTINGS\",\n /** Parent → child: result of a page settings update. */\n pageSettingsUpdated: \"PAGE_SETTINGS_UPDATED\",\n /** Parent → child: ask the SPA to navigate without reloading the iframe. */\n requestNavigationChange: \"REQUEST_NAVIGATION_CHANGE\",\n /** Child → parent: extension route changed (ack or spontaneous). */\n navigationChange: \"NAVIGATION_CHANGE\",\n} as const;\n\n/** Union of all {@link CONNECTION_EVENTS} wire string values. */\nexport type ConnectionEvent =\n (typeof CONNECTION_EVENTS)[keyof typeof CONNECTION_EVENTS];\n","import type { ConnectionEvent } from \"./events\";\nimport type { ExtensionMessage } from \"./types\";\n\nexport type MessageHandler = (event: MessageEvent<ExtensionMessage>) => void;\n\nexport function postToParent(\n type: ConnectionEvent,\n allowedOrigin: string,\n payload?: unknown,\n): void {\n if (!window.parent || window.parent === window) {\n throw new Error(\"Extension is not running inside a parent frame.\");\n }\n\n const message: ExtensionMessage = payload === undefined ? { type } : { type, payload };\n window.parent.postMessage(message, allowedOrigin);\n}\n\nexport function subscribeToParentMessages(\n allowedOrigin: string,\n handler: MessageHandler,\n): () => void {\n const listener = (event: MessageEvent) => {\n if (event.origin !== allowedOrigin) {\n return;\n }\n\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n return;\n }\n\n handler(event as MessageEvent<ExtensionMessage>);\n };\n\n window.addEventListener(\"message\", listener);\n return () => window.removeEventListener(\"message\", listener);\n}\n","const LOCALHOST_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/** Thrown when `tenantUrl` is missing or not a safe postMessage origin. */\nexport class InvalidOriginError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidOriginError\";\n }\n}\n\n/**\n * Parses and validates a tenant URL into a postMessage-safe origin.\n * Allows https always; http only for localhost / 127.0.0.1 / [::1].\n *\n * @param tenantUrl - Absolute URL from the `tenantUrl` search param.\n * @returns Origin string suitable for `postMessage` targetOrigin.\n * @throws {InvalidOriginError} When missing, unparsable, or disallowed scheme/host.\n */\nexport function resolveAllowedOrigin(tenantUrl: string | null): string {\n if (!tenantUrl) {\n throw new InvalidOriginError(\n 'Missing \"tenantUrl\" search parameter required to establish parent origin.',\n );\n }\n\n let url: URL;\n try {\n url = new URL(tenantUrl);\n } catch {\n throw new InvalidOriginError(`Invalid tenantUrl: \"${tenantUrl}\".`);\n }\n\n const isLocalhost = LOCALHOST_HOSTS.has(url.hostname);\n const isHttps = url.protocol === \"https:\";\n const isLocalHttp = url.protocol === \"http:\" && isLocalhost;\n\n if (!isHttps && !isLocalHttp) {\n throw new InvalidOriginError(\n `tenantUrl must use https (or http on localhost). Received: \"${url.protocol}//${url.host}\".`,\n );\n }\n\n return url.origin;\n}\n\n/**\n * Reads the parent-supplied `tenantUrl` from a URL search string.\n *\n * The parent must pass `tenantUrl` on the iframe `src` so\n * {@link resolveAllowedOrigin} can whitelist the parent origin.\n *\n * @param search - Query string (defaults to `window.location.search`).\n */\nexport function readTenantUrlFromLocation(\n search: string = window.location.search,\n): string | null {\n return new URLSearchParams(search).get(\"tenantUrl\");\n}\n","export type PendingRequestOptions<TContext = undefined> = {\n timeoutMs: number;\n timeoutMessage: string;\n /** Optional correlation data (e.g. expected path). */\n context?: TContext;\n /** Called once when the request settles (resolve, reject, timeout, or abandon). */\n onSettle?: () => void;\n};\n\n/**\n * One-shot promise with timeout for request/ACK postMessage flows.\n *\n * Owns timer lifecycle and optional correlation {@link context}. Callers keep a\n * single in-flight slot and clear it via {@link onSettle}.\n */\nexport class PendingRequest<TResult, TContext = undefined> {\n readonly promise: Promise<TResult>;\n readonly context: TContext;\n\n #resolve!: (value: TResult) => void;\n #reject!: (reason?: unknown) => void;\n #timer: ReturnType<typeof setTimeout>;\n #settled = false;\n #onSettle: (() => void) | undefined;\n\n private constructor(options: PendingRequestOptions<TContext>) {\n this.context = options.context as TContext;\n this.#onSettle = options.onSettle;\n\n this.promise = new Promise<TResult>((resolve, reject) => {\n this.#resolve = resolve;\n this.#reject = reject;\n });\n\n this.#timer = setTimeout(() => {\n this.reject(new Error(options.timeoutMessage));\n }, options.timeoutMs);\n }\n\n static create<TResult, TContext = undefined>(\n options: PendingRequestOptions<TContext>,\n ): PendingRequest<TResult, TContext> {\n return new PendingRequest(options);\n }\n\n get isPending(): boolean {\n return !this.#settled;\n }\n\n /** Whether still open and {@link context} satisfies `predicate`. */\n matches(predicate: (context: TContext) => boolean): boolean {\n return this.isPending && predicate(this.context);\n }\n\n resolve(value: TResult): boolean {\n if (this.#settled) {\n return false;\n }\n this.#settle();\n this.#resolve(value);\n return true;\n }\n\n reject(reason?: unknown): boolean {\n if (this.#settled) {\n return false;\n }\n this.#settle();\n this.#reject(reason);\n return true;\n }\n\n /**\n * Clears the timer without resolving or rejecting the promise.\n * Use when abandoning an in-flight request that no caller will await.\n */\n abandon(): boolean {\n if (this.#settled) {\n return false;\n }\n this.#settle();\n return true;\n }\n\n #settle(): void {\n this.#settled = true;\n clearTimeout(this.#timer);\n this.#onSettle?.();\n this.#onSettle = undefined;\n }\n}\n","import type { ConnectionEvent } from \"./events\";\n\n/** Auth token issued by the parent (`authToken` on the wire). */\nexport interface AuthToken {\n jwt: string;\n /** Unix expiry seconds as a string, per parent payload. */\n expires: string;\n}\n\n/**\n * Host context from handshake. Only set when the parent includes it.\n *\n * Field availability depends on where the iframe is mounted:\n * - Experience Pages typically send `spaceId` and `pageId`.\n * - Admin Console Pages typically omit those — there is no space page.\n *\n * Other identity fields are sent when the host knows them. Handshake does not\n * require this object.\n */\nexport interface ExtensionDetails {\n extensionId?: string;\n tenantId?: string;\n tenantDomain?: string;\n /** Base URL of the parent application (used to derive the Platform API origin). */\n baseUrl?: string;\n /** Space that owns the Experience Page. Omitted in Admin Console. */\n spaceId?: string;\n /** Experience Page id. Omitted in Admin Console. */\n pageId?: string;\n /** BCP 47 locale from the parent. */\n locale?: string;\n}\n\n/**\n * Tenant branding from the parent, for aligning extension UI with the host.\n *\n * The host should send this whenever a tenant is in context (Experience Pages\n * and Admin Console). Handshake still succeeds if it is omitted — treat every\n * field as optional.\n *\n * - `primaryColor` — tenant palette primary; accents, buttons, links\n * - `fontFamily` — tenant font family; body and UI type\n * - `borderRadius` — tenant roundness; controls and cards\n *\n * Values may be missing when the tenant has no setting configured.\n */\nexport interface DesignTokens {\n primaryColor?: string;\n fontFamily?: string;\n borderRadius?: string;\n}\n\n/** Opaque page-level settings bag owned by the extension and synced with the parent. */\nexport type PageSettings = Record<string, unknown>;\n\n/**\n * How the extension handles in-app routing.\n *\n * - `server` — full document loads (iframe `src` reload). Default.\n * - `client-hash` — hash router SPA; supports soft navigation via\n * `REQUEST_NAVIGATION_CHANGE` / `NAVIGATION_CHANGE`.\n */\nexport type RoutingType = \"server\" | \"client-hash\";\n\n/** Default when `routingType` is omitted or unrecognized. */\nexport const DEFAULT_ROUTING_TYPE: RoutingType = \"server\";\n\n/** Normalize a wire / option value to a known {@link RoutingType}. */\nexport function normalizeRoutingType(value: unknown): RoutingType {\n if (value === \"client-hash\") {\n return \"client-hash\";\n }\n return DEFAULT_ROUTING_TYPE;\n}\n\n/** Payload for `HANDSHAKE_INIT` (child → parent). */\nexport interface HandshakeInitPayload {\n /**\n * Declares how the extension routes. Omit or unknown → `server`.\n * Soft navigation requires `client-hash`.\n */\n routingType?: RoutingType;\n}\n\n/**\n * Context delivered with `HANDSHAKE_ACK`.\n *\n * Handshake completes as soon as the parent ACKs. Every property is optional\n * and only populated when that host surface provides it. After `connect()`,\n * omitted wire fields are normalized to `null`.\n */\nexport interface HandshakePayload {\n authToken: AuthToken | null;\n extensionDetails: ExtensionDetails | null;\n designTokens: DesignTokens | null;\n pageSettings: PageSettings | null;\n}\n\n/** Payload for `TOKEN_REFRESH` (requested or parent-pushed). */\nexport interface AuthTokenRefreshPayload {\n authToken: AuthToken;\n}\n\n/** Payload for `PAGE_SETTINGS_UPDATED` after a page settings update request. */\nexport interface PageSettingsUpdatedPayload {\n success: boolean;\n}\n\n/** Payload for `UPDATE_PAGE_SETTINGS` (child → parent). */\nexport interface UpdatePageSettingsPayload {\n settings: PageSettings;\n}\n\n/** Payload for `REQUEST_NAVIGATION_CHANGE` (parent → child). */\nexport interface RequestNavigationChangePayload {\n /** Path within the extension (e.g. `/settings`). */\n path: string;\n /**\n * Query string for the target route, including the leading `?`\n * (e.g. `?tab=history`). Omitted or empty means no query.\n *\n * For `client-hash` extensions the query belongs inside the hash fragment;\n * the child SDK consumer is responsible for applying it to its router.\n */\n search?: string;\n}\n\n/** Payload for `NAVIGATION_CHANGE` (child → parent). */\nexport interface NavigationChangePayload {\n /** Path within the extension (e.g. `/settings`). */\n path: string;\n /**\n * Query string of the child's current route, including the leading `?`\n * (e.g. `?tab=history`). Omitted or empty means no query.\n *\n * For `client-hash` extensions the child reads this from its hash fragment,\n * not from `window.location.search`.\n */\n search?: string;\n}\n\n/**\n * Normalizes a route query string for the navigation contract.\n *\n * Returns `\"\"` for missing / empty input, otherwise guarantees a single\n * leading `?` (e.g. `tab=1` → `?tab=1`, `?tab=1` → `?tab=1`).\n */\nexport function normalizeNavigationSearch(value: unknown): string {\n if (typeof value !== \"string\" || value === \"\" || value === \"?\") {\n return \"\";\n }\n return value.startsWith(\"?\") ? value : `?${value}`;\n}\n\n/** Envelope for parent/child `postMessage` traffic. */\nexport interface ExtensionMessage<TPayload = unknown> {\n type: ConnectionEvent | string;\n payload?: TPayload;\n}\n\n/** Options for {@link ExtensionSDK.connect} / handshake. */\nexport interface ConnectOptions {\n /** Handshake timeout in milliseconds (default 10_000). */\n timeoutMs?: number;\n /**\n * Routing mode announced to the parent on `HANDSHAKE_INIT`.\n * HashRouter SPAs should pass `client-hash`. Default: `server`.\n */\n routingType?: RoutingType;\n /**\n * Enables proactive auth-token refresh. After the handshake, the SDK reads\n * the auth token's Unix-seconds `expires` value and arms a one-shot timer\n * for `expires - authTokenBufferMs`; it does not run a polling interval.\n *\n * Every successful manual, automatic, or parent-pushed auth-token update\n * cancels the previous timer and schedules a new one from the new auth\n * auth token's expiry. If an automatic request fails, it retries once after 30\n * seconds while the same auth token is still current and unexpired.\n * {@link ExtensionSDK.destroy} clears the timer. Missing or invalid expiry\n * values disable scheduling for that auth token without failing the\n * connection.\n *\n * Default: `false`.\n */\n authTokenAutoRefresh?: boolean;\n /**\n * How many milliseconds before auth-token expiry the one-shot refresh timer\n * should fire. Must be finite and non-negative. If the auth token is already\n * inside this buffer when received, refresh is requested immediately.\n *\n * Default: `120_000` (2 minutes). Ignored unless\n * {@link authTokenAutoRefresh} is enabled.\n */\n authTokenBufferMs?: number;\n}\n\n/** Options for request/response SDK methods that wait on a parent reply. */\nexport interface RequestOptions {\n /** Request timeout in milliseconds (default 10_000). */\n timeoutMs?: number;\n}\n","import { callApi, deriveApiUrl } from \"./api\";\nimport { CONNECTION_EVENTS } from \"./events\";\nimport { postToParent, subscribeToParentMessages } from \"./messaging\";\nimport { readTenantUrlFromLocation, resolveAllowedOrigin } from \"./origin\";\nimport { PendingRequest } from \"./PendingRequest\";\nimport type {\n AuthToken,\n AuthTokenRefreshPayload,\n ConnectOptions,\n DesignTokens,\n ExtensionDetails,\n ExtensionMessage,\n HandshakeInitPayload,\n HandshakePayload,\n NavigationChangePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n RequestNavigationChangePayload,\n RequestOptions,\n UpdatePageSettingsPayload,\n} from \"./types\";\nimport { normalizeRoutingType } from \"./types\";\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_AUTH_TOKEN_BUFFER_MS = 120_000;\nconst AUTO_REFRESH_RETRY_DELAY_MS = 30_000;\n/** Largest delay `setTimeout` can reliably use in browsers. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Public contract for talking to the Kadanza parent frame over `postMessage`.\n *\n * Requires a `tenantUrl` search param on the extension URL so\n * {@link connect} can validate the parent origin.\n */\nexport interface IExtensionSDK {\n /**\n * Establishes the parent connection and waits for `HANDSHAKE_ACK`.\n *\n * Safe to call repeatedly (e.g. from a React `useEffect` with deps):\n * - Already connected: returns the cached handshake and re-applies\n * `authTokenAutoRefresh` / `authTokenBufferMs` without another init.\n * `timeoutMs` is ignored after connect.\n * - Handshake in progress: returns the same in-flight promise. Options from\n * the first connecting call apply until the handshake completes.\n *\n * Opt-in proactive refresh can be enabled with\n * `authTokenAutoRefresh: true` (optional `authTokenBufferMs`).\n *\n * Pass `routingType: \"client-hash\"` for HashRouter SPAs so the parent can\n * soft-navigate without reloading the iframe. Default is `\"server\"`.\n *\n * Handshake completes when the parent sends `HANDSHAKE_ACK`. Context fields\n * (`authToken`, `extensionDetails`, `designTokens`, `pageSettings`) are\n * optional — omitted values are `null`. Use them only when the current host\n * surface provides them (e.g. `spaceId` / `pageId` on Experience Pages,\n * `designTokens` when a tenant is in context).\n *\n * @param options - Handshake timeout, routing type, and optional auth-token auto-refresh.\n * @throws {InvalidOriginError} When `tenantUrl` is missing or invalid.\n * @throws When not embedded in a parent frame, destroyed, or the handshake\n * times out.\n */\n connect(options?: ConnectOptions): Promise<HandshakePayload>;\n\n /**\n * Tears down listeners, proactive refresh timers, and cached state.\n * Rejects any in-flight requests. The instance cannot be reused after destroy.\n * When created via {@link createExtensionSDK}, clears the singleton so a\n * later create call can return a fresh instance.\n */\n destroy(): void;\n\n /** Whether a successful handshake has completed and not been destroyed. */\n readonly isConnected: boolean;\n\n /**\n * Last auth token from handshake or refresh.\n * `null` until connected, or when the host omitted it.\n */\n getAuthToken(): AuthToken | null;\n\n /**\n * Extension context from handshake.\n * `null` until connected, or when the host omitted it.\n */\n getExtensionDetails(): ExtensionDetails | null;\n\n /**\n * Tenant branding from handshake.\n * `null` until connected, or when the host omitted it.\n */\n getDesignTokens(): DesignTokens | null;\n\n /**\n * Latest page settings from handshake or `LOAD_PAGE_SETTINGS`;\n * `null` until set.\n */\n getPageSettings(): PageSettings | null;\n\n /**\n * Validated parent `postMessage` origin from `tenantUrl`;\n * `null` until `connect` resolves it.\n */\n getAllowedOrigin(): string | null;\n\n /**\n * Raw `tenantUrl` search-param value used for the allowed origin;\n * `null` until `connect` reads it.\n */\n getTenantUrl(): string | null;\n\n /**\n * Platform API origin derived from handshake `baseUrl`.\n * `null` until connected, or when `extensionDetails.baseUrl` was omitted.\n */\n getApiUrl(): string | null;\n\n /**\n * Calls the Kadanza Platform API with the current connection credentials.\n *\n * The API origin is derived from the handshake `baseUrl`. Authorization and\n * tenant headers are managed by the SDK and cannot be overridden.\n *\n * @typeParam T - Expected JSON response body.\n * @param endpoint - Root-relative API path.\n * @param options - Standard fetch options.\n * @throws When not connected, handshake did not include `authToken` and\n * `extensionDetails` (`baseUrl`, `tenantDomain`), the endpoint is invalid,\n * the request fails, or the response is not successful JSON.\n */\n apiCall<T>(endpoint: string, options?: RequestInit): Promise<T>;\n\n /**\n * Asks the parent for a new auth token and waits for `TOKEN_REFRESH`.\n *\n * @param options - Optional request timeout (default 10s).\n * @throws When not connected, a refresh is already in progress,\n * or the request times out / returns an invalid payload.\n */\n emitRequestAuthTokenRefresh(\n options?: RequestOptions,\n ): Promise<AuthToken>;\n\n /**\n * Pushes page settings to the parent and waits for `PAGE_SETTINGS_UPDATED`.\n *\n * @param payload - Update payload containing the settings object.\n * @param options - Optional request timeout (default 10s).\n * @throws When not connected, an update is already in progress,\n * or the request times out / returns an invalid payload.\n */\n emitUpdatePageSettings(\n payload: UpdatePageSettingsPayload,\n options?: RequestOptions,\n ): Promise<PageSettingsUpdatedPayload>;\n\n /**\n * Registers a handler for parent-initiated `LOAD_PAGE_SETTINGS`.\n *\n * @returns Unsubscribe function.\n */\n onLoadPageSettings(handler: (settings: PageSettings) => void): () => void;\n\n /**\n * Registers a handler for parent-initiated `REQUEST_NAVIGATION_CHANGE`.\n * Navigate the SPA to `payload.path` and acknowledge with\n * {@link emitNavigationChange}.\n *\n * @returns Unsubscribe function.\n */\n onNavigate(\n handler: (payload: RequestNavigationChangePayload) => void,\n ): () => void;\n\n /**\n * Registers a handler for auth-token updates (requested or parent-pushed).\n *\n * @returns Unsubscribe function.\n */\n onAuthTokenRefresh(handler: (authToken: AuthToken) => void): () => void;\n\n /**\n * Notifies the parent that the extension's route changed.\n *\n * Wire: `NAVIGATION_CHANGE` (fire-and-forget). Also used as the ACK after\n * handling {@link onNavigate}.\n *\n * @param payload - Navigation change payload (`path` within the extension).\n * @throws When not connected or destroyed.\n */\n emitNavigationChange(payload: NavigationChangePayload): void;\n}\n\n/** Default {@link IExtensionSDK} implementation. Prefer {@link createExtensionSDK}. */\nexport class ExtensionSDK implements IExtensionSDK {\n #allowedOrigin: string | null = null;\n #tenantUrl: string | null = null;\n #unsubscribe: (() => void) | null = null;\n #connected = false;\n #destroyed = false;\n\n #authToken: AuthToken | null = null;\n #extensionDetails: ExtensionDetails | null = null;\n #designTokens: DesignTokens | null = null;\n #pageSettings: PageSettings | null = null;\n\n #authTokenAutoRefresh = false;\n #authTokenBufferMs = DEFAULT_AUTH_TOKEN_BUFFER_MS;\n #authTokenRefreshTimer: ReturnType<typeof setTimeout> | null = null;\n #authTokenRefreshRetryUsed = false;\n\n #pendingHandshake: PendingRequest<HandshakePayload> | null = null;\n #handshakePromise: Promise<HandshakePayload> | null = null;\n #pendingAuthTokenRefresh: PendingRequest<AuthToken> | null = null;\n #pendingPageSettingsUpdate: PendingRequest<PageSettingsUpdatedPayload> | null =\n null;\n\n #loadPageSettingsHandlers = new Set<(settings: PageSettings) => void>();\n #navigateHandlers = new Set<\n (payload: RequestNavigationChangePayload) => void\n >();\n #authTokenRefreshHandlers = new Set<(authToken: AuthToken) => void>();\n\n get isConnected(): boolean {\n return this.#connected;\n }\n\n async connect(options: ConnectOptions = {}): Promise<HandshakePayload> {\n this.#assertNotDestroyed();\n\n if (this.#connected) {\n this.#configureAuthTokenAutoRefresh(options);\n this.#scheduleProactiveAuthTokenRefresh();\n return this.#toHandshakePayload();\n }\n\n if (this.#handshakePromise) {\n return this.#handshakePromise;\n }\n\n this.#configureAuthTokenAutoRefresh(options);\n\n const tenantUrl = readTenantUrlFromLocation();\n this.#allowedOrigin = resolveAllowedOrigin(tenantUrl);\n this.#tenantUrl = tenantUrl;\n\n this.#unsubscribe?.();\n this.#unsubscribe = subscribeToParentMessages(\n this.#allowedOrigin,\n (event) => this.#onMessage(event),\n );\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const pending = PendingRequest.create<HandshakePayload>({\n timeoutMs,\n timeoutMessage: `Handshake timed out after ${timeoutMs}ms.`,\n onSettle: () => {\n if (this.#pendingHandshake === pending) {\n this.#pendingHandshake = null;\n }\n this.#handshakePromise = null;\n },\n });\n this.#pendingHandshake = pending;\n this.#handshakePromise = pending.promise;\n\n const initPayload: HandshakeInitPayload = {\n routingType: normalizeRoutingType(options.routingType),\n };\n\n try {\n postToParent(\n CONNECTION_EVENTS.handshakeInit,\n this.#allowedOrigin,\n initPayload,\n );\n } catch (error) {\n pending.abandon();\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n this.#allowedOrigin = null;\n this.#tenantUrl = null;\n this.#clearProactiveAuthTokenRefreshTimer();\n throw error;\n }\n\n return pending.promise;\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.#connected = false;\n\n this.#clearProactiveAuthTokenRefreshTimer();\n this.#authTokenAutoRefresh = false;\n this.#authTokenRefreshRetryUsed = false;\n\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n this.#allowedOrigin = null;\n this.#tenantUrl = null;\n\n this.#pendingHandshake?.reject(new Error(\"SDK destroyed.\"));\n this.#pendingAuthTokenRefresh?.reject(new Error(\"SDK destroyed.\"));\n this.#pendingPageSettingsUpdate?.reject(new Error(\"SDK destroyed.\"));\n this.#pendingHandshake = null;\n this.#handshakePromise = null;\n this.#pendingAuthTokenRefresh = null;\n this.#pendingPageSettingsUpdate = null;\n\n this.#loadPageSettingsHandlers.clear();\n this.#navigateHandlers.clear();\n this.#authTokenRefreshHandlers.clear();\n\n this.#authToken = null;\n this.#extensionDetails = null;\n this.#designTokens = null;\n this.#pageSettings = null;\n }\n\n getAuthToken(): AuthToken | null {\n return this.#authToken;\n }\n\n getExtensionDetails(): ExtensionDetails | null {\n return this.#extensionDetails;\n }\n\n getDesignTokens(): DesignTokens | null {\n return this.#designTokens;\n }\n\n getPageSettings(): PageSettings | null {\n return this.#pageSettings;\n }\n\n getAllowedOrigin(): string | null {\n return this.#allowedOrigin;\n }\n\n getTenantUrl(): string | null {\n return this.#tenantUrl;\n }\n\n getApiUrl(): string | null {\n const baseUrl = this.#extensionDetails?.baseUrl;\n if (!baseUrl) {\n return null;\n }\n\n try {\n return deriveApiUrl(baseUrl);\n } catch {\n return null;\n }\n }\n\n async apiCall<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n this.#assertConnected();\n\n const baseUrl = this.#extensionDetails?.baseUrl;\n const tenantDomain = this.#extensionDetails?.tenantDomain;\n const authTokenJwt = this.#authToken?.jwt;\n\n if (!baseUrl || !tenantDomain || !authTokenJwt) {\n throw new Error(\n \"API calls require handshake authToken and extensionDetails (baseUrl, tenantDomain).\",\n );\n }\n\n return callApi<T>(\n endpoint,\n {\n baseUrl,\n tenantDomain,\n authTokenJwt,\n },\n options,\n );\n }\n\n async emitRequestAuthTokenRefresh(\n options: RequestOptions = {},\n ): Promise<AuthToken> {\n this.#assertConnected();\n\n if (this.#pendingAuthTokenRefresh) {\n throw new Error(\"Auth token refresh already in progress.\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const allowedOrigin = this.#allowedOrigin!;\n\n const pending = PendingRequest.create<AuthToken>({\n timeoutMs,\n timeoutMessage: `Auth token refresh timed out after ${timeoutMs}ms.`,\n onSettle: () => {\n if (this.#pendingAuthTokenRefresh === pending) {\n this.#pendingAuthTokenRefresh = null;\n }\n },\n });\n this.#pendingAuthTokenRefresh = pending;\n\n postToParent(CONNECTION_EVENTS.requestAuthTokenRefresh, allowedOrigin);\n return pending.promise;\n }\n\n async emitUpdatePageSettings(\n payload: UpdatePageSettingsPayload,\n options: RequestOptions = {},\n ): Promise<PageSettingsUpdatedPayload> {\n this.#assertConnected();\n\n if (this.#pendingPageSettingsUpdate) {\n throw new Error(\"Page settings update already in progress.\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const allowedOrigin = this.#allowedOrigin!;\n\n const pending = PendingRequest.create<PageSettingsUpdatedPayload>({\n timeoutMs,\n timeoutMessage: `Page settings update timed out after ${timeoutMs}ms.`,\n onSettle: () => {\n if (this.#pendingPageSettingsUpdate === pending) {\n this.#pendingPageSettingsUpdate = null;\n }\n },\n });\n this.#pendingPageSettingsUpdate = pending;\n\n postToParent(CONNECTION_EVENTS.updatePageSettings, allowedOrigin, payload);\n return pending.promise;\n }\n\n onLoadPageSettings(handler: (settings: PageSettings) => void): () => void {\n this.#loadPageSettingsHandlers.add(handler);\n return () => {\n this.#loadPageSettingsHandlers.delete(handler);\n };\n }\n\n onNavigate(\n handler: (payload: RequestNavigationChangePayload) => void,\n ): () => void {\n this.#navigateHandlers.add(handler);\n return () => {\n this.#navigateHandlers.delete(handler);\n };\n }\n\n onAuthTokenRefresh(handler: (authToken: AuthToken) => void): () => void {\n this.#authTokenRefreshHandlers.add(handler);\n return () => {\n this.#authTokenRefreshHandlers.delete(handler);\n };\n }\n\n emitNavigationChange(payload: NavigationChangePayload): void {\n this.#assertConnected();\n postToParent(\n CONNECTION_EVENTS.navigationChange,\n this.#allowedOrigin!,\n payload,\n );\n }\n\n #onMessage(event: MessageEvent<ExtensionMessage>): void {\n const { type, payload } = event.data;\n\n switch (type) {\n case CONNECTION_EVENTS.handshakeAck: {\n this.#handleHandshakeAck(payload as HandshakePayload | undefined);\n break;\n }\n case CONNECTION_EVENTS.authTokenRefresh: {\n this.#handleAuthTokenRefresh(payload as AuthTokenRefreshPayload | undefined);\n break;\n }\n case CONNECTION_EVENTS.loadPageSettings: {\n this.#handleLoadPageSettings(payload as PageSettings | undefined);\n break;\n }\n case CONNECTION_EVENTS.requestNavigationChange: {\n this.#handleRequestNavigationChange(\n payload as RequestNavigationChangePayload | undefined,\n );\n break;\n }\n case CONNECTION_EVENTS.pageSettingsUpdated: {\n this.#handlePageSettingsUpdated(\n payload as PageSettingsUpdatedPayload | undefined,\n );\n break;\n }\n default: {\n break;\n }\n }\n }\n\n #toHandshakePayload(): HandshakePayload {\n return {\n authToken: this.#authToken,\n extensionDetails: this.#extensionDetails,\n designTokens: this.#designTokens,\n pageSettings: this.#pageSettings,\n };\n }\n\n #handleHandshakeAck(payload: HandshakePayload | undefined): void {\n this.#authToken = isPlainObject(payload?.authToken)\n ? (payload?.authToken as AuthToken)\n : null;\n this.#extensionDetails = isPlainObject(payload?.extensionDetails)\n ? (payload?.extensionDetails as ExtensionDetails)\n : null;\n this.#designTokens = isPlainObject(payload?.designTokens)\n ? (payload?.designTokens as DesignTokens)\n : null;\n this.#pageSettings = isPlainObject(payload?.pageSettings)\n ? (payload?.pageSettings as PageSettings)\n : null;\n this.#connected = true;\n\n this.#pendingHandshake?.resolve(this.#toHandshakePayload());\n\n this.#authTokenRefreshRetryUsed = false;\n this.#scheduleProactiveAuthTokenRefresh();\n }\n\n #handleAuthTokenRefresh(payload: AuthTokenRefreshPayload | undefined): void {\n if (!payload?.authToken) {\n this.#pendingAuthTokenRefresh?.reject(\n new Error(\"Invalid TOKEN_REFRESH payload.\"),\n );\n return;\n }\n\n this.#authToken = payload.authToken;\n\n this.#pendingAuthTokenRefresh?.resolve(this.#authToken);\n\n for (const handler of this.#authTokenRefreshHandlers) {\n handler(this.#authToken);\n }\n\n this.#authTokenRefreshRetryUsed = false;\n this.#scheduleProactiveAuthTokenRefresh();\n }\n\n #handleLoadPageSettings(payload: PageSettings | undefined): void {\n const settings = payload ?? {};\n this.#pageSettings = settings;\n\n for (const handler of this.#loadPageSettingsHandlers) {\n handler(settings);\n }\n }\n\n #handleRequestNavigationChange(\n payload: RequestNavigationChangePayload | undefined,\n ): void {\n if (!payload || typeof payload.path !== \"string\") {\n return;\n }\n\n for (const handler of this.#navigateHandlers) {\n handler(payload);\n }\n }\n\n #handlePageSettingsUpdated(\n payload: PageSettingsUpdatedPayload | undefined,\n ): void {\n if (!payload || typeof payload.success !== \"boolean\") {\n this.#pendingPageSettingsUpdate?.reject(\n new Error(\"Invalid PAGE_SETTINGS_UPDATED payload.\"),\n );\n return;\n }\n\n this.#pendingPageSettingsUpdate?.resolve(payload);\n }\n\n #assertNotDestroyed(): void {\n if (this.#destroyed) {\n throw new Error(\"SDK has been destroyed.\");\n }\n }\n\n #assertConnected(): void {\n this.#assertNotDestroyed();\n if (!this.#connected || !this.#allowedOrigin) {\n throw new Error(\"SDK is not connected. Call connect() first.\");\n }\n }\n\n #configureAuthTokenAutoRefresh(options: ConnectOptions): void {\n if (options.authTokenBufferMs !== undefined) {\n if (\n !Number.isFinite(options.authTokenBufferMs) ||\n options.authTokenBufferMs < 0\n ) {\n throw new Error(\n \"authTokenBufferMs must be a finite non-negative number.\",\n );\n }\n }\n\n this.#authTokenAutoRefresh = options.authTokenAutoRefresh === true;\n this.#authTokenBufferMs =\n options.authTokenBufferMs ?? DEFAULT_AUTH_TOKEN_BUFFER_MS;\n this.#authTokenRefreshRetryUsed = false;\n this.#clearProactiveAuthTokenRefreshTimer();\n }\n\n #parseExpiresAtMs(authToken: AuthToken): number | null {\n const expiresSec = Number(authToken.expires);\n if (!Number.isFinite(expiresSec)) {\n return null;\n }\n return expiresSec * 1000;\n }\n\n #getProactiveRefreshDelayMs(authToken: AuthToken): number | null {\n const expiresAtMs = this.#parseExpiresAtMs(authToken);\n if (expiresAtMs === null) {\n return null;\n }\n return Math.max(0, expiresAtMs - this.#authTokenBufferMs - Date.now());\n }\n\n #clearProactiveAuthTokenRefreshTimer(): void {\n if (this.#authTokenRefreshTimer !== null) {\n clearTimeout(this.#authTokenRefreshTimer);\n this.#authTokenRefreshTimer = null;\n }\n }\n\n #scheduleProactiveAuthTokenRefresh(): void {\n this.#clearProactiveAuthTokenRefreshTimer();\n\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n !this.#authToken\n ) {\n return;\n }\n\n const delayMs = this.#getProactiveRefreshDelayMs(this.#authToken);\n if (delayMs === null) {\n return;\n }\n\n const clampedDelay = Math.min(delayMs, MAX_TIMEOUT_MS);\n this.#authTokenRefreshTimer = setTimeout(() => {\n this.#authTokenRefreshTimer = null;\n void this.#runProactiveAuthTokenRefresh();\n }, clampedDelay);\n }\n\n async #runProactiveAuthTokenRefresh(): Promise<void> {\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n !this.#authToken\n ) {\n return;\n }\n\n const authTokenAtRequest = this.#authToken;\n const delayMs = this.#getProactiveRefreshDelayMs(authTokenAtRequest);\n if (delayMs === null) {\n return;\n }\n\n if (delayMs > 0) {\n this.#scheduleProactiveAuthTokenRefresh();\n return;\n }\n\n if (this.#pendingAuthTokenRefresh) {\n this.#handleProactiveAuthTokenRefreshFailure(authTokenAtRequest);\n return;\n }\n\n try {\n await this.emitRequestAuthTokenRefresh();\n } catch {\n this.#handleProactiveAuthTokenRefreshFailure(authTokenAtRequest);\n }\n }\n\n #handleProactiveAuthTokenRefreshFailure(authTokenAtRequest: AuthToken): void {\n if (\n !this.#authTokenAutoRefresh ||\n !this.#connected ||\n this.#destroyed ||\n this.#authToken?.jwt !== authTokenAtRequest.jwt\n ) {\n return;\n }\n\n if (this.#authTokenRefreshRetryUsed) {\n return;\n }\n\n const expiresAtMs = this.#parseExpiresAtMs(authTokenAtRequest);\n if (expiresAtMs === null) {\n return;\n }\n\n const remainingMs = expiresAtMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n this.#authTokenRefreshRetryUsed = true;\n this.#clearProactiveAuthTokenRefreshTimer();\n\n const retryDelay = Math.min(AUTO_REFRESH_RETRY_DELAY_MS, remainingMs);\n this.#authTokenRefreshTimer = setTimeout(() => {\n this.#authTokenRefreshTimer = null;\n void this.#runProactiveAuthTokenRefresh();\n }, retryDelay);\n }\n}\n","import type { ConnectionEvent } from \"./events\";\nimport type { ExtensionMessage } from \"./types\";\n\nexport type HostMessageHandler = (\n event: MessageEvent<ExtensionMessage>,\n) => void;\n\n/**\n * Posts a typed extension message to an embedded child frame.\n *\n * @param contentWindow - The iframe's `contentWindow`.\n * @param origin - Target origin for `postMessage` (extension origin).\n * @param type - Wire event type from {@link CONNECTION_EVENTS}.\n * @param payload - Optional message payload.\n */\nexport function postToChild(\n contentWindow: Window,\n origin: string,\n type: ConnectionEvent,\n payload?: unknown,\n): void {\n const message: ExtensionMessage =\n payload === undefined ? { type } : { type, payload };\n contentWindow.postMessage(message, origin);\n}\n\n/**\n * Subscribes to `message` events from a child frame at `origin`.\n *\n * Ignores events from other origins and payloads without a `type` field.\n *\n * @returns Unsubscribe function.\n */\nexport function subscribeToChildMessages(\n origin: string,\n handler: HostMessageHandler,\n): () => void {\n const listener = (event: MessageEvent) => {\n if (event.origin !== origin) {\n return;\n }\n\n if (!event.data || typeof event.data !== \"object\" || !(\"type\" in event.data)) {\n return;\n }\n\n handler(event as MessageEvent<ExtensionMessage>);\n };\n\n window.addEventListener(\"message\", listener);\n return () => window.removeEventListener(\"message\", listener);\n}\n","import { CONNECTION_EVENTS } from \"./events\";\nimport { postToChild, subscribeToChildMessages } from \"./hostMessaging\";\nimport { PendingRequest } from \"./PendingRequest\";\nimport type {\n AuthToken,\n ExtensionMessage,\n HandshakeInitPayload,\n HandshakePayload,\n NavigationChangePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n RequestNavigationChangePayload,\n RequestOptions,\n RoutingType,\n UpdatePageSettingsPayload,\n} from \"./types\";\nimport {\n DEFAULT_ROUTING_TYPE,\n normalizeNavigationSearch,\n normalizeRoutingType,\n} from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/** Correlation key for a navigation request/ACK (path + normalized query). */\nfunction navigationKey(path: string, search?: string): string {\n return `${path}\\u0000${normalizeNavigationSearch(search)}`;\n}\n\n/**\n * Options for {@link ExtensionSDKHost}.\n *\n * Platform code supplies token minting and page-settings persistence via\n * callbacks; the host owns only the postMessage protocol.\n */\nexport interface ExtensionSDKHostOptions {\n /** Returns the iframe `contentWindow`, or `null` if not ready. */\n getContentWindow: () => Window | null;\n /** Extension origin used as `postMessage` targetOrigin / source filter. */\n origin: string;\n /**\n * Builds the handshake payload when the child sends `HANDSHAKE_INIT`.\n * Context fields are optional; send `designTokens` whenever a tenant is in\n * context. `spaceId` / `pageId` apply to Experience Pages only.\n */\n resolveHandshakePayload: (\n extensionSDKHost: IExtensionSDKHost,\n ) => Partial<HandshakePayload> | Promise<Partial<HandshakePayload>>;\n /**\n * Resolves a fresh auth token when the child sends `REQUEST_TOKEN_REFRESH`.\n */\n resolveAuthToken: () => AuthToken | Promise<AuthToken>;\n /**\n * Persists page settings from `UPDATE_PAGE_SETTINGS`.\n * Return `true` on success; the host emits `PAGE_SETTINGS_UPDATED`.\n */\n onUpdatePageSettings?: (\n settings: PageSettings,\n ) => boolean | Promise<boolean>;\n /**\n * Called when the child reports a route change via `NAVIGATION_CHANGE`\n * (spontaneous or as ACK to {@link IExtensionSDKHost.requestNavigationChange}).\n */\n onNavigationChange?: (payload: NavigationChangePayload) => void;\n}\n\n/**\n * Parent-frame counterpart to {@link IExtensionSDK}.\n *\n * Listens for child messages and replies with the shared wire protocol.\n * Create one instance per iframe.\n */\nexport interface IExtensionSDKHost {\n /**\n * Attaches the child message listener. Safe to call once; no-ops if already\n * started or destroyed.\n */\n start(): void;\n\n /**\n * Tears down the listener. The instance cannot be restarted after destroy.\n */\n destroy(): void;\n\n /**\n * Routing type from the last `HANDSHAKE_INIT`. Defaults to `server` until\n * the child connects (and for omit / unrecognized values).\n */\n getRoutingType(): RoutingType;\n\n /**\n * Asks the child to open page settings UI with the given values.\n *\n * Wire: `LOAD_PAGE_SETTINGS` (fire-and-forget).\n */\n emitLoadPageSettings(settings: PageSettings | null): void;\n\n /**\n * Asks the child SPA to navigate to `payload.path` without reloading the\n * iframe. Resolves when the child ACKs with a matching `NAVIGATION_CHANGE`.\n *\n * Soft navigation is intended for `client-hash` extensions; the host decides\n * whether to call this or fall back to an iframe `src` reload.\n *\n * @param payload - Target path within the extension.\n * @param options - Optional request timeout (default 10s).\n * @throws When destroyed, a request is already in progress, the payload is\n * invalid, or the request times out.\n */\n requestNavigationChange(\n payload: RequestNavigationChangePayload,\n options?: RequestOptions,\n ): Promise<NavigationChangePayload>;\n}\n\n/** Default {@link IExtensionSDKHost} implementation. */\nexport class ExtensionSDKHost implements IExtensionSDKHost {\n #options: ExtensionSDKHostOptions;\n #unsubscribe: (() => void) | null = null;\n #started = false;\n #destroyed = false;\n #routingType: RoutingType = DEFAULT_ROUTING_TYPE;\n #pendingNavigationChange: PendingRequest<\n NavigationChangePayload,\n string\n > | null = null;\n\n constructor(options: ExtensionSDKHostOptions) {\n this.#options = options;\n }\n\n start(): void {\n if (this.#destroyed || this.#started) {\n return;\n }\n\n this.#unsubscribe = subscribeToChildMessages(\n this.#options.origin,\n (event) => {\n void this.#onMessage(event);\n },\n );\n this.#started = true;\n }\n\n destroy(): void {\n this.#destroyed = true;\n this.#started = false;\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n this.#routingType = DEFAULT_ROUTING_TYPE;\n this.#pendingNavigationChange?.reject(\n new Error(\"ExtensionSDKHost has been destroyed.\"),\n );\n this.#pendingNavigationChange = null;\n }\n\n getRoutingType(): RoutingType {\n return this.#routingType;\n }\n\n emitLoadPageSettings(settings: PageSettings | null): void {\n this.#assertNotDestroyed();\n this.#post(CONNECTION_EVENTS.loadPageSettings, settings);\n }\n\n async requestNavigationChange(\n payload: RequestNavigationChangePayload,\n options: RequestOptions = {},\n ): Promise<NavigationChangePayload> {\n this.#assertNotDestroyed();\n\n if (!payload || typeof payload.path !== \"string\") {\n throw new Error(\"Invalid REQUEST_NAVIGATION_CHANGE payload.\");\n }\n\n if (this.#pendingNavigationChange) {\n throw new Error(\"Navigation change request already in progress.\");\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const pending = PendingRequest.create<NavigationChangePayload, string>({\n timeoutMs,\n timeoutMessage: `Navigation change timed out after ${timeoutMs}ms.`,\n context: navigationKey(payload.path, payload.search),\n onSettle: () => {\n if (this.#pendingNavigationChange === pending) {\n this.#pendingNavigationChange = null;\n }\n },\n });\n this.#pendingNavigationChange = pending;\n\n this.#post(CONNECTION_EVENTS.requestNavigationChange, payload);\n return pending.promise;\n }\n\n async #onMessage(event: MessageEvent<ExtensionMessage>): Promise<void> {\n if (this.#destroyed) {\n return;\n }\n\n const { type, payload } = event.data;\n\n switch (type) {\n case CONNECTION_EVENTS.handshakeInit: {\n await this.#handleHandshakeInit(\n payload as HandshakeInitPayload | undefined,\n );\n break;\n }\n case CONNECTION_EVENTS.requestAuthTokenRefresh: {\n await this.#handleAuthTokenRefresh();\n break;\n }\n case CONNECTION_EVENTS.updatePageSettings: {\n await this.#handleUpdatePageSettings(\n payload as UpdatePageSettingsPayload | undefined,\n );\n break;\n }\n case CONNECTION_EVENTS.navigationChange: {\n this.#handleNavigationChange(\n payload as NavigationChangePayload | undefined,\n );\n break;\n }\n default: {\n break;\n }\n }\n }\n\n async #handleHandshakeInit(\n payload: HandshakeInitPayload | undefined,\n ): Promise<void> {\n this.#routingType = normalizeRoutingType(payload?.routingType);\n\n try {\n const handshakePayload =\n await this.#options.resolveHandshakePayload(this);\n if (this.#destroyed) {\n return;\n }\n this.#post(CONNECTION_EVENTS.handshakeAck, handshakePayload);\n } catch {\n // Platform owns error UI / retry; do not reply with a partial ACK.\n }\n }\n\n async #handleAuthTokenRefresh(): Promise<void> {\n try {\n const authToken = await this.#options.resolveAuthToken();\n if (this.#destroyed) {\n return;\n }\n this.#post(CONNECTION_EVENTS.authTokenRefresh, { authToken });\n } catch {\n // Platform owns error UI / retry; do not reply with an invalid token.\n }\n }\n\n async #handleUpdatePageSettings(\n payload: UpdatePageSettingsPayload | undefined,\n ): Promise<void> {\n const settings = payload?.settings;\n if (!settings || typeof settings !== \"object\") {\n this.#postPageSettingsUpdated(false);\n return;\n }\n\n const handler = this.#options.onUpdatePageSettings;\n if (!handler) {\n this.#postPageSettingsUpdated(false);\n return;\n }\n\n try {\n const success = await handler(settings);\n if (this.#destroyed) {\n return;\n }\n this.#postPageSettingsUpdated(Boolean(success));\n } catch {\n if (this.#destroyed) {\n return;\n }\n this.#postPageSettingsUpdated(false);\n }\n }\n\n #handleNavigationChange(payload: NavigationChangePayload | undefined): void {\n if (!payload || typeof payload.path !== \"string\") {\n return;\n }\n\n const key = navigationKey(payload.path, payload.search);\n\n if (this.#pendingNavigationChange?.matches((context) => context === key)) {\n this.#pendingNavigationChange.resolve(payload);\n }\n\n this.#options.onNavigationChange?.(payload);\n }\n\n #postPageSettingsUpdated(success: boolean): void {\n const payload: PageSettingsUpdatedPayload = { success };\n this.#post(CONNECTION_EVENTS.pageSettingsUpdated, payload);\n }\n\n #post(\n type: (typeof CONNECTION_EVENTS)[keyof typeof CONNECTION_EVENTS],\n payload?: unknown,\n ): void {\n const contentWindow = this.#options.getContentWindow();\n if (!contentWindow) {\n return;\n }\n postToChild(contentWindow, this.#options.origin, type, payload);\n }\n\n #assertNotDestroyed(): void {\n if (this.#destroyed) {\n throw new Error(\"ExtensionSDKHost has been destroyed.\");\n }\n }\n}\n","/**\n * Returns whether `urlRaw` is a valid HTTPS extension URL.\n *\n * When `checkOrigin` is provided, the URL's origin must also pass that predicate\n * (e.g. hostname ends with `.kadanza.app`).\n */\nexport function isValidExtensionUrl(\n urlRaw: string | null | undefined,\n checkOrigin?: (origin: string) => boolean,\n): boolean {\n if (typeof urlRaw !== \"string\") {\n return false;\n }\n\n try {\n const url = new URL(urlRaw);\n const allowedProtocol = url.protocol === \"https:\";\n const allowedOrigin = checkOrigin ? checkOrigin(url.origin) : true;\n return allowedProtocol && allowedOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * Clones `urlRaw` and sets the `tenantUrl` search param (parent origin).\n *\n * @param urlRaw - Absolute extension URL string.\n * @param tenantUrl - Parent origin to embed (defaults to `window.location.origin`).\n * @returns Enriched `URL`, or `null` when `urlRaw` is missing or unparsable.\n */\nexport function enrichExtensionUrl(\n urlRaw: string | null | undefined,\n tenantUrl: string = window.location.origin,\n): URL | null {\n if (typeof urlRaw !== \"string\") {\n return null;\n }\n\n try {\n const enrichedUrl = new URL(urlRaw);\n\n if (typeof tenantUrl === \"string\") {\n enrichedUrl.searchParams.set(\"tenantUrl\", tenantUrl);\n }\n\n return enrichedUrl;\n } catch {\n return null;\n }\n}\n","import { ExtensionSDK, type IExtensionSDK } from \"./ExtensionSDK\";\n\nexport { ExtensionSDK, type IExtensionSDK } from \"./ExtensionSDK\";\nexport {\n ExtensionSDKHost,\n type ExtensionSDKHostOptions,\n type IExtensionSDKHost,\n} from \"./ExtensionSDKHost\";\nexport { CONNECTION_EVENTS, type ConnectionEvent } from \"./events\";\nexport {\n postToChild,\n subscribeToChildMessages,\n type HostMessageHandler,\n} from \"./hostMessaging\";\nexport { enrichExtensionUrl, isValidExtensionUrl } from \"./hostUrl\";\nexport {\n InvalidOriginError,\n readTenantUrlFromLocation,\n resolveAllowedOrigin,\n} from \"./origin\";\nexport type {\n AuthToken,\n AuthTokenRefreshPayload,\n ConnectOptions,\n DesignTokens,\n ExtensionDetails,\n ExtensionMessage,\n HandshakeInitPayload,\n HandshakePayload,\n NavigationChangePayload,\n PageSettings,\n PageSettingsUpdatedPayload,\n RequestNavigationChangePayload,\n RequestOptions,\n RoutingType,\n UpdatePageSettingsPayload,\n} from \"./types\";\nexport { DEFAULT_ROUTING_TYPE, normalizeRoutingType } from \"./types\";\n\nlet instance: IExtensionSDK | null = null;\nlet hasWarnedDuplicateCreate = false;\n\n/** Clears the factory singleton when the managed instance is destroyed. */\nclass ManagedExtensionSDK extends ExtensionSDK {\n destroy(): void {\n super.destroy();\n if (instance === this) {\n instance = null;\n hasWarnedDuplicateCreate = false;\n }\n }\n}\n\n/**\n * Returns the shared {@link IExtensionSDK} for the current extension frame.\n *\n * Creates the instance on first call. Later calls return the same instance and\n * log a console warning — use the SDK once per app. After {@link IExtensionSDK.destroy},\n * the next call creates a fresh instance.\n */\nexport const createExtensionSDK = (): IExtensionSDK => {\n if (instance) {\n if (!hasWarnedDuplicateCreate) {\n console.warn(\n `[${__PACKAGE_NAME__}] createExtensionSDK() was called more than once. ` +\n \"The SDK is a singleton — reuse the same instance for the whole app.\",\n );\n hasWarnedDuplicateCreate = true;\n }\n return instance;\n }\n\n instance = new ManagedExtensionSDK();\n return instance;\n};\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -31,7 +31,9 @@ export declare const CONNECTION_EVENTS: {
31
31
  readonly updatePageSettings: "UPDATE_PAGE_SETTINGS";
32
32
  /** Parent → child: result of a page settings update. */
33
33
  readonly pageSettingsUpdated: "PAGE_SETTINGS_UPDATED";
34
- /** Childparent: extension route changed. */
34
+ /** Parentchild: ask the SPA to navigate without reloading the iframe. */
35
+ readonly requestNavigationChange: "REQUEST_NAVIGATION_CHANGE";
36
+ /** Child → parent: extension route changed (ack or spontaneous). */
35
37
  readonly navigationChange: "NAVIGATION_CHANGE";
36
38
  };
37
39
 
@@ -42,6 +44,11 @@ export declare type ConnectionEvent = (typeof CONNECTION_EVENTS)[keyof typeof CO
42
44
  export declare interface ConnectOptions {
43
45
  /** Handshake timeout in milliseconds (default 10_000). */
44
46
  timeoutMs?: number;
47
+ /**
48
+ * Routing mode announced to the parent on `HANDSHAKE_INIT`.
49
+ * HashRouter SPAs should pass `client-hash`. Default: `server`.
50
+ */
51
+ routingType?: RoutingType;
45
52
  /**
46
53
  * Enables proactive auth-token refresh. After the handshake, the SDK reads
47
54
  * the auth token's Unix-seconds `expires` value and arms a one-shot timer
@@ -78,6 +85,9 @@ export declare interface ConnectOptions {
78
85
  */
79
86
  export declare const createExtensionSDK: () => IExtensionSDK;
80
87
 
88
+ /** Default when `routingType` is omitted or unrecognized. */
89
+ export declare const DEFAULT_ROUTING_TYPE: RoutingType;
90
+
81
91
  /**
82
92
  * Tenant branding from the parent, for aligning extension UI with the host.
83
93
  *
@@ -153,6 +163,7 @@ export declare class ExtensionSDK implements IExtensionSDK {
153
163
  emitRequestAuthTokenRefresh(options?: RequestOptions): Promise<AuthToken>;
154
164
  emitUpdatePageSettings(payload: UpdatePageSettingsPayload, options?: RequestOptions): Promise<PageSettingsUpdatedPayload>;
155
165
  onLoadPageSettings(handler: (settings: PageSettings) => void): () => void;
166
+ onNavigate(handler: (payload: RequestNavigationChangePayload) => void): () => void;
156
167
  onAuthTokenRefresh(handler: (authToken: AuthToken) => void): () => void;
157
168
  emitNavigationChange(payload: NavigationChangePayload): void;
158
169
  }
@@ -163,7 +174,9 @@ export declare class ExtensionSDKHost implements IExtensionSDKHost {
163
174
  constructor(options: ExtensionSDKHostOptions);
164
175
  start(): void;
165
176
  destroy(): void;
177
+ getRoutingType(): RoutingType;
166
178
  emitLoadPageSettings(settings: PageSettings | null): void;
179
+ requestNavigationChange(payload: RequestNavigationChangePayload, options?: RequestOptions): Promise<NavigationChangePayload>;
167
180
  }
168
181
 
169
182
  /**
@@ -182,7 +195,7 @@ export declare interface ExtensionSDKHostOptions {
182
195
  * Context fields are optional; send `designTokens` whenever a tenant is in
183
196
  * context. `spaceId` / `pageId` apply to Experience Pages only.
184
197
  */
185
- resolveHandshakePayload: () => Partial<HandshakePayload> | Promise<Partial<HandshakePayload>>;
198
+ resolveHandshakePayload: (extensionSDKHost: IExtensionSDKHost) => Partial<HandshakePayload> | Promise<Partial<HandshakePayload>>;
186
199
  /**
187
200
  * Resolves a fresh auth token when the child sends `REQUEST_TOKEN_REFRESH`.
188
201
  */
@@ -192,6 +205,20 @@ export declare interface ExtensionSDKHostOptions {
192
205
  * Return `true` on success; the host emits `PAGE_SETTINGS_UPDATED`.
193
206
  */
194
207
  onUpdatePageSettings?: (settings: PageSettings) => boolean | Promise<boolean>;
208
+ /**
209
+ * Called when the child reports a route change via `NAVIGATION_CHANGE`
210
+ * (spontaneous or as ACK to {@link IExtensionSDKHost.requestNavigationChange}).
211
+ */
212
+ onNavigationChange?: (payload: NavigationChangePayload) => void;
213
+ }
214
+
215
+ /** Payload for `HANDSHAKE_INIT` (child → parent). */
216
+ export declare interface HandshakeInitPayload {
217
+ /**
218
+ * Declares how the extension routes. Omit or unknown → `server`.
219
+ * Soft navigation requires `client-hash`.
220
+ */
221
+ routingType?: RoutingType;
195
222
  }
196
223
 
197
224
  /**
@@ -230,13 +257,16 @@ export declare interface IExtensionSDK {
230
257
  * Opt-in proactive refresh can be enabled with
231
258
  * `authTokenAutoRefresh: true` (optional `authTokenBufferMs`).
232
259
  *
260
+ * Pass `routingType: "client-hash"` for HashRouter SPAs so the parent can
261
+ * soft-navigate without reloading the iframe. Default is `"server"`.
262
+ *
233
263
  * Handshake completes when the parent sends `HANDSHAKE_ACK`. Context fields
234
264
  * (`authToken`, `extensionDetails`, `designTokens`, `pageSettings`) are
235
265
  * optional — omitted values are `null`. Use them only when the current host
236
266
  * surface provides them (e.g. `spaceId` / `pageId` on Experience Pages,
237
267
  * `designTokens` when a tenant is in context).
238
268
  *
239
- * @param options - Handshake timeout and optional auth-token auto-refresh.
269
+ * @param options - Handshake timeout, routing type, and optional auth-token auto-refresh.
240
270
  * @throws {InvalidOriginError} When `tenantUrl` is missing or invalid.
241
271
  * @throws When not embedded in a parent frame, destroyed, or the handshake
242
272
  * times out.
@@ -323,6 +353,14 @@ export declare interface IExtensionSDK {
323
353
  * @returns Unsubscribe function.
324
354
  */
325
355
  onLoadPageSettings(handler: (settings: PageSettings) => void): () => void;
356
+ /**
357
+ * Registers a handler for parent-initiated `REQUEST_NAVIGATION_CHANGE`.
358
+ * Navigate the SPA to `payload.path` and acknowledge with
359
+ * {@link emitNavigationChange}.
360
+ *
361
+ * @returns Unsubscribe function.
362
+ */
363
+ onNavigate(handler: (payload: RequestNavigationChangePayload) => void): () => void;
326
364
  /**
327
365
  * Registers a handler for auth-token updates (requested or parent-pushed).
328
366
  *
@@ -332,7 +370,8 @@ export declare interface IExtensionSDK {
332
370
  /**
333
371
  * Notifies the parent that the extension's route changed.
334
372
  *
335
- * Wire: `NAVIGATION_CHANGE` (fire-and-forget).
373
+ * Wire: `NAVIGATION_CHANGE` (fire-and-forget). Also used as the ACK after
374
+ * handling {@link onNavigate}.
336
375
  *
337
376
  * @param payload - Navigation change payload (`path` within the extension).
338
377
  * @throws When not connected or destroyed.
@@ -356,12 +395,30 @@ export declare interface IExtensionSDKHost {
356
395
  * Tears down the listener. The instance cannot be restarted after destroy.
357
396
  */
358
397
  destroy(): void;
398
+ /**
399
+ * Routing type from the last `HANDSHAKE_INIT`. Defaults to `server` until
400
+ * the child connects (and for omit / unrecognized values).
401
+ */
402
+ getRoutingType(): RoutingType;
359
403
  /**
360
404
  * Asks the child to open page settings UI with the given values.
361
405
  *
362
406
  * Wire: `LOAD_PAGE_SETTINGS` (fire-and-forget).
363
407
  */
364
408
  emitLoadPageSettings(settings: PageSettings | null): void;
409
+ /**
410
+ * Asks the child SPA to navigate to `payload.path` without reloading the
411
+ * iframe. Resolves when the child ACKs with a matching `NAVIGATION_CHANGE`.
412
+ *
413
+ * Soft navigation is intended for `client-hash` extensions; the host decides
414
+ * whether to call this or fall back to an iframe `src` reload.
415
+ *
416
+ * @param payload - Target path within the extension.
417
+ * @param options - Optional request timeout (default 10s).
418
+ * @throws When destroyed, a request is already in progress, the payload is
419
+ * invalid, or the request times out.
420
+ */
421
+ requestNavigationChange(payload: RequestNavigationChangePayload, options?: RequestOptions): Promise<NavigationChangePayload>;
365
422
  }
366
423
 
367
424
  /** Thrown when `tenantUrl` is missing or not a safe postMessage origin. */
@@ -379,9 +436,21 @@ export declare function isValidExtensionUrl(urlRaw: string | null | undefined, c
379
436
 
380
437
  /** Payload for `NAVIGATION_CHANGE` (child → parent). */
381
438
  export declare interface NavigationChangePayload {
439
+ /** Path within the extension (e.g. `/settings`). */
382
440
  path: string;
441
+ /**
442
+ * Query string of the child's current route, including the leading `?`
443
+ * (e.g. `?tab=history`). Omitted or empty means no query.
444
+ *
445
+ * For `client-hash` extensions the child reads this from its hash fragment,
446
+ * not from `window.location.search`.
447
+ */
448
+ search?: string;
383
449
  }
384
450
 
451
+ /** Normalize a wire / option value to a known {@link RoutingType}. */
452
+ export declare function normalizeRoutingType(value: unknown): RoutingType;
453
+
385
454
  /** Opaque page-level settings bag owned by the extension and synced with the parent. */
386
455
  export declare type PageSettings = Record<string, unknown>;
387
456
 
@@ -410,6 +479,20 @@ export declare function postToChild(contentWindow: Window, origin: string, type:
410
479
  */
411
480
  export declare function readTenantUrlFromLocation(search?: string): string | null;
412
481
 
482
+ /** Payload for `REQUEST_NAVIGATION_CHANGE` (parent → child). */
483
+ export declare interface RequestNavigationChangePayload {
484
+ /** Path within the extension (e.g. `/settings`). */
485
+ path: string;
486
+ /**
487
+ * Query string for the target route, including the leading `?`
488
+ * (e.g. `?tab=history`). Omitted or empty means no query.
489
+ *
490
+ * For `client-hash` extensions the query belongs inside the hash fragment;
491
+ * the child SDK consumer is responsible for applying it to its router.
492
+ */
493
+ search?: string;
494
+ }
495
+
413
496
  /** Options for request/response SDK methods that wait on a parent reply. */
414
497
  export declare interface RequestOptions {
415
498
  /** Request timeout in milliseconds (default 10_000). */
@@ -426,6 +509,15 @@ export declare interface RequestOptions {
426
509
  */
427
510
  export declare function resolveAllowedOrigin(tenantUrl: string | null): string;
428
511
 
512
+ /**
513
+ * How the extension handles in-app routing.
514
+ *
515
+ * - `server` — full document loads (iframe `src` reload). Default.
516
+ * - `client-hash` — hash router SPA; supports soft navigation via
517
+ * `REQUEST_NAVIGATION_CHANGE` / `NAVIGATION_CHANGE`.
518
+ */
519
+ export declare type RoutingType = "server" | "client-hash";
520
+
429
521
  /**
430
522
  * Subscribes to `message` events from a child frame at `origin`.
431
523
  *