export const HOST_BRIDGE_PROTOCOL = 'GenarrativeHostBridge'; export const HOST_BRIDGE_VERSION = 1; export type HostShellKind = 'browser' | 'wechat_mini_program' | 'native_app'; export type NativeHostShell = 'expo_mobile' | 'tauri_desktop'; export type NativeHostPlatform = | 'ios' | 'android' | 'macos' | 'windows' | 'linux' | 'unknown'; export type HostBridgeMethod = | 'host.getRuntime' | 'auth.requestLogin' | 'payment.request' | 'share.setTarget' | 'share.open' | 'navigation.openNativePage' | 'app.openExternalUrl' | 'app.setTitle' | 'clipboard.writeText' | 'haptics.impact'; export type HostBridgeCapability = | HostBridgeMethod | 'host.events' | 'navigation.canGoBack'; export type HostBridgeRuntimeResult = { shell: NativeHostShell; platform: NativeHostPlatform; hostVersion: string | null; bridgeVersion: number; capabilities: HostBridgeCapability[]; }; export type HostBridgeRequest = { bridge: typeof HOST_BRIDGE_PROTOCOL; version: typeof HOST_BRIDGE_VERSION; id: string; method: HostBridgeMethod; payload?: Payload; timeoutMs?: number; }; export type HostBridgeError = { code: | 'invalid_request' | 'unsupported_method' | 'unsupported_capability' | 'timeout' | 'cancelled' | 'host_error'; message: string; }; export type HostBridgeResponse = { bridge: typeof HOST_BRIDGE_PROTOCOL; version: typeof HOST_BRIDGE_VERSION; id: string; } & ( | { ok: true; result?: Result; } | { ok: false; error: HostBridgeError; } ); export type HostBridgeEvent = { bridge: typeof HOST_BRIDGE_PROTOCOL; version: typeof HOST_BRIDGE_VERSION; event: string; payload?: Payload; }; export type NavigateNativePagePayload = { url: string; }; export type OpenExternalUrlPayload = { url: string; }; export type SetTitlePayload = { title: string; }; export const HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS = [ 'http:', 'https:', 'mailto:', 'tel:', ] as const; export type HostBridgeExternalUrlProtocol = (typeof HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS)[number]; function hasHostBridgeControlCharacter(value: string) { return [...value].some((character) => { const codePoint = character.codePointAt(0) ?? 0; return codePoint <= 31 || codePoint === 127; }); } export function normalizeHostBridgeExternalUrl(rawUrl: unknown) { if (typeof rawUrl !== 'string') { return null; } const urlText = rawUrl.trim(); if (!urlText || hasHostBridgeControlCharacter(urlText)) { return null; } try { const url = new URL(urlText); if ( !HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS.includes( url.protocol as HostBridgeExternalUrlProtocol, ) ) { return null; } return url.toString(); } catch { return null; } } export type ClipboardWriteTextPayload = { text: string; }; export type HapticsImpactPayload = { style?: 'light' | 'medium' | 'heavy'; }; export type ShareSetTargetPayload = { target: unknown; }; export type ShareOpenPayload = { title?: string; message?: string; url?: string; };