import type { WidgetConfigResponse } from "./public-contracts.js"; import { loadAccessibilityCapabilities } from "./accessibility-loader.js"; import type { AccessibilityCapabilities } from "./accessibility/types.js"; import { createWidgetApiClient, type WidgetApiClient } from "./api.js"; import { EMBED_TAG_NAME, INLINE_TAG_NAME, registerEmbedElement, registerInlineElement, } from "./register.js"; import { getOrCreateSessionId } from "./session.js"; import { isAkintuSurfaceElement, type AkintuSurfaceConfiguration, } from "./surface-element.js"; export interface InitAkintuOptions { apiKey: string; apiBaseUrl?: string | undefined; fetchImplementation?: typeof fetch | undefined; root?: ParentNode; } export interface InitAkintuResult { configuredEmbeds: number; mountedInlines: number; } interface LoadedRuntime { accessibility: AccessibilityCapabilities; apiClient: WidgetApiClient; config: WidgetConfigResponse; configuredElements: WeakSet; sessionId: string; } type AccessibilityLoader = typeof loadAccessibilityCapabilities; // A document gets one configuration request and one in-memory credential // closure. Repeated loader calls can discover late React mounts without // duplicating already configured elements. const runtimeByDocument = new WeakMap< Document, Promise >(); let accessibilityLoader: AccessibilityLoader = loadAccessibilityCapabilities; /** * Loads tenant configuration and activates only its enabled page surfaces. * Configuration failure is intentionally represented as an empty result so a * host page never receives a partially initialized widget. */ export async function initAkintu( options: InitAkintuOptions, ): Promise { const root = options.root ?? document; const ownerDocument = getOwnerDocument(root); if (options.apiKey.trim().length === 0) return emptyResult(); let runtimePromise = runtimeByDocument.get(ownerDocument); if (runtimePromise === undefined) { runtimePromise = loadRuntime(options); runtimeByDocument.set(ownerDocument, runtimePromise); } const runtime = await runtimePromise; if (runtime === undefined) return emptyResult(); return mountConfiguredSurfaces(root, ownerDocument, runtime); } /** * Builds the shared per-page runtime. The API key remains reachable only from * the API client closure captured here. */ async function loadRuntime( options: InitAkintuOptions, ): Promise { const apiClient = createWidgetApiClient({ apiKey: options.apiKey, apiBaseUrl: options.apiBaseUrl, fetchImplementation: options.fetchImplementation, }); try { const config = await apiClient.getConfig(); const accessibility = await getAccessibility(config.accessibility); return { accessibility, apiClient, config, configuredElements: new WeakSet(), sessionId: getOrCreateSessionId({ sessionVersion: config.sessionVersion, }), }; } catch { // Configuration is a fail-closed boundary: no surface mounts on failure. return undefined; } } async function getAccessibility( flags: WidgetConfigResponse["accessibility"], ): Promise { // Avoid even evaluating import() when voice support is disabled, keeping the // accessibility modules out of the browser's network graph. if (!flags.stt && !flags.tts) return {}; try { return await accessibilityLoader(flags); } catch { // Voice features are enhancements and must never block text interaction. return {}; } } function mountConfiguredSurfaces( root: ParentNode, ownerDocument: Document, runtime: LoadedRuntime, ): InitAkintuResult { const registry = ownerDocument.defaultView?.customElements; if (registry === undefined) return emptyResult(); let configuredEmbeds = 0; let mountedInlines = 0; // Custom elements are registered only after their remote feature is known to // be enabled. Disabled markup therefore stays inert by contract. if (runtime.config.embedEnabled) { registerEmbedElement(registry); for (const element of findElements(root, EMBED_TAG_NAME)) { if (configureElement(element, runtime)) configuredEmbeds += 1; } } if (runtime.config.inlineEnabled) { registerInlineElement(registry); for (const target of findElements(root, "[data-akintu-context]")) { if (mountInline(target, ownerDocument, runtime)) mountedInlines += 1; } } return { configuredEmbeds, mountedInlines }; } function configureElement( element: Element, runtime: LoadedRuntime, context?: string, ): boolean { if ( runtime.configuredElements.has(element) || !isAkintuSurfaceElement(element) ) { return false; } const configuration: AkintuSurfaceConfiguration = { // Passing the client object keeps the key out of element attributes and // host-visible markup. apiClient: runtime.apiClient, accessibility: runtime.accessibility, sessionId: runtime.sessionId, sessionVersion: runtime.config.sessionVersion, theme: runtime.config.theme, }; if (context !== undefined) configuration.context = context; element.configure(configuration); runtime.configuredElements.add(element); return true; } function mountInline( target: Element, ownerDocument: Document, runtime: LoadedRuntime, ): boolean { // Context comes from untrusted host markup and must satisfy the same length // boundary as ChatRequest before it can be sent to the API. const context = target.getAttribute("data-akintu-context")?.trim(); if (context === undefined || context.length < 1 || context.length > 128) { return false; } // A direct-child lookup makes repeated initialization idempotent without // confusing nested contextual sections with this target's own surface. const existingInline = Array.from(target.children).find( (child) => child.localName === INLINE_TAG_NAME, ); if (existingInline !== undefined) { return configureElement(existingInline, runtime, context); } const inline = ownerDocument.createElement(INLINE_TAG_NAME); target.append(inline); configureElement(inline, runtime, context); return true; } function findElements(root: ParentNode, selector: string): Element[] { const matches: Element[] = []; if (root instanceof Element && root.matches(selector)) matches.push(root); matches.push(...root.querySelectorAll(selector)); return matches; } function getOwnerDocument(root: ParentNode): Document { return root instanceof Document ? root : (root.ownerDocument ?? document); } function emptyResult(): InitAkintuResult { return { configuredEmbeds: 0, mountedInlines: 0 }; } export function setAccessibilityLoaderForTests( loader: AccessibilityLoader, ): void { accessibilityLoader = loader; } export function resetLoaderForTests(ownerDocument: Document): void { runtimeByDocument.delete(ownerDocument); accessibilityLoader = loadAccessibilityCapabilities; }