{"version":3,"sources":["jsdelivr-header.js","/npm/@mentra/sdk@2.1.29/src/types/streams.ts","/npm/@mentra/sdk@2.1.29/src/types/message-types.ts","/npm/@mentra/sdk@2.1.29/src/types/dashboard/index.ts","/npm/@mentra/sdk@2.1.29/src/app/session/dashboard.ts","/npm/@mentra/sdk@2.1.29/src/app/token/utils.ts","/npm/@mentra/sdk@2.1.29/src/types/messages/glasses-to-cloud.ts","/npm/@mentra/sdk@2.1.29/src/types/enums.ts","/npm/@mentra/sdk@2.1.29/src/types/models.ts","/npm/@mentra/sdk@2.1.29/src/types/messages/cloud-to-glasses.ts","/npm/@mentra/sdk@2.1.29/src/types/messages/app-to-cloud.ts","/npm/@mentra/sdk@2.1.29/src/index.ts","/npm/@mentra/sdk@2.1.29/src/utils/bitmap-utils.ts","/npm/@mentra/sdk@2.1.29/src/utils/animation-utils.ts","/npm/@mentra/sdk@2.1.29/src/types/messages/cloud-to-app.ts","/npm/@mentra/sdk@2.1.29/src/types/webhooks.ts","/npm/@mentra/sdk@2.1.29/src/app/server/index.ts","/npm/@mentra/sdk@2.1.29/src/app/session/index.ts","/npm/@mentra/sdk@2.1.29/src/app/session/events.ts","/npm/@mentra/sdk@2.1.29/src/types/index.ts","/npm/@mentra/sdk@2.1.29/src/constants/log-messages/warning.ts","/npm/@mentra/sdk@2.1.29/src/constants/log-messages/logos.ts","/npm/@mentra/sdk@2.1.29/src/utils/permissions-utils.ts","/npm/@mentra/sdk@2.1.29/src/app/session/layouts.ts","/npm/@mentra/sdk@2.1.29/src/app/session/settings.ts","/npm/@mentra/sdk@2.1.29/src/app/session/api-client.ts","/npm/@mentra/sdk@2.1.29/src/logging/logger.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/location.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/camera.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/camera-managed-extension.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/led.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/audio.ts","/npm/@mentra/sdk@2.1.29/src/utils/resource-tracker.ts","/npm/@mentra/sdk@2.1.29/src/app/session/modules/simple-storage.ts","/npm/@mentra/sdk@2.1.29/src/utils/Observable.ts","/npm/@mentra/sdk@2.1.29/src/app/session/device-state.ts","/npm/@mentra/sdk@2.1.29/src/app/webview/index.ts","/npm/@mentra/sdk@2.1.29/src/constants/log-messages/updates.ts","/npm/@mentra/sdk@2.1.29/src/app/token/index.ts"],"names":[],"mappings":"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","file":"/npm/@mentra/sdk@2.1.29/dist/index.js","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v5.39.0.\n * Original file: /npm/@mentra/sdk@2.1.29/dist/index.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","// src/streams.ts\n\n/**\n * Types of streams that Apps can subscribe to\n *\n * These are events and data that Apps can receive from the cloud.\n * Not all message types can be subscribed to as streams.\n */\nexport enum StreamType {\n  // Hardware streams\n  BUTTON_PRESS = \"button_press\",\n  HEAD_POSITION = \"head_position\",\n  TOUCH_EVENT = \"touch_event\",\n  GLASSES_BATTERY_UPDATE = \"glasses_battery_update\",\n  PHONE_BATTERY_UPDATE = \"phone_battery_update\",\n  GLASSES_CONNECTION_STATE = \"glasses_connection_state\",\n  LOCATION_UPDATE = \"location_update\",\n  LOCATION_STREAM = \"location_stream\",\n  VPS_COORDINATES = \"vps_coordinates\",\n\n  // Audio streams\n  TRANSCRIPTION = \"transcription\",\n  TRANSLATION = \"translation\",\n  VAD = \"VAD\",\n  AUDIO_CHUNK = \"audio_chunk\",\n\n  // Phone streams\n  PHONE_NOTIFICATION = \"phone_notification\",\n  PHONE_NOTIFICATION_DISMISSED = \"phone_notification_dismissed\",\n  CALENDAR_EVENT = \"calendar_event\",\n\n  // System streams\n  START_APP = \"start_app\",\n  STOP_APP = \"stop_app\",\n  OPEN_DASHBOARD = \"open_dashboard\",\n  CORE_STATUS_UPDATE = \"core_status_update\",\n\n  // Video streams\n  VIDEO = \"video\",\n  PHOTO_REQUEST = \"photo_request\",\n  PHOTO_RESPONSE = \"photo_response\",\n  RTMP_STREAM_STATUS = \"rtmp_stream_status\",\n  MANAGED_STREAM_STATUS = \"managed_stream_status\",\n\n  // Special subscription types\n  ALL = \"all\",\n  WILDCARD = \"*\",\n\n  // New stream type\n  MENTRAOS_SETTINGS_UPDATE_REQUEST = \"settings_update_request\",\n  CUSTOM_MESSAGE = \"custom_message\",\n  PHOTO_TAKEN = \"photo_taken\",\n}\n\n/**\n * Extended StreamType to support language-specific streams\n * This allows us to treat language-specific strings as StreamType values\n */\nexport type ExtendedStreamType = StreamType | string\n\n/**\n * Categories of stream data\n */\nexport enum StreamCategory {\n  /** Data from hardware sensors */\n  HARDWARE = \"hardware\",\n\n  /** Audio processing results */\n  AUDIO = \"audio\",\n\n  /** Phone-related events */\n  PHONE = \"phone\",\n\n  /** System-level events */\n  SYSTEM = \"system\",\n}\n\n/**\n * Map of stream categories for each stream type\n */\nexport const STREAM_CATEGORIES: Record<StreamType, StreamCategory> = {\n  [StreamType.BUTTON_PRESS]: StreamCategory.HARDWARE,\n  [StreamType.HEAD_POSITION]: StreamCategory.HARDWARE,\n  [StreamType.TOUCH_EVENT]: StreamCategory.HARDWARE,\n  [StreamType.GLASSES_BATTERY_UPDATE]: StreamCategory.HARDWARE,\n  [StreamType.PHONE_BATTERY_UPDATE]: StreamCategory.HARDWARE,\n  [StreamType.GLASSES_CONNECTION_STATE]: StreamCategory.HARDWARE,\n  [StreamType.LOCATION_UPDATE]: StreamCategory.HARDWARE,\n  [StreamType.LOCATION_STREAM]: StreamCategory.HARDWARE,\n  [StreamType.VPS_COORDINATES]: StreamCategory.HARDWARE,\n\n  [StreamType.TRANSCRIPTION]: StreamCategory.AUDIO,\n  [StreamType.TRANSLATION]: StreamCategory.AUDIO,\n  [StreamType.VAD]: StreamCategory.AUDIO,\n  [StreamType.AUDIO_CHUNK]: StreamCategory.AUDIO,\n\n  [StreamType.PHONE_NOTIFICATION]: StreamCategory.PHONE,\n  [StreamType.PHONE_NOTIFICATION_DISMISSED]: StreamCategory.PHONE,\n  [StreamType.CALENDAR_EVENT]: StreamCategory.PHONE,\n  [StreamType.START_APP]: StreamCategory.SYSTEM,\n  [StreamType.STOP_APP]: StreamCategory.SYSTEM,\n  [StreamType.OPEN_DASHBOARD]: StreamCategory.SYSTEM,\n  [StreamType.CORE_STATUS_UPDATE]: StreamCategory.SYSTEM,\n\n  [StreamType.VIDEO]: StreamCategory.HARDWARE,\n  [StreamType.PHOTO_REQUEST]: StreamCategory.HARDWARE,\n  [StreamType.PHOTO_RESPONSE]: StreamCategory.HARDWARE,\n  [StreamType.RTMP_STREAM_STATUS]: StreamCategory.HARDWARE,\n  [StreamType.MANAGED_STREAM_STATUS]: StreamCategory.HARDWARE,\n  [StreamType.ALL]: StreamCategory.SYSTEM,\n  [StreamType.WILDCARD]: StreamCategory.SYSTEM,\n\n  [StreamType.MENTRAOS_SETTINGS_UPDATE_REQUEST]: StreamCategory.SYSTEM,\n  [StreamType.CUSTOM_MESSAGE]: StreamCategory.SYSTEM,\n  [StreamType.PHOTO_TAKEN]: StreamCategory.HARDWARE,\n}\n\n/**\n * Branded type for TypeScript to recognize language-specific stream types\n * This helps maintain type safety when using language-specific streams\n */\nexport type LanguageStreamType<T extends string> = T & {\n  __languageStreamBrand: never\n}\n\n/**\n * Create a language-branded stream type\n * This is a type helper to ensure type safety for language-specific streams\n */\nfunction createLanguageStream<T extends string>(type: T): LanguageStreamType<T> {\n  return type as LanguageStreamType<T>\n}\n\n/**\n * Structure of a parsed language stream subscription\n */\nexport interface LanguageStreamInfo {\n  type: StreamType // Base stream type (e.g., TRANSCRIPTION)\n  baseType: string // String representation of base type (e.g., \"transcription\")\n  transcribeLanguage: string // Source language code (e.g., \"en-US\")\n  translateLanguage?: string // Target language code for translations (e.g., \"es-ES\")\n  options?: Record<string, string | boolean> // Query parameters/options\n  original: ExtendedStreamType // Original subscription string\n}\n\n/**\n * Check if a string is a valid language code\n * Simple validation for language code format: xx-XX (e.g., en-US)\n */\nexport function isValidLanguageCode(code: string): boolean {\n  return /^[a-z]{2,3}-[A-Z]{2}$/.test(code)\n}\n\n/**\n * Parse a subscription string to extract language information\n *\n * @param subscription Subscription string (e.g., \"transcription:en-US\" or \"translation:es-ES-to-en-US\" or \"transcription:en-US?no-language-identification=true\")\n * @returns Parsed language stream info or null if not a language-specific subscription\n */\nexport function parseLanguageStream(subscription: ExtendedStreamType): LanguageStreamInfo | null {\n  // console.log(`🎤 Parsing language stream: ${subscription}`);\n\n  if (typeof subscription !== \"string\") {\n    return null\n  }\n\n  // Handle transcription format (transcription:en-US or transcription:en-US?options)\n  if (subscription.startsWith(`${StreamType.TRANSCRIPTION}:`)) {\n    const [baseType, rest] = subscription.split(\":\")\n    const [languageCode, queryString] = rest?.split(\"?\") ?? []\n\n    if (languageCode && isValidLanguageCode(languageCode)) {\n      const options: Record<string, string | boolean> = {}\n\n      // Parse query parameters if present\n      if (queryString) {\n        const params = new URLSearchParams(queryString)\n        for (const [key, value] of params.entries()) {\n          // Convert string values to boolean when appropriate\n          if (value === \"true\") {\n            options[key] = true\n          } else if (value === \"false\") {\n            options[key] = false\n          } else {\n            options[key] = value\n          }\n        }\n      }\n\n      return {\n        type: StreamType.TRANSCRIPTION,\n        baseType,\n        transcribeLanguage: languageCode,\n        options: Object.keys(options).length > 0 ? options : undefined,\n        original: subscription,\n      }\n    }\n  }\n\n  // Handle translation format (translation:es-ES-to-en-US, translation:all-to-en-US, or with ?options)\n  if (subscription.startsWith(`${StreamType.TRANSLATION}:`)) {\n    const [baseType, rest] = subscription.split(\":\")\n    const [languagePair, queryString] = rest?.split(\"?\") ?? []\n    const [sourceLanguage, targetLanguage] = languagePair?.split(\"-to-\") ?? []\n\n    // Check for \"all-to-LANG\" format (one-way translation from any language)\n    const isAllToFormat = sourceLanguage === \"all\" && targetLanguage && isValidLanguageCode(targetLanguage)\n\n    // Check for \"LANG-to-LANG\" format (two-way translation between specific languages)\n    const isSpecificPairFormat =\n      sourceLanguage && targetLanguage && isValidLanguageCode(sourceLanguage) && isValidLanguageCode(targetLanguage)\n\n    if (isAllToFormat || isSpecificPairFormat) {\n      const options: Record<string, string | boolean> = {}\n\n      // Parse query parameters if present\n      if (queryString) {\n        const params = new URLSearchParams(queryString)\n        for (const [key, value] of params.entries()) {\n          // Convert string values to boolean when appropriate\n          if (value === \"true\") {\n            options[key] = true\n          } else if (value === \"false\") {\n            options[key] = false\n          } else {\n            options[key] = value\n          }\n        }\n      }\n\n      return {\n        type: StreamType.TRANSLATION,\n        baseType,\n        transcribeLanguage: sourceLanguage, // \"all\" for all-to-LANG, or specific language code\n        translateLanguage: targetLanguage,\n        options: Object.keys(options).length > 0 ? options : undefined,\n        original: subscription,\n      }\n    }\n  }\n\n  return null\n}\n\n/**\n * Create a transcription stream identifier for a specific language\n * Returns a type-safe stream type that can be used like a StreamType\n *\n * @param language Language code (e.g., \"en-US\") or \"auto\" for automatic detection\n * @param options Optional configuration options\n * @returns Typed stream identifier\n */\nexport function createTranscriptionStream(\n  language: string,\n  options?: {\n    disableLanguageIdentification?: boolean\n    hints?: string[]\n  },\n): ExtendedStreamType {\n  console.log(`🎤 Creating transcription stream for language: ${language}`)\n  console.log(`🎤 Options: ${JSON.stringify(options)}`)\n\n  // Defensively remove any query string from the language parameter\n  const languageCode = language.split(\"?\")[0]\n\n  if (languageCode !== \"auto\" && !isValidLanguageCode(languageCode)) {\n    throw new Error(`Invalid language code: ${languageCode}`)\n  }\n\n  const base = `${StreamType.TRANSCRIPTION}:${languageCode}`\n  const params = new URLSearchParams()\n\n  if (options?.disableLanguageIdentification) {\n    params.set(\"no-language-identification\", \"true\")\n  }\n\n  if (options?.hints && options.hints.length > 0) {\n    params.set(\"hints\", options.hints.join(\",\"))\n  }\n\n  const queryString = params.toString()\n  return queryString ? (`${base}?${queryString}` as ExtendedStreamType) : (base as ExtendedStreamType)\n}\n\n/**\n * Create a translation stream identifier for a language pair\n * Returns a type-safe stream type that can be used like a StreamType\n *\n * @param sourceLanguage Source language code (e.g., \"es-ES\") or \"all\" for one-way translation\n * @param targetLanguage Target language code (e.g., \"en-US\")\n * @param options Optional configuration options\n * @returns Typed stream identifier\n */\nexport function createTranslationStream(\n  sourceLanguage: string,\n  targetLanguage: string,\n  options?: {disableLanguageIdentification?: boolean},\n): ExtendedStreamType {\n  // Defensively remove any query string from the language parameters\n  const cleanSourceLanguage = sourceLanguage.split(\"?\")[0]\n  const cleanTargetLanguage = targetLanguage.split(\"?\")[0]\n\n  // Support \"all\" as source for one-way translation (all languages → target)\n  const isAllToFormat = cleanSourceLanguage === \"all\"\n\n  if ((!isAllToFormat && !isValidLanguageCode(cleanSourceLanguage)) || !isValidLanguageCode(cleanTargetLanguage)) {\n    throw new Error(`Invalid language code(s): ${cleanSourceLanguage}, ${cleanTargetLanguage}`)\n  }\n  const base = `${StreamType.TRANSLATION}:${cleanSourceLanguage}-to-${cleanTargetLanguage}`\n  if (options?.disableLanguageIdentification) {\n    return `${base}?no-language-identification=true` as ExtendedStreamType\n  }\n  return createLanguageStream(base)\n}\n\n/**\n * Parse a touch event subscription to extract gesture information\n * @param subscription Subscription string (e.g., \"touch_event:forward_swipe\")\n * @returns Gesture name or null if not a gesture-specific subscription\n */\nexport function parseTouchEventStream(subscription: ExtendedStreamType): string | null {\n  if (typeof subscription !== \"string\") {\n    return null\n  }\n\n  if (subscription.startsWith(`${StreamType.TOUCH_EVENT}:`)) {\n    const [, gestureName] = subscription.split(\":\")\n    const validGestures = [\n      \"single_tap\",\n      \"double_tap\",\n      \"triple_tap\",\n      \"long_press\",\n      \"forward_swipe\",\n      \"backward_swipe\",\n      \"up_swipe\",\n      \"down_swipe\",\n    ]\n\n    if (gestureName && validGestures.includes(gestureName)) {\n      return gestureName\n    }\n  }\n\n  return null\n}\n\n/**\n * Create a touch event subscription for a specific gesture\n * @param gesture Gesture name (e.g., \"forward_swipe\")\n * @returns Typed stream identifier\n */\nexport function createTouchEventStream(gesture: string): ExtendedStreamType {\n  const validGestures = [\n    \"single_tap\",\n    \"double_tap\",\n    \"triple_tap\",\n    \"long_press\",\n    \"forward_swipe\",\n    \"backward_swipe\",\n    \"up_swipe\",\n    \"down_swipe\",\n  ]\n\n  if (!validGestures.includes(gesture)) {\n    throw new Error(`Invalid gesture: ${gesture}`)\n  }\n\n  return `${StreamType.TOUCH_EVENT}:${gesture}` as ExtendedStreamType\n}\n\n/**\n * Create a universal translation stream identifier that translates from any language to target\n * This is useful when you want to support all languages translating to a single target\n * Returns a type-safe stream type that can be used like a StreamType\n *\n * Example: createUniversalTranslationStream('es-ES') creates \"translation:all-to-es-ES\"\n *\n * @param targetLanguage Target language code (e.g., \"es-ES\")\n * @param options Optional configuration options\n * @returns Typed stream identifier\n */\nexport function createUniversalTranslationStream(\n  targetLanguage: string,\n  options?: {disableLanguageIdentification?: boolean},\n): ExtendedStreamType {\n  const cleanTargetLanguage = targetLanguage.split(\"?\")[0]\n\n  if (!isValidLanguageCode(cleanTargetLanguage)) {\n    throw new Error(`Invalid target language code: ${cleanTargetLanguage}`)\n  }\n\n  const base = `${StreamType.TRANSLATION}:all-to-${cleanTargetLanguage}`\n  if (options?.disableLanguageIdentification) {\n    return `${base}?no-language-identification=true` as ExtendedStreamType\n  }\n  return createLanguageStream(base)\n}\n\n/**\n * Check if a subscription is a valid stream type\n * This handles both enum-based StreamType values and language-specific stream formats\n *\n * @param subscription Subscription to validate\n * @returns True if valid, false otherwise\n */\nexport function isValidStreamType(subscription: ExtendedStreamType): boolean {\n  // Check if it's a standard StreamType\n  if (Object.values(StreamType).includes(subscription as StreamType)) {\n    return true\n  }\n\n  // Check if it's a valid language-specific stream\n  const languageStream = parseLanguageStream(subscription)\n  if (languageStream !== null) {\n    return true\n  }\n\n  // Check if it's a valid gesture-specific stream\n  const gestureStream = parseTouchEventStream(subscription)\n  if (gestureStream !== null) {\n    return true\n  }\n\n  return false\n}\n\n/**\n * Helper function to check if a stream type is of a particular category\n * Works with both standard and language-specific stream types\n */\nexport function isStreamCategory(streamType: ExtendedStreamType, category: StreamCategory): boolean {\n  const baseType = getBaseStreamType(streamType)\n  return baseType ? STREAM_CATEGORIES[baseType] === category : false\n}\n\n/**\n * Helper function to get all stream types in a category\n */\nexport function getStreamTypesByCategory(category: StreamCategory): StreamType[] {\n  return Object.entries(STREAM_CATEGORIES)\n    .filter(([_, cat]) => cat === category)\n    .map(([type]) => type as StreamType)\n}\n\n/**\n * Get the base StreamType for a subscription\n * Works with both standard StreamType values and language-specific formats\n *\n * @param subscription Subscription string or StreamType\n * @returns The base StreamType enum value\n */\nexport function getBaseStreamType(subscription: ExtendedStreamType): StreamType | null {\n  // Check if it's already a standard StreamType\n  if (Object.values(StreamType).includes(subscription as StreamType)) {\n    return subscription as StreamType\n  }\n\n  // Check if it's a language-specific stream\n  const languageStream = parseLanguageStream(subscription)\n  if (languageStream) {\n    return languageStream.type\n  }\n\n  // Check if it's a gesture-specific stream\n  const gestureStream = parseTouchEventStream(subscription)\n  if (gestureStream) {\n    return StreamType.TOUCH_EVENT\n  }\n\n  return null\n}\n\n/**\n * Check if a stream is a language-specific stream\n */\nexport function isLanguageStream(subscription: ExtendedStreamType): boolean {\n  return parseLanguageStream(subscription) !== null\n}\n\n/**\n * Get language information from a stream type\n * Returns null for regular stream types\n */\nexport function getLanguageInfo(subscription: ExtendedStreamType): LanguageStreamInfo | null {\n  return parseLanguageStream(subscription)\n}\n\n// this is the blueprint for our new rich subscription object\n// it allows a developer to specify a rate for the location stream\nexport interface LocationStreamRequest {\n  stream: \"location_stream\"\n  rate: \"standard\" | \"high\" | \"realtime\" | \"tenMeters\" | \"hundredMeters\" | \"kilometer\" | \"threeKilometers\" | \"reduced\"\n}\n","// src/message-types.ts\n\nimport { StreamType } from \"./streams\";\n\n/**\n * Types of messages from glasses to cloud\n */\nexport enum GlassesToCloudMessageType {\n  // Control actions\n  CONNECTION_INIT = \"connection_init\",\n  REQUEST_SETTINGS = \"request_settings\",\n\n  START_APP = StreamType.START_APP,\n  STOP_APP = StreamType.STOP_APP,\n\n  DASHBOARD_STATE = \"dashboard_state\",\n  OPEN_DASHBOARD = StreamType.OPEN_DASHBOARD,\n\n  // Mentra Live\n  PHOTO_RESPONSE = StreamType.PHOTO_RESPONSE,\n\n  // Local Transcription\n  LOCAL_TRANSCRIPTION = \"local_transcription\",\n\n  // RTMP streaming\n  RTMP_STREAM_STATUS = StreamType.RTMP_STREAM_STATUS,\n  KEEP_ALIVE_ACK = \"keep_alive_ack\",\n\n  BUTTON_PRESS = StreamType.BUTTON_PRESS,\n  HEAD_POSITION = StreamType.HEAD_POSITION,\n  TOUCH_EVENT = StreamType.TOUCH_EVENT,\n  GLASSES_BATTERY_UPDATE = StreamType.GLASSES_BATTERY_UPDATE,\n  PHONE_BATTERY_UPDATE = StreamType.PHONE_BATTERY_UPDATE,\n  GLASSES_CONNECTION_STATE = StreamType.GLASSES_CONNECTION_STATE,\n  LOCATION_UPDATE = StreamType.LOCATION_UPDATE,\n\n  // TODO(isaiah): Remove VPS_COORDINATES once confirmed we don't use this system.\n  VPS_COORDINATES = StreamType.VPS_COORDINATES,\n  VAD = StreamType.VAD,\n\n  // TODO(isaiah): Remove PHONE_NOTIFICATION, and PHONE_NOTIFICATION_DISMISSED after moving to REST request.\n  PHONE_NOTIFICATION = StreamType.PHONE_NOTIFICATION,\n  PHONE_NOTIFICATION_DISMISSED = StreamType.PHONE_NOTIFICATION_DISMISSED,\n\n  // TODO(isaiah): Remove CALENDAR_EVENT after moving to REST request.\n  CALENDAR_EVENT = StreamType.CALENDAR_EVENT,\n  MENTRAOS_SETTINGS_UPDATE_REQUEST = StreamType.MENTRAOS_SETTINGS_UPDATE_REQUEST,\n\n  // TODO(isaiah): Remove CORE_STATUS_UPDATE after moving to REST request.\n  CORE_STATUS_UPDATE = StreamType.CORE_STATUS_UPDATE,\n\n  PHOTO_TAKEN = StreamType.PHOTO_TAKEN,\n  AUDIO_PLAY_RESPONSE = \"audio_play_response\",\n\n  // RGB LED control\n  RGB_LED_CONTROL_RESPONSE = \"rgb_led_control_response\",\n\n  // LiveKit handshake\n  LIVEKIT_INIT = \"livekit_init\",\n\n  // UDP audio\n  UDP_REGISTER = \"udp_register\",\n  UDP_UNREGISTER = \"udp_unregister\",\n}\n\n/**\n * Types of messages from cloud to glasses\n */\nexport enum CloudToGlassesMessageType {\n  // Responses\n  CONNECTION_ACK = \"connection_ack\",\n  CONNECTION_ERROR = \"connection_error\",\n  AUTH_ERROR = \"auth_error\",\n\n  // Updates\n  DISPLAY_EVENT = \"display_event\",\n  APP_STATE_CHANGE = \"app_state_change\",\n  MICROPHONE_STATE_CHANGE = \"microphone_state_change\",\n  SETTINGS_UPDATE = \"settings_update\",\n\n  // Requests\n  PHOTO_REQUEST = \"photo_request\",\n  AUDIO_PLAY_REQUEST = \"audio_play_request\",\n  AUDIO_STOP_REQUEST = \"audio_stop_request\",\n  RGB_LED_CONTROL = \"rgb_led_control\",\n  SHOW_WIFI_SETUP = \"show_wifi_setup\",\n\n  // RTMP streaming\n  START_RTMP_STREAM = \"start_rtmp_stream\",\n  STOP_RTMP_STREAM = \"stop_rtmp_stream\",\n  KEEP_RTMP_STREAM_ALIVE = \"keep_rtmp_stream_alive\",\n\n  // Dashboard updates\n  DASHBOARD_MODE_CHANGE = \"dashboard_mode_change\",\n  DASHBOARD_ALWAYS_ON_CHANGE = \"dashboard_always_on_change\",\n\n  // Location Service\n  SET_LOCATION_TIER = \"set_location_tier\",\n  REQUEST_SINGLE_LOCATION = \"request_single_location\",\n\n  WEBSOCKET_ERROR = \"websocket_error\",\n\n  // LiveKit info (URL, room, token)\n  LIVEKIT_INFO = \"livekit_info\",\n\n  // UDP audio\n  UDP_PING_ACK = \"udp_ping_ack\",\n}\n\n/**\n * Types of messages from Apps to cloud\n */\nexport enum AppToCloudMessageType {\n  // Commands\n  CONNECTION_INIT = \"tpa_connection_init\",\n  SUBSCRIPTION_UPDATE = \"subscription_update\",\n  LOCATION_POLL_REQUEST = \"location_poll_request\",\n\n  // Requests\n  DISPLAY_REQUEST = \"display_event\",\n  PHOTO_REQUEST = \"photo_request\",\n  AUDIO_PLAY_REQUEST = \"audio_play_request\",\n  AUDIO_STOP_REQUEST = \"audio_stop_request\",\n  RGB_LED_CONTROL = \"rgb_led_control\",\n  REQUEST_WIFI_SETUP = \"request_wifi_setup\",\n\n  // RTMP streaming\n  RTMP_STREAM_REQUEST = \"rtmp_stream_request\",\n  RTMP_STREAM_STOP = \"rtmp_stream_stop\",\n\n  // Managed RTMP streaming\n  MANAGED_STREAM_REQUEST = \"managed_stream_request\",\n  MANAGED_STREAM_STOP = \"managed_stream_stop\",\n\n  // Stream status check (both managed and unmanaged)\n  STREAM_STATUS_CHECK = \"stream_status_check\",\n\n  // Dashboard requests\n  DASHBOARD_CONTENT_UPDATE = \"dashboard_content_update\",\n  DASHBOARD_MODE_CHANGE = \"dashboard_mode_change\",\n  DASHBOARD_SYSTEM_UPDATE = \"dashboard_system_update\",\n\n  // TODO(isaiah): Remove after confirming not in use.\n  // App-to-App Communication\n  APP_BROADCAST_MESSAGE = \"app_broadcast_message\",\n  APP_DIRECT_MESSAGE = \"app_direct_message\",\n  APP_USER_DISCOVERY = \"app_user_discovery\",\n  APP_ROOM_JOIN = \"app_room_join\",\n  APP_ROOM_LEAVE = \"app_room_leave\",\n\n  // Session lifecycle\n  OWNERSHIP_RELEASE = \"ownership_release\",\n}\n\n/**\n * Types of messages from cloud to Apps\n */\nexport enum CloudToAppMessageType {\n  // Responses\n  CONNECTION_ACK = \"tpa_connection_ack\",\n  CONNECTION_ERROR = \"tpa_connection_error\",\n\n  // Updates\n  APP_STOPPED = \"app_stopped\",\n  SETTINGS_UPDATE = \"settings_update\",\n  CAPABILITIES_UPDATE = \"capabilities_update\",\n  DEVICE_STATE_UPDATE = \"device_state_update\",\n\n  // Dashboard updates\n  DASHBOARD_MODE_CHANGED = \"dashboard_mode_changed\",\n  DASHBOARD_ALWAYS_ON_CHANGED = \"dashboard_always_on_changed\",\n\n  // Stream data\n  DATA_STREAM = \"data_stream\",\n\n  // Media responses\n  PHOTO_RESPONSE = \"photo_response\",\n  AUDIO_PLAY_RESPONSE = \"audio_play_response\",\n  RGB_LED_CONTROL_RESPONSE = \"rgb_led_control_response\",\n  RTMP_STREAM_STATUS = \"rtmp_stream_status\",\n  MANAGED_STREAM_STATUS = \"managed_stream_status\",\n  STREAM_STATUS_CHECK_RESPONSE = \"stream_status_check_response\",\n\n  WEBSOCKET_ERROR = \"websocket_error\",\n\n  // Permissions\n  PERMISSION_ERROR = \"permission_error\",\n\n  // General purpose messaging\n  CUSTOM_MESSAGE = \"custom_message\",\n\n  // TODO(isaiah): Remove after confirming not in use.\n  // App-to-App Communication Responses\n  APP_MESSAGE_RECEIVED = \"app_message_received\",\n  APP_USER_JOINED = \"app_user_joined\",\n  APP_USER_LEFT = \"app_user_left\",\n  APP_ROOM_UPDATED = \"app_room_updated\",\n  APP_DIRECT_MESSAGE_RESPONSE = \"app_direct_message_response\",\n}\n\n/**\n * Control action message types (subset of GlassesToCloudMessageType)\n */\nexport const ControlActionTypes = [\n  GlassesToCloudMessageType.CONNECTION_INIT,\n  GlassesToCloudMessageType.START_APP,\n  GlassesToCloudMessageType.STOP_APP,\n  GlassesToCloudMessageType.DASHBOARD_STATE,\n  GlassesToCloudMessageType.OPEN_DASHBOARD,\n] as const;\n\n/**\n * Event message types (subset of GlassesToCloudMessageType)\n */\nexport const EventTypes = [\n  GlassesToCloudMessageType.BUTTON_PRESS,\n  GlassesToCloudMessageType.HEAD_POSITION,\n  GlassesToCloudMessageType.GLASSES_BATTERY_UPDATE,\n  GlassesToCloudMessageType.PHONE_BATTERY_UPDATE,\n  GlassesToCloudMessageType.GLASSES_CONNECTION_STATE,\n  GlassesToCloudMessageType.LOCATION_UPDATE,\n  GlassesToCloudMessageType.VPS_COORDINATES,\n  GlassesToCloudMessageType.VAD,\n  GlassesToCloudMessageType.PHONE_NOTIFICATION,\n  GlassesToCloudMessageType.PHONE_NOTIFICATION_DISMISSED,\n  GlassesToCloudMessageType.CALENDAR_EVENT,\n  GlassesToCloudMessageType.MENTRAOS_SETTINGS_UPDATE_REQUEST,\n  GlassesToCloudMessageType.CORE_STATUS_UPDATE,\n  GlassesToCloudMessageType.LOCAL_TRANSCRIPTION,\n] as const;\n\n/**\n * Response message types (subset of CloudToGlassesMessageType)\n */\nexport const ResponseTypes = [\n  CloudToGlassesMessageType.CONNECTION_ACK,\n  CloudToGlassesMessageType.CONNECTION_ERROR,\n  CloudToGlassesMessageType.AUTH_ERROR,\n] as const;\n\n/**\n * Update message types (subset of CloudToGlassesMessageType)\n */\nexport const UpdateTypes = [\n  CloudToGlassesMessageType.DISPLAY_EVENT,\n  CloudToGlassesMessageType.APP_STATE_CHANGE,\n  CloudToGlassesMessageType.MICROPHONE_STATE_CHANGE,\n  CloudToGlassesMessageType.PHOTO_REQUEST,\n  CloudToGlassesMessageType.AUDIO_PLAY_REQUEST,\n  CloudToGlassesMessageType.AUDIO_STOP_REQUEST,\n  CloudToGlassesMessageType.RGB_LED_CONTROL,\n  CloudToGlassesMessageType.SETTINGS_UPDATE,\n  CloudToGlassesMessageType.DASHBOARD_MODE_CHANGE,\n  CloudToGlassesMessageType.DASHBOARD_ALWAYS_ON_CHANGE,\n  CloudToGlassesMessageType.START_RTMP_STREAM,\n  CloudToGlassesMessageType.STOP_RTMP_STREAM,\n  CloudToGlassesMessageType.KEEP_RTMP_STREAM_ALIVE,\n  CloudToGlassesMessageType.LIVEKIT_INFO,\n] as const;\n\n/**\n * Dashboard message types\n */\nexport const DashboardMessageTypes = [\n  AppToCloudMessageType.DASHBOARD_CONTENT_UPDATE,\n  AppToCloudMessageType.DASHBOARD_MODE_CHANGE,\n  AppToCloudMessageType.DASHBOARD_SYSTEM_UPDATE,\n  CloudToAppMessageType.DASHBOARD_MODE_CHANGED,\n  CloudToAppMessageType.DASHBOARD_ALWAYS_ON_CHANGED,\n] as const;\n","// TODO:\n/**\n * Dashboard API Types\n *\n * Type definitions for the dashboard functionality in the SDK.\n */\nimport { Layout } from '../layouts';\nimport { AppToCloudMessageType } from '../message-types';\n\n/**\n * Dashboard modes supported by the system\n */\nexport enum DashboardMode {\n  MAIN = 'main',           // Full dashboard experience\n  EXPANDED = 'expanded',   // More space for App content\n  // ALWAYS_ON = 'always_on'  // Persistent minimal dashboard\n}\n\n/**\n * Dashboard API for the system dashboard App\n */\nexport interface DashboardSystemAPI {\n  /**\n   * Set content for the top left section of the dashboard\n   * @param content Content to display\n   */\n  setTopLeft(content: string): void;\n\n  /**\n   * Set content for the top right section of the dashboard\n   * @param content Content to display\n   */\n  setTopRight(content: string): void;\n\n  /**\n   * Set content for the bottom left section of the dashboard\n   * @param content Content to display\n   */\n  setBottomLeft(content: string): void;\n\n  /**\n   * Set content for the bottom right section of the dashboard\n   * @param content Content to display\n   */\n  setBottomRight(content: string): void;\n\n  /**\n   * Set the current dashboard mode\n   * @param mode Dashboard mode to set\n   */\n  setViewMode(mode: DashboardMode): void;\n}\n\n/**\n * Dashboard API for all Apps\n */\nexport interface DashboardContentAPI {\n  /**\n   * Write content to dashboard\n   * @param content Content to display\n   * @param targets Optional list of dashboard modes to target\n   */\n  write(content: string, targets?: DashboardMode[]): void;\n\n  /**\n   * Write content to main dashboard mode\n   * @param content Content to display\n   */\n  writeToMain(content: string): void;\n\n  /**\n   * Write content to expanded dashboard mode\n   * @param content Text content to display\n   */\n  writeToExpanded(content: string): void;\n\n  /**\n   * Write content to always-on dashboard mode\n   * @param content Content to display\n   */\n  // writeToAlwaysOn(content: string): void;\n\n  /**\n   * Get current active dashboard mode\n   * @returns Promise resolving to current mode or 'none'\n   */\n  getCurrentMode(): Promise<DashboardMode | 'none'>;\n\n  /**\n   * Check if always-on dashboard is enabled\n   * @returns Promise resolving to boolean\n   */\n  // isAlwaysOnEnabled(): Promise<boolean>;\n\n  /**\n   * Register for mode change notifications\n   * @param callback Function to call when mode changes\n   * @returns Cleanup function to unregister callback\n   */\n  onModeChange(callback: (mode: DashboardMode | 'none') => void): () => void;\n\n  /**\n   * Register for always-on mode change notifications\n   * @param callback Function to call when always-on mode changes\n   * @returns Cleanup function to unregister callback\n   */\n  // onAlwaysOnChange(callback: (enabled: boolean) => void): () => void;\n}\n\n/**\n * Dashboard API exposed on AppSession\n */\nexport interface DashboardAPI {\n  /**\n   * System dashboard API (only available for system dashboard App)\n   */\n  system?: DashboardSystemAPI;\n\n  /**\n   * Content API (available to all Apps)\n   */\n  content: DashboardContentAPI;\n}\n\n/**\n * Message to update dashboard content\n */\nexport interface DashboardContentUpdate {\n  type: AppToCloudMessageType.DASHBOARD_CONTENT_UPDATE;\n  packageName: string;\n  sessionId: string;\n  content: string;\n  modes: DashboardMode[];\n  timestamp: Date;\n}\n\n/**\n * Message for dashboard mode change\n */\nexport interface DashboardModeChange {\n  type: AppToCloudMessageType.DASHBOARD_MODE_CHANGE;\n  packageName: string;\n  sessionId: string;\n  mode: DashboardMode;\n  timestamp: Date;\n}\n\n/**\n * Message to update system dashboard content\n */\nexport interface DashboardSystemUpdate {\n  type: AppToCloudMessageType.DASHBOARD_SYSTEM_UPDATE;\n  packageName: string;\n  sessionId: string;\n  section: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';\n  content: string;\n  timestamp: Date;\n}\n\n/**\n * Union type of all dashboard message types\n */\nexport type DashboardMessage =\n  | DashboardContentUpdate\n  | DashboardModeChange\n  | DashboardSystemUpdate;","/**\n * Dashboard API Implementation\n *\n * Provides dashboard functionality for Apps, allowing them to write content\n * to the dashboard and respond to dashboard mode changes.\n */\n// import { systemApps } from '../../constants';\nimport {\n  DashboardAPI,\n  DashboardContentAPI,\n  DashboardMode,\n  DashboardSystemAPI,\n  DashboardContentUpdate,\n  DashboardModeChange,\n  DashboardSystemUpdate,\n} from \"../../types/dashboard\"\nimport {AppToCloudMessageType} from \"../../types/message-types\"\nimport {EventManager} from \"./events\"\n\n// Import AppSession interface for typing\nimport type {AppSession} from \"./index\"\nimport dotenv from \"dotenv\"\n// Load environment variables from .env file\ndotenv.config()\n\nconst SYSTEM_DASHBOARD_PACKAGE_NAME = process.env.SYSTEM_DASHBOARD_PACKAGE_NAME || \"system.augmentos.dashboard\"\n\n/**\n * Implementation of DashboardSystemAPI interface for system dashboard App\n */\nexport class DashboardSystemManager implements DashboardSystemAPI {\n  private session: AppSession\n  private packageName: string\n\n  constructor(session: AppSession, packageName: string) {\n    this.session = session\n    this.packageName = packageName\n  }\n\n  setTopLeft(content: string): void {\n    this.updateSystemSection(\"topLeft\", content)\n  }\n\n  setTopRight(content: string): void {\n    this.updateSystemSection(\"topRight\", content)\n  }\n\n  setBottomLeft(content: string): void {\n    this.updateSystemSection(\"bottomLeft\", content)\n  }\n\n  setBottomRight(content: string): void {\n    this.updateSystemSection(\"bottomRight\", content)\n  }\n\n  setViewMode(mode: DashboardMode): void {\n    const message: DashboardModeChange = {\n      type: AppToCloudMessageType.DASHBOARD_MODE_CHANGE,\n      packageName: this.packageName,\n      sessionId: `${this.session.getSessionId()}-${this.packageName}`,\n      mode,\n      timestamp: new Date(),\n    }\n    this.session.sendMessage(message)\n  }\n\n  private updateSystemSection(section: \"topLeft\" | \"topRight\" | \"bottomLeft\" | \"bottomRight\", content: string): void {\n    const message: DashboardSystemUpdate = {\n      type: AppToCloudMessageType.DASHBOARD_SYSTEM_UPDATE,\n      packageName: this.packageName,\n      sessionId: `${this.session.getSessionId()}-${this.packageName}`,\n      section,\n      content,\n      timestamp: new Date(),\n    }\n    this.session.sendMessage(message)\n  }\n}\n\n/**\n * Implementation of DashboardContentAPI interface for all Apps\n */\nexport class DashboardContentManager implements DashboardContentAPI {\n  private session: AppSession\n  private packageName: string\n  private events: EventManager\n  private currentMode: DashboardMode | \"none\" = \"none\"\n  // private alwaysOnEnabled: boolean = false;\n\n  constructor(session: AppSession, packageName: string, events: EventManager) {\n    this.session = session\n    this.packageName = packageName\n    this.events = events\n  }\n\n  write(content: string, targets: DashboardMode[] = [DashboardMode.MAIN]): void {\n    const message: DashboardContentUpdate = {\n      type: AppToCloudMessageType.DASHBOARD_CONTENT_UPDATE,\n      packageName: this.packageName,\n      sessionId: `${this.session.getSessionId()}-${this.packageName}`,\n      content,\n      modes: targets,\n      timestamp: new Date(),\n    }\n    this.session.sendMessage(message)\n  }\n\n  writeToMain(content: string): void {\n    this.write(content, [DashboardMode.MAIN])\n  }\n\n  writeToExpanded(content: string): void {\n    const message: DashboardContentUpdate = {\n      type: AppToCloudMessageType.DASHBOARD_CONTENT_UPDATE,\n      packageName: this.packageName,\n      sessionId: `${this.session.getSessionId()}-${this.packageName}`,\n      content,\n      modes: [DashboardMode.EXPANDED],\n      timestamp: new Date(),\n    }\n    this.session.sendMessage(message)\n  }\n\n  // writeToAlwaysOn(content: string): void {\n  //   this.write(content, [DashboardMode.ALWAYS_ON]);\n  // }\n\n  async getCurrentMode(): Promise<DashboardMode | \"none\"> {\n    return this.currentMode\n  }\n\n  // async isAlwaysOnEnabled(): Promise<boolean> {\n  //   return this.alwaysOnEnabled;\n  // }\n\n  onModeChange(callback: (mode: DashboardMode | \"none\") => void): () => void {\n    return this.events.onDashboardModeChange((data) => {\n      this.currentMode = data.mode\n      callback(data.mode)\n    })\n  }\n\n  // onAlwaysOnChange(callback: (enabled: boolean) => void): () => void {\n  //   return this.events.onDashboardAlwaysOnChange((data) => {\n  //     this.alwaysOnEnabled = data.enabled;\n  //     callback(data.enabled);\n  //   });\n  // }\n\n  // Internal methods to update state\n  setCurrentMode(mode: DashboardMode | \"none\"): void {\n    this.currentMode = mode\n    this.events.emit(\"dashboard_mode_change\", {mode})\n  }\n\n  // setAlwaysOnEnabled(enabled: boolean): void {\n  //   this.alwaysOnEnabled = enabled;\n  //   this.events.emit('dashboard_always_on_change', { enabled });\n  // }\n}\n\n/**\n * Dashboard Manager - Main class that manages dashboard functionality\n * Each AppSession instance gets its own DashboardManager instance\n */\nexport class DashboardManager implements DashboardAPI {\n  public content: DashboardContentAPI\n  public system?: DashboardSystemAPI\n\n  constructor(session: AppSession) {\n    const packageName = session.getPackageName()\n    const events = session.events\n\n    // Create content API (available to all Apps)\n    this.content = new DashboardContentManager(session, packageName, events)\n\n    // Add system API if this is the system dashboard App\n    if (packageName === SYSTEM_DASHBOARD_PACKAGE_NAME) {\n      session.logger.info({service: \"SDK:DashboardManager\"}, \"Initializing system dashboard manager\")\n      this.system = new DashboardSystemManager(session, packageName)\n    } else {\n      session.logger.info({service: \"SDK:DashboardManager\"}, `Not the system dashboard: ${packageName}`)\n    }\n  }\n}\n","/**\n * 🔐 App Token Utilities\n *\n * Provides utilities for working with App tokens.\n */\nimport * as jwt from 'jsonwebtoken';\nimport { AppTokenPayload, TokenValidationResult, TokenConfig } from '../../types/token';\n\n/**\n * Default token expiration (1 day)\n */\nconst DEFAULT_EXPIRATION = 60 * 60 * 24; // seconds * minutes * hours (1 day).\n\n/**\n * Create a App token for a user session\n *\n * @param payload - Token payload data\n * @param config - Token configuration\n * @returns JWT token string\n *\n * @example\n * ```typescript\n * const token = createToken(\n *   {\n *     userId: 'user123',\n *     packageName: 'org.example.myapp',\n *     sessionId: 'session789'\n *   },\n *   { secretKey: 'your_secret_key' }\n * );\n * ```\n */\nexport function createToken(\n  payload: Omit<AppTokenPayload, 'iat' | 'exp'>,\n  config: TokenConfig\n): string {\n  return jwt.sign(\n    payload,\n    config.secretKey,\n    { expiresIn: config.expiresIn || DEFAULT_EXPIRATION }\n  );\n}\n\n/**\n * Validate and decode a App token\n *\n * @param token - JWT token string\n * @param secretKey - Secret key used for validation\n * @returns Token validation result\n *\n * @example\n * ```typescript\n * const result = validateToken('eyJhbGciOiJIUzI1...', 'your_secret_key');\n * if (result.valid) {\n *   // Use result.payload\n * }\n * ```\n */\nexport function validateToken(\n  token: string,\n  secretKey: string\n): TokenValidationResult {\n  try {\n    const decoded = jwt.verify(token, secretKey) as AppTokenPayload;\n    return {\n      valid: true,\n      payload: decoded\n    };\n  } catch (error) {\n    return {\n      valid: false,\n      error: error instanceof Error ? error.message : 'Unknown error'\n    };\n  }\n}\n\n/**\n * Generate a webview URL with an embedded App token\n *\n * @param baseUrl - Base URL of the webview\n * @param token - JWT token string\n * @returns Full URL with token parameter\n *\n * @example\n * ```typescript\n * const url = generateWebviewUrl(\n *   'https://example.com/webview',\n *   'eyJhbGciOiJIUzI1...'\n * );\n * // Returns: https://example.com/webview?token=eyJhbGciOiJIUzI1...\n * ```\n */\nexport function generateWebviewUrl(baseUrl: string, token: string): string {\n  const url = new URL(baseUrl);\n  url.searchParams.append('token', token);\n  return url.toString();\n}\n\n/**\n * Extract a App token from a URL\n *\n * @param url - URL string containing a token parameter\n * @returns Token string or null if not found\n *\n * @example\n * ```typescript\n * const token = extractTokenFromUrl(\n *   'https://example.com/webview?token=eyJhbGciOiJIUzI1...'\n * );\n * ```\n */\nexport function extractTokenFromUrl(url: string): string | null {\n  try {\n    const parsedUrl = new URL(url);\n    return parsedUrl.searchParams.get('token');\n  } catch (error) {\n    return null;\n  }\n}","// src/messages/glasses-to-cloud.ts\n\nimport { GlassesToCloudMessageType, ControlActionTypes, EventTypes } from \"../message-types\";\nimport { StreamType } from \"../streams\";\n\nimport { BaseMessage } from \"./base\";\n\n//===========================================================\n// Control actions\n//===========================================================\n\n/**\n * Connection initialization from glasses\n */\nexport interface ConnectionInit extends BaseMessage {\n  type: GlassesToCloudMessageType.CONNECTION_INIT;\n  userId?: string;\n  coreToken?: string;\n}\n\n/**\n * Client requests LiveKit info (url, room, token)\n */\nexport interface LiveKitInit extends BaseMessage {\n  type: GlassesToCloudMessageType.LIVEKIT_INIT;\n  mode?: \"publish\" | \"subscribe\"; // Optional mode - defaults to 'publish' for backward compatibility\n}\n\nexport interface RequestSettings extends BaseMessage {\n  type: GlassesToCloudMessageType.REQUEST_SETTINGS;\n  sessionId: string;\n}\n\n/**\n * Start app request from glasses\n */\nexport interface StartApp extends BaseMessage {\n  type: GlassesToCloudMessageType.START_APP;\n  packageName: string;\n}\n\n/**\n * Stop app request from glasses\n */\nexport interface StopApp extends BaseMessage {\n  type: GlassesToCloudMessageType.STOP_APP;\n  packageName: string;\n}\n\n/**\n * Dashboard state update from glasses\n */\nexport interface DashboardState extends BaseMessage {\n  type: GlassesToCloudMessageType.DASHBOARD_STATE;\n  isOpen: boolean;\n}\n\n/**\n * Open dashboard request from glasses\n */\nexport interface OpenDashboard extends BaseMessage {\n  type: GlassesToCloudMessageType.OPEN_DASHBOARD;\n}\n\n//===========================================================\n// Events and data\n//===========================================================\n\n/**\n * Button press event from glasses\n */\nexport interface ButtonPress extends BaseMessage {\n  type: GlassesToCloudMessageType.BUTTON_PRESS;\n  buttonId: string;\n  pressType: \"short\" | \"long\";\n}\n\n/**\n * Head position event from glasses\n */\nexport interface HeadPosition extends BaseMessage {\n  type: GlassesToCloudMessageType.HEAD_POSITION;\n  position: \"up\" | \"down\";\n}\n\n/**\n * Touch gesture event from glasses\n */\nexport interface TouchEvent extends BaseMessage {\n  type: GlassesToCloudMessageType.TOUCH_EVENT;\n  device_model: string;\n  gesture_name: string;\n  timestamp: Date;\n}\n\n/**\n * Glasses battery update from glasses\n */\nexport interface GlassesBatteryUpdate extends BaseMessage {\n  type: GlassesToCloudMessageType.GLASSES_BATTERY_UPDATE;\n  level: number; // 0-100\n  charging: boolean;\n  timeRemaining?: number; // minutes\n}\n\n/**\n * Phone battery update from glasses\n */\nexport interface PhoneBatteryUpdate extends BaseMessage {\n  type: GlassesToCloudMessageType.PHONE_BATTERY_UPDATE;\n  level: number; // 0-100\n  charging: boolean;\n  timeRemaining?: number; // minutes\n}\n\n/**\n * Glasses connection state from glasses\n */\nexport interface GlassesConnectionState extends BaseMessage {\n  type: GlassesToCloudMessageType.GLASSES_CONNECTION_STATE;\n  modelName: string;\n  status: string;\n\n  // Optional WiFi details (only present for WiFi-capable glasses)\n  wifi?: {\n    connected: boolean;\n    ssid?: string | null;\n  };\n}\n\n/**\n * Location update from glasses\n */\nexport interface LocationUpdate extends BaseMessage {\n  type: GlassesToCloudMessageType.LOCATION_UPDATE | StreamType.LOCATION_UPDATE;\n  lat: number;\n  lng: number;\n  accuracy?: number; // Accuracy in meters\n  correlationId?: string; // for poll responses\n}\n\n/**\n * VPS coordinates update from glasses\n */\nexport interface VpsCoordinates extends BaseMessage {\n  type: GlassesToCloudMessageType.VPS_COORDINATES | StreamType.VPS_COORDINATES;\n  deviceModel: string;\n  requestId: string;\n  x: number;\n  y: number;\n  z: number;\n  qx: number;\n  qy: number;\n  qz: number;\n  qw: number;\n  confidence: number;\n}\n\nexport interface LocalTranscription extends BaseMessage {\n  type: GlassesToCloudMessageType.LOCAL_TRANSCRIPTION;\n  text: string;\n  isFinal: boolean;\n  startTime: number;\n  endTime: number;\n  speakerId: number;\n  transcribeLanguage: string;\n  provider: string;\n}\n\nexport interface CalendarEvent extends BaseMessage {\n  type: GlassesToCloudMessageType.CALENDAR_EVENT | StreamType.CALENDAR_EVENT;\n  eventId: string;\n  title: string;\n  dtStart: string;\n  dtEnd: string;\n  timezone: string;\n  timeStamp: string;\n}\n\n/**\n * Voice activity detection from glasses\n */\nexport interface Vad extends BaseMessage {\n  type: GlassesToCloudMessageType.VAD;\n  status: boolean | \"true\" | \"false\";\n}\n\n/**\n * Phone notification from glasses\n */\nexport interface PhoneNotification extends BaseMessage {\n  type: GlassesToCloudMessageType.PHONE_NOTIFICATION;\n  notificationId: string;\n  app: string;\n  title: string;\n  content: string;\n  priority: \"low\" | \"normal\" | \"high\";\n}\n\n/**\n * Notification dismissed from glasses\n */\nexport interface PhoneNotificationDismissed extends BaseMessage {\n  type: GlassesToCloudMessageType.PHONE_NOTIFICATION_DISMISSED;\n  notificationId: string;\n  app: string;\n  title: string;\n  content: string;\n  notificationKey: string;\n}\n\n/**\n * MentraOS settings update from glasses\n */\nexport interface MentraosSettingsUpdateRequest extends BaseMessage {\n  type: GlassesToCloudMessageType.MENTRAOS_SETTINGS_UPDATE_REQUEST;\n}\nexport interface MentraosSettingsUpdateRequest extends BaseMessage {\n  type: GlassesToCloudMessageType.MENTRAOS_SETTINGS_UPDATE_REQUEST;\n}\n\n/**\n * Core status update from glasses\n */\nexport interface CoreStatusUpdate extends BaseMessage {\n  type: GlassesToCloudMessageType.CORE_STATUS_UPDATE;\n  status: string;\n  details?: Record<string, any>;\n}\n\n// ===========================================================\n// Mentra Live\n// ===========================================================\n\n/**\n * Photo error codes for detailed error reporting\n */\nexport enum PhotoErrorCode {\n  CAMERA_INIT_FAILED = \"CAMERA_INIT_FAILED\",\n  CAMERA_CAPTURE_FAILED = \"CAMERA_CAPTURE_FAILED\",\n  CAMERA_TIMEOUT = \"CAMERA_TIMEOUT\",\n  CAMERA_BUSY = \"CAMERA_BUSY\",\n  UPLOAD_FAILED = \"UPLOAD_FAILED\",\n  UPLOAD_TIMEOUT = \"UPLOAD_TIMEOUT\",\n  BLE_TRANSFER_FAILED = \"BLE_TRANSFER_FAILED\",\n  BLE_TRANSFER_BUSY = \"BLE_TRANSFER_BUSY\",\n  BLE_TRANSFER_FAILED_TO_START = \"BLE_TRANSFER_FAILED_TO_START\",\n  BLE_TRANSFER_TIMEOUT = \"BLE_TRANSFER_TIMEOUT\",\n  COMPRESSION_FAILED = \"COMPRESSION_FAILED\",\n  PERMISSION_DENIED = \"PERMISSION_DENIED\",\n  STORAGE_FULL = \"STORAGE_FULL\",\n  NETWORK_ERROR = \"NETWORK_ERROR\",\n  // Phone-side error codes\n  PHONE_GLASSES_NOT_CONNECTED = \"PHONE_GLASSES_NOT_CONNECTED\",\n  PHONE_BLE_TRANSFER_FAILED = \"PHONE_BLE_TRANSFER_FAILED\",\n  PHONE_UPLOAD_FAILED = \"PHONE_UPLOAD_FAILED\",\n  PHONE_TIMEOUT = \"PHONE_TIMEOUT\",\n  UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n}\n\n/**\n * Photo processing stages for error context\n */\nexport enum PhotoStage {\n  REQUEST_RECEIVED = \"REQUEST_RECEIVED\",\n  CAMERA_INIT = \"CAMERA_INIT\",\n  PHOTO_CAPTURE = \"PHOTO_CAPTURE\",\n  COMPRESSION = \"COMPRESSION\",\n  UPLOAD_START = \"UPLOAD_START\",\n  UPLOAD_PROGRESS = \"UPLOAD_PROGRESS\",\n  BLE_TRANSFER = \"BLE_TRANSFER\",\n  RESPONSE_SENT = \"RESPONSE_SENT\",\n}\n\n/**\n * Connection state information for error diagnostics\n */\nexport interface ConnectionState {\n  wifi: {\n    connected: boolean;\n    ssid?: string;\n    hasInternet: boolean;\n  };\n  ble: {\n    connected: boolean;\n    transferInProgress: boolean;\n  };\n  camera: {\n    available: boolean;\n    initialized: boolean;\n  };\n  storage: {\n    availableSpace: number;\n    totalSpace: number;\n  };\n}\n\n/**\n * Detailed error information for photo failures\n */\nexport interface PhotoErrorDetails {\n  stage: PhotoStage;\n  connectionState?: ConnectionState;\n  retryable: boolean;\n  suggestedAction?: string;\n  diagnosticInfo?: {\n    timestamp: number;\n    duration: number;\n    retryCount: number;\n    lastSuccessfulStage?: PhotoStage;\n  };\n}\n\n/**\n * Enhanced photo response with error support\n */\nexport interface PhotoResponse extends BaseMessage {\n  type: GlassesToCloudMessageType.PHOTO_RESPONSE;\n  requestId: string; // Unique ID for the photo request\n  success: boolean; // Explicit success/failure flag\n\n  // Success fields (only present when success = true)\n  photoUrl?: string; // URL of the uploaded photo\n  savedToGallery?: boolean; // Whether the photo was saved to gallery\n\n  // Error fields (only present when success = false)\n  error?: {\n    code: PhotoErrorCode;\n    message: string;\n    details?: PhotoErrorDetails;\n  };\n}\n\n/**\n * RGB LED control response from glasses\n */\nexport interface RgbLedControlResponse extends BaseMessage {\n  type: GlassesToCloudMessageType.RGB_LED_CONTROL_RESPONSE;\n  requestId: string;\n  success: boolean;\n  error?: string;\n}\n\n/**\n * RTMP stream status update from glasses\n */\nexport interface RtmpStreamStatus extends BaseMessage {\n  type: GlassesToCloudMessageType.RTMP_STREAM_STATUS;\n  streamId?: string; // Unique identifier for the stream\n  status:\n    | \"initializing\"\n    | \"connecting\"\n    | \"reconnecting\"\n    | \"streaming\"\n    | \"error\"\n    | \"stopped\"\n    | \"active\"\n    | \"stopping\"\n    | \"disconnected\"\n    | \"timeout\"\n    | \"reconnected\"\n    | \"reconnect_failed\";\n  errorDetails?: string;\n  appId?: string; // ID of the app that requested the stream\n  stats?: {\n    bitrate: number;\n    fps: number;\n    droppedFrames: number;\n    duration: number;\n  };\n}\n\n/**\n * Keep-alive acknowledgment from glasses\n */\nexport interface KeepAliveAck extends BaseMessage {\n  type: GlassesToCloudMessageType.KEEP_ALIVE_ACK;\n  streamId: string; // ID of the stream being kept alive\n  ackId: string; // Acknowledgment ID that was sent by cloud\n}\n\n/**\n * Photo taken event from glasses\n */\nexport interface PhotoTaken extends BaseMessage {\n  type: GlassesToCloudMessageType.PHOTO_TAKEN;\n  photoData: ArrayBuffer;\n  mimeType: string;\n  timestamp: Date;\n}\n\n/**\n * Audio play response from glasses/core\n */\nexport interface AudioPlayResponse extends BaseMessage {\n  type: GlassesToCloudMessageType.AUDIO_PLAY_RESPONSE;\n  requestId: string;\n  success: boolean;\n  error?: string;\n  duration?: number;\n}\n\n/**\n * UDP audio registration request from glasses/phone\n * Mobile sends this to register its userIdHash for UDP audio routing\n */\nexport interface UdpRegister extends BaseMessage {\n  type: GlassesToCloudMessageType.UDP_REGISTER;\n  userIdHash: number; // FNV-1a 32-bit hash of userId\n}\n\n/**\n * UDP audio unregistration request from glasses/phone\n * Mobile sends this when stopping UDP audio\n */\nexport interface UdpUnregister extends BaseMessage {\n  type: GlassesToCloudMessageType.UDP_UNREGISTER;\n  userIdHash: number; // FNV-1a 32-bit hash of userId\n}\n\n/**\n * Union type for all messages from glasses to cloud\n */\nexport type GlassesToCloudMessage =\n  | ConnectionInit\n  | LiveKitInit\n  | RequestSettings\n  | StartApp\n  | StopApp\n  | DashboardState\n  | OpenDashboard\n  | ButtonPress\n  | HeadPosition\n  | TouchEvent\n  | GlassesBatteryUpdate\n  | PhoneBatteryUpdate\n  | GlassesConnectionState\n  | LocationUpdate\n  | VpsCoordinates\n  | CalendarEvent\n  | Vad\n  | PhoneNotification\n  | PhoneNotificationDismissed\n  | MentraosSettingsUpdateRequest\n  | CoreStatusUpdate\n  | RtmpStreamStatus\n  | KeepAliveAck\n  | PhotoResponse\n  | RgbLedControlResponse\n  | PhotoTaken\n  | AudioPlayResponse\n  | LocalTranscription\n  | UdpRegister\n  | UdpUnregister;\n\n//===========================================================\n// Type guards\n//===========================================================\n\nexport function isControlAction(message: GlassesToCloudMessage): boolean {\n  return ControlActionTypes.includes(message.type as any);\n}\n\nexport function isEvent(message: GlassesToCloudMessage): boolean {\n  return EventTypes.includes(message.type as any);\n}\n\n// Individual type guards\nexport function isConnectionInit(message: GlassesToCloudMessage): message is ConnectionInit {\n  return message.type === GlassesToCloudMessageType.CONNECTION_INIT;\n}\n\nexport function isRequestSettings(message: GlassesToCloudMessage): message is RequestSettings {\n  return message.type === GlassesToCloudMessageType.REQUEST_SETTINGS;\n}\n\nexport function isStartApp(message: GlassesToCloudMessage): message is StartApp {\n  return message.type === GlassesToCloudMessageType.START_APP;\n}\n\nexport function isStopApp(message: GlassesToCloudMessage): message is StopApp {\n  return message.type === GlassesToCloudMessageType.STOP_APP;\n}\n\nexport function isButtonPress(message: GlassesToCloudMessage): message is ButtonPress {\n  return message.type === GlassesToCloudMessageType.BUTTON_PRESS;\n}\n\nexport function isHeadPosition(message: GlassesToCloudMessage): message is HeadPosition {\n  return message.type === GlassesToCloudMessageType.HEAD_POSITION;\n}\n\nexport function isGlassesBatteryUpdate(message: GlassesToCloudMessage): message is GlassesBatteryUpdate {\n  return message.type === GlassesToCloudMessageType.GLASSES_BATTERY_UPDATE;\n}\n\nexport function isPhoneBatteryUpdate(message: GlassesToCloudMessage): message is PhoneBatteryUpdate {\n  return message.type === GlassesToCloudMessageType.PHONE_BATTERY_UPDATE;\n}\n\nexport function isGlassesConnectionState(message: GlassesToCloudMessage): message is GlassesConnectionState {\n  return message.type === GlassesToCloudMessageType.GLASSES_CONNECTION_STATE;\n}\n\nexport function isLocationUpdate(message: GlassesToCloudMessage): message is LocationUpdate {\n  return message.type === GlassesToCloudMessageType.LOCATION_UPDATE;\n}\n\nexport function isCalendarEvent(message: GlassesToCloudMessage): message is CalendarEvent {\n  return message.type === GlassesToCloudMessageType.CALENDAR_EVENT;\n}\n\nexport function isVad(message: GlassesToCloudMessage): message is Vad {\n  return message.type === GlassesToCloudMessageType.VAD;\n}\n\nexport function isPhoneNotification(message: GlassesToCloudMessage): message is PhoneNotification {\n  return message.type === GlassesToCloudMessageType.PHONE_NOTIFICATION;\n}\n\nexport function isPhoneNotificationDismissed(message: GlassesToCloudMessage): message is PhoneNotificationDismissed {\n  return message.type === GlassesToCloudMessageType.PHONE_NOTIFICATION_DISMISSED;\n}\n\nexport function isRtmpStreamStatus(message: GlassesToCloudMessage): message is RtmpStreamStatus {\n  return message.type === GlassesToCloudMessageType.RTMP_STREAM_STATUS;\n}\n\nexport function isPhotoResponse(message: GlassesToCloudMessage): message is PhotoResponse {\n  return message.type === GlassesToCloudMessageType.PHOTO_RESPONSE;\n}\n\nexport function isRgbLedControlResponse(message: GlassesToCloudMessage): message is RgbLedControlResponse {\n  return message.type === GlassesToCloudMessageType.RGB_LED_CONTROL_RESPONSE;\n}\n\nexport function isKeepAliveAck(message: GlassesToCloudMessage): message is KeepAliveAck {\n  return message.type === GlassesToCloudMessageType.KEEP_ALIVE_ACK;\n}\n\nexport function isPhotoTaken(message: GlassesToCloudMessage): message is PhotoTaken {\n  return message.type === GlassesToCloudMessageType.PHOTO_TAKEN;\n}\n\nexport function isAudioPlayResponse(message: GlassesToCloudMessage): message is AudioPlayResponse {\n  return message.type === GlassesToCloudMessageType.AUDIO_PLAY_RESPONSE;\n}\n\nexport function isLocalTranscription(message: GlassesToCloudMessage): message is LocalTranscription {\n  return message.type === GlassesToCloudMessageType.LOCAL_TRANSCRIPTION;\n}\n\nexport function isUdpRegister(message: GlassesToCloudMessage): message is UdpRegister {\n  return message.type === GlassesToCloudMessageType.UDP_REGISTER;\n}\n\nexport function isUdpUnregister(message: GlassesToCloudMessage): message is UdpUnregister {\n  return message.type === GlassesToCloudMessageType.UDP_UNREGISTER;\n}\n","// src/enums.ts\n\n/**\n * Types of Third-Party Applications (Apps)\n */\nexport enum AppType {\n  SYSTEM_DASHBOARD = \"system_dashboard\", // Special UI placement, system functionality\n  BACKGROUND = \"background\", // Can temporarily take control of display\n  STANDARD = \"standard\", // Regular App (default) only one standard app can run at a time. starting a standard App will close any other standard App that is running.\n}\n\n/**\n * Types of layouts for displaying content\n */\nexport enum LayoutType {\n  TEXT_WALL = \"text_wall\",\n  DOUBLE_TEXT_WALL = \"double_text_wall\",\n  DASHBOARD_CARD = \"dashboard_card\",\n  REFERENCE_CARD = \"reference_card\",\n  BITMAP_VIEW = \"bitmap_view\",\n  BITMAP_ANIMATION = \"bitmap_animation\",\n  CLEAR_VIEW = \"clear_view\",\n}\n\n/**\n * Types of views for displaying content\n */\nexport enum ViewType {\n  DASHBOARD = \"dashboard\", // Regular dashboard (main/expanded)\n  // ALWAYS_ON = \"always_on\", // Persistent overlay dashboard\n  MAIN = \"main\", // Regular app content\n}\n\n// Types for AppSettings\nexport enum AppSettingType {\n  TOGGLE = \"toggle\",\n  TEXT = \"text\",\n  SELECT = \"select\",\n  SLIDER = \"slider\",\n  GROUP = \"group\",\n  TEXT_NO_SAVE_BUTTON = \"text_no_save_button\",\n  SELECT_WITH_SEARCH = \"select_with_search\",\n  MULTISELECT = \"multiselect\",\n  TITLE_VALUE = \"titleValue\",\n  NUMERIC_INPUT = \"numeric_input\",\n  TIME_PICKER = \"time_picker\",\n}\n// | { type: \"toggle\"; key: string; label: string; defaultValue: boolean }\n// | { type: \"text\"; key: string; label: string; defaultValue?: string }\n// | { type: \"select\"; key: string; label: string; options: { label: string; value: string }[]; defaultValue?: string };\n\n/**\n * Types of hardware components that apps can require\n */\nexport enum HardwareType {\n  CAMERA = \"CAMERA\",\n  DISPLAY = \"DISPLAY\",\n  MICROPHONE = \"MICROPHONE\",\n  SPEAKER = \"SPEAKER\",\n  IMU = \"IMU\",\n  BUTTON = \"BUTTON\",\n  LIGHT = \"LIGHT\",\n  WIFI = \"WIFI\",\n}\n\n/**\n * Levels of hardware requirements\n */\nexport enum HardwareRequirementLevel {\n  REQUIRED = \"REQUIRED\", // App cannot function without this hardware\n  OPTIONAL = \"OPTIONAL\", // App has enhanced features with this hardware\n}\n","// @mentra/sdk\n// packages/sdk/types/src/models.ts - Core models\n\nimport {\n  AppSettingType,\n  AppType,\n  HardwareType,\n  HardwareRequirementLevel,\n} from \"./enums\";\n\n// Tool parameter type definition\nexport interface ToolParameterSchema {\n  type: \"string\" | \"number\" | \"boolean\";\n  description: string;\n  enum?: string[];\n  required?: boolean;\n}\n\n// Tool schema definition for Apps\nexport interface ToolSchema {\n  id: string;\n  description: string;\n  activationPhrases?: string[];\n  parameters?: Record<string, ToolParameterSchema>;\n}\n\n/**\n * Developer profile information\n */\nexport interface DeveloperProfile {\n  company?: string;\n  website?: string;\n  contactEmail?: string;\n  description?: string;\n  logo?: string;\n}\n\n// Define PermissionType enum with legacy support\nexport enum PermissionType {\n  MICROPHONE = \"MICROPHONE\",\n  LOCATION = \"LOCATION\",\n  BACKGROUND_LOCATION = \"BACKGROUND_LOCATION\",\n  CALENDAR = \"CALENDAR\",\n  CAMERA = \"CAMERA\",\n\n  // Legacy notification permission (backward compatibility)\n  NOTIFICATIONS = \"NOTIFICATIONS\",\n\n  // New granular notification permissions\n  READ_NOTIFICATIONS = \"READ_NOTIFICATIONS\",\n  POST_NOTIFICATIONS = \"POST_NOTIFICATIONS\",\n\n  ALL = \"ALL\",\n}\n\n// Legacy permission mapping for backward compatibility\nexport const LEGACY_PERMISSION_MAP = new Map<PermissionType, PermissionType[]>([\n  [PermissionType.NOTIFICATIONS, [PermissionType.READ_NOTIFICATIONS]],\n]);\n\n// Permission interface\nexport interface Permission {\n  type: PermissionType;\n  description?: string;\n}\n\n/**\n * Hardware requirement for an app\n */\nexport interface HardwareRequirement {\n  type: HardwareType;\n  level: HardwareRequirementLevel;\n  description?: string; // Why this hardware is needed\n}\n\n/**\n * Base interface for applications\n */\nexport interface AppI {\n  packageName: string;\n  name: string;\n  publicUrl: string; // Base URL of the app server\n  isSystemApp?: boolean; // Is this a system app?\n  uninstallable?: boolean; // Can the app be uninstalled?\n\n  webviewURL?: string; // URL for phone UI\n  logoURL: string;\n  appType: AppType; // Type of app\n  appStoreId?: string; // Which app store registered this app\n\n  /**\n   * @deprecated Use organizationId instead. Will be removed after migration.\n   */\n  developerId?: string; // ID of the developer who created the app\n  organizationId?: any; // ID of the organization that owns this app\n\n  // Auth\n  hashedEndpointSecret?: string;\n  hashedApiKey?: string;\n\n  // App details\n  permissions?: Permission[];\n  description?: string;\n  version?: string;\n  settings?: AppSettings;\n  tools?: ToolSchema[];\n\n  /**\n   * Hardware requirements for the app\n   * If not specified, app is assumed to work with any hardware\n   */\n  hardwareRequirements?: HardwareRequirement[];\n\n  isPublic?: boolean;\n  appStoreStatus?: \"DEVELOPMENT\" | \"SUBMITTED\" | \"REJECTED\" | \"PUBLISHED\";\n}\n\n/**\n * Base interface for all app settings\n */\nexport interface BaseAppSetting {\n  key: string;\n  label: string;\n  value?: any; // User's selected value\n  defaultValue?: any; // System default\n}\n\n/**\n * Setting types for applications\n */\nexport type AppSetting =\n  | (BaseAppSetting & {\n      type: AppSettingType.TOGGLE;\n      defaultValue: boolean;\n      value?: boolean;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.TEXT;\n      defaultValue?: string;\n      value?: string;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.TEXT_NO_SAVE_BUTTON;\n      defaultValue?: string;\n      value?: string;\n      maxLines?: number;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.SELECT;\n      options: { label: string; value: any }[];\n      defaultValue?: any;\n      value?: any;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.SELECT_WITH_SEARCH;\n      options: { label: string; value: any }[];\n      defaultValue?: any;\n      value?: any;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.MULTISELECT;\n      options: { label: string; value: any }[];\n      defaultValue?: any[];\n      value?: any[];\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.SLIDER;\n      min: number;\n      max: number;\n      defaultValue: number;\n      value?: number;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.NUMERIC_INPUT;\n      min?: number;\n      max?: number;\n      step?: number;\n      placeholder?: string;\n      defaultValue?: number;\n      value?: number;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.TIME_PICKER;\n      showSeconds?: boolean;\n      defaultValue?: number; // Total seconds\n      value?: number; // Total seconds\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.GROUP;\n      title: string;\n    })\n  | (BaseAppSetting & {\n      type: AppSettingType.TITLE_VALUE;\n      label: string;\n      value: any;\n      key?: never; // TITLE_VALUE settings don't need keys since they're display-only\n    });\n\nexport type AppSettings = AppSetting[];\n\n/**\n * App configuration file structure\n * Represents the schema in app_config.json\n */\nexport interface AppConfig {\n  name: string;\n  description: string;\n  version: string;\n  settings: AppSetting[];\n  tools: ToolSchema[];\n}\n\n/**\n * Validate a App configuration object\n * @param config Object to validate\n * @returns True if the config is valid\n */\nexport function validateAppConfig(config: any): config is AppConfig {\n  if (!config || typeof config !== \"object\") return false;\n\n  // Check required string properties\n  if (\n    typeof config.name !== \"string\" ||\n    typeof config.description !== \"string\" ||\n    typeof config.version !== \"string\"\n  ) {\n    return false;\n  }\n\n  // Check settings array\n  if (!Array.isArray(config.settings)) return false;\n\n  // Validate each setting\n  return config.settings.every((setting: any) => {\n    // Group settings just need a title\n    if (setting.type === \"group\") {\n      return typeof setting.title === \"string\";\n    }\n\n    // TITLE_VALUE settings just need label and value\n    if (setting.type === \"titleValue\") {\n      return typeof setting.label === \"string\" && \"value\" in setting;\n    }\n\n    // Regular settings need key and label\n    if (typeof setting.key !== \"string\" || typeof setting.label !== \"string\") {\n      return false;\n    }\n\n    // Type-specific validation\n    switch (setting.type) {\n      case AppSettingType.TOGGLE:\n        return typeof setting.defaultValue === \"boolean\";\n\n      case AppSettingType.TEXT:\n      case AppSettingType.TEXT_NO_SAVE_BUTTON:\n        return (\n          setting.defaultValue === undefined ||\n          typeof setting.defaultValue === \"string\"\n        );\n\n      case AppSettingType.SELECT:\n      case AppSettingType.SELECT_WITH_SEARCH:\n        return (\n          Array.isArray(setting.options) &&\n          setting.options.every(\n            (opt: any) => typeof opt.label === \"string\" && \"value\" in opt,\n          )\n        );\n\n      case AppSettingType.MULTISELECT:\n        return (\n          Array.isArray(setting.options) &&\n          setting.options.every(\n            (opt: any) => typeof opt.label === \"string\" && \"value\" in opt,\n          ) &&\n          (setting.defaultValue === undefined ||\n            Array.isArray(setting.defaultValue))\n        );\n\n      case AppSettingType.SLIDER:\n        return (\n          typeof setting.defaultValue === \"number\" &&\n          typeof setting.min === \"number\" &&\n          typeof setting.max === \"number\" &&\n          setting.min <= setting.max\n        );\n\n      case AppSettingType.NUMERIC_INPUT:\n        return (\n          (setting.defaultValue === undefined ||\n            typeof setting.defaultValue === \"number\") &&\n          (setting.min === undefined || typeof setting.min === \"number\") &&\n          (setting.max === undefined || typeof setting.max === \"number\") &&\n          (setting.step === undefined || typeof setting.step === \"number\") &&\n          (setting.placeholder === undefined ||\n            typeof setting.placeholder === \"string\")\n        );\n\n      case AppSettingType.TIME_PICKER:\n        return (\n          (setting.defaultValue === undefined ||\n            typeof setting.defaultValue === \"number\") &&\n          (setting.showSeconds === undefined ||\n            typeof setting.showSeconds === \"boolean\")\n        );\n\n      case AppSettingType.GROUP:\n        return typeof setting.title === \"string\";\n\n      case AppSettingType.TITLE_VALUE:\n        return typeof setting.label === \"string\" && \"value\" in setting;\n\n      default:\n        return false;\n    }\n  });\n}\n\n/**\n * Transcript segment for speech processing\n */\nexport interface TranscriptSegment {\n  speakerId?: string;\n  resultId: string;\n  text: string;\n  timestamp: Date;\n  isFinal: boolean;\n}\n\n/**\n * Complete transcript\n */\nexport interface TranscriptI {\n  segments: TranscriptSegment[];\n  languageSegments?: Map<string, TranscriptSegment[]>; // Language-indexed map for multi-language support\n}\n","// src/messages/cloud-to-glasses.ts\n\nimport { Layout } from \"../layouts\";\nimport { CloudToGlassesMessageType, ResponseTypes, UpdateTypes } from \"../message-types\";\n\nimport { BaseMessage } from \"./base\";\n// import { UserSession } from \"../user-session\";\n\n//===========================================================\n// Responses\n//===========================================================\n\n/**\n * Connection acknowledgment to glasses\n */\nexport interface ConnectionAck extends BaseMessage {\n  type: CloudToGlassesMessageType.CONNECTION_ACK;\n  // userSession: Partial<UserSession>;\n  sessionId: string;\n  livekit?: {\n    url: string;\n    roomName: string;\n    token: string;\n  };\n}\n\n/**\n * Connection error to glasses\n */\nexport interface ConnectionError extends BaseMessage {\n  type: CloudToGlassesMessageType.CONNECTION_ERROR;\n  code?: string;\n  message: string;\n}\n\n/**\n * Authentication error to glasses\n */\nexport interface AuthError extends BaseMessage {\n  type: CloudToGlassesMessageType.AUTH_ERROR;\n  message: string;\n}\n\n//===========================================================\n// Updates\n//===========================================================\n\n/**\n * Display update to glasses\n */\nexport interface DisplayEvent extends BaseMessage {\n  type: CloudToGlassesMessageType.DISPLAY_EVENT;\n  layout: Layout;\n  durationMs?: number;\n}\n\n/**\n * App state change to glasses\n */\nexport interface AppStateChange extends BaseMessage {\n  type: CloudToGlassesMessageType.APP_STATE_CHANGE;\n  // userSession: Partial<UserSession>;\n  error?: string;\n}\n\n/**\n * Microphone state change to glasses\n */\nexport interface MicrophoneStateChange extends BaseMessage {\n  type: CloudToGlassesMessageType.MICROPHONE_STATE_CHANGE;\n  // userSession: Partial<UserSession>;\n  isMicrophoneEnabled: boolean;\n  requiredData: Array<\"pcm\" | \"transcription\" | \"pcm_or_transcription\">;\n  bypassVad?: boolean; // NEW: PCM subscription bypass\n}\n\n/**\n * Photo request to glasses\n */\nexport interface PhotoRequestToGlasses extends BaseMessage {\n  type: CloudToGlassesMessageType.PHOTO_REQUEST;\n  // userSession: Partial<UserSession>;\n  requestId: string;\n  appId: string;\n  saveToGallery?: boolean;\n  webhookUrl?: string; // URL where ASG should send the photo directly\n  authToken?: string; // Auth token for webhook authentication\n  /** Desired capture size to guide device resolution selection */\n  size?: \"small\" | \"medium\" | \"large\" | \"full\";\n  /** Image compression level: none, medium, or heavy */\n  compress?: \"none\" | \"medium\" | \"heavy\";\n}\n\n/**\n * LED color type for RGB LED control\n */\nexport type LedColor = \"red\" | \"green\" | \"blue\" | \"orange\" | \"white\";\n\n/**\n * RGB LED control request to glasses\n */\nexport interface RgbLedControlToGlasses extends BaseMessage {\n  type: CloudToGlassesMessageType.RGB_LED_CONTROL;\n  requestId: string;\n  appId: string;\n  action: \"on\" | \"off\"; // Only low-level on/off actions\n  color?: LedColor; // LED color name\n  ontime?: number;\n  offtime?: number;\n  count?: number;\n}\n\n// TODO(isaiah): Deprecated, remove this after new mobile client refactor complete, and we migrate to SettingsStateChange.\n/**\n * Settings update to glasses\n */\nexport interface SettingsUpdate extends BaseMessage {\n  type: CloudToGlassesMessageType.SETTINGS_UPDATE;\n  sessionId: string;\n  settings: {\n    useOnboardMic: boolean;\n    contextualDashboard: boolean;\n    metricSystemEnabled: boolean;\n    headUpAngle: number;\n    brightness: number;\n    autoBrightness: boolean;\n    sensingEnabled: boolean;\n    alwaysOnStatusBar: boolean;\n    bypassVad: boolean;\n    bypassAudioEncoding: boolean;\n  };\n}\n\n/**\n * LiveKit info for client to connect & publish\n */\nexport interface LiveKitInfo extends BaseMessage {\n  type: CloudToGlassesMessageType.LIVEKIT_INFO;\n  url: string;\n  roomName: string;\n  token: string;\n}\n\n//===========================================================\n// RTMP Streaming Commands\n//===========================================================\n\n/**\n * Start RTMP stream command to glasses\n */\nexport interface StartRtmpStream extends BaseMessage {\n  type: CloudToGlassesMessageType.START_RTMP_STREAM;\n  rtmpUrl: string;\n  appId: string;\n  streamId?: string;\n  video?: any; // Video configuration\n  audio?: any; // Audio configuration\n  stream?: any; // Stream configuration\n}\n\n/**\n * Stop RTMP stream command to glasses\n */\nexport interface StopRtmpStream extends BaseMessage {\n  type: CloudToGlassesMessageType.STOP_RTMP_STREAM;\n  appId: string;\n  streamId?: string;\n}\n\n/**\n * Keep RTMP stream alive command to glasses\n */\nexport interface KeepRtmpStreamAlive extends BaseMessage {\n  type: CloudToGlassesMessageType.KEEP_RTMP_STREAM_ALIVE;\n  streamId: string;\n  ackId: string;\n}\n\n//===========================================================\n// Location Service Commands\n//===========================================================\n\n/**\n * Sets the continuous location update tier on the device.\n */\nexport interface SetLocationTier extends BaseMessage {\n  type: CloudToGlassesMessageType.SET_LOCATION_TIER;\n  tier: \"realtime\" | \"high\" | \"tenMeters\" | \"hundredMeters\" | \"kilometer\" | \"threeKilometers\" | \"reduced\" | \"standard\";\n}\n\n/**\n * Requests a single, on-demand location fix from the device.\n */\nexport interface RequestSingleLocation extends BaseMessage {\n  type: CloudToGlassesMessageType.REQUEST_SINGLE_LOCATION;\n  accuracy: string; // The accuracy tier requested by the app\n  correlationId: string; // To match the response with the poll request\n}\n\n/**\n * Audio play request to glasses\n */\nexport interface AudioPlayRequestToGlasses extends BaseMessage {\n  type: CloudToGlassesMessageType.AUDIO_PLAY_REQUEST;\n  // userSession: Partial<UserSession>;\n  requestId: string;\n  appId: string;\n  audioUrl: string; // URL to audio file for download and play\n  volume?: number; // Volume level 0.0-1.0, defaults to 1.0\n  stopOtherAudio?: boolean; // Whether to stop other audio playback, defaults to true\n}\n\n/**\n * Audio stop request to glasses\n */\nexport interface AudioStopRequestToGlasses extends BaseMessage {\n  type: CloudToGlassesMessageType.AUDIO_STOP_REQUEST;\n  // userSession: Partial<UserSession>;\n  appId: string;\n}\n\n/**\n * WiFi setup request to glasses/mobile\n */\nexport interface ShowWifiSetup extends BaseMessage {\n  type: CloudToGlassesMessageType.SHOW_WIFI_SETUP;\n  reason?: string;\n  appPackageName?: string;\n}\n\n/**\n * UDP ping acknowledgment from cloud to mobile\n * Sent when the Go UDP listener receives a ping from the mobile client\n */\nexport interface UdpPingAck extends BaseMessage {\n  type: CloudToGlassesMessageType.UDP_PING_ACK;\n}\n\n/**\n * Union type for all messages from cloud to glasses\n */\nexport type CloudToGlassesMessage =\n  | ConnectionAck\n  | ConnectionError\n  | AuthError\n  | DisplayEvent\n  | AppStateChange\n  | MicrophoneStateChange\n  | PhotoRequestToGlasses\n  | RgbLedControlToGlasses\n  | AudioPlayRequestToGlasses\n  | AudioStopRequestToGlasses\n  | SettingsUpdate\n  | StartRtmpStream\n  | StopRtmpStream\n  | KeepRtmpStreamAlive\n  | SetLocationTier\n  | RequestSingleLocation\n  | LiveKitInfo\n  | ShowWifiSetup\n  | UdpPingAck;\n\n//===========================================================\n// Type guards\n//===========================================================\n\nexport function isResponse(message: CloudToGlassesMessage): boolean {\n  return ResponseTypes.includes(message.type as any);\n}\n\nexport function isUpdate(message: CloudToGlassesMessage): boolean {\n  return UpdateTypes.includes(message.type as any);\n}\n\n// Individual type guards\nexport function isConnectionAck(message: CloudToGlassesMessage): message is ConnectionAck {\n  return message.type === CloudToGlassesMessageType.CONNECTION_ACK;\n}\n\nexport function isConnectionError(message: CloudToGlassesMessage): message is ConnectionError {\n  return message.type === CloudToGlassesMessageType.CONNECTION_ERROR;\n}\n\nexport function isAuthError(message: CloudToGlassesMessage): message is AuthError {\n  return message.type === CloudToGlassesMessageType.AUTH_ERROR;\n}\n\nexport function isDisplayEvent(message: CloudToGlassesMessage): message is DisplayEvent {\n  return message.type === CloudToGlassesMessageType.DISPLAY_EVENT;\n}\n\nexport function isAppStateChange(message: CloudToGlassesMessage): message is AppStateChange {\n  return message.type === CloudToGlassesMessageType.APP_STATE_CHANGE;\n}\n\nexport function isMicrophoneStateChange(message: CloudToGlassesMessage): message is MicrophoneStateChange {\n  return message.type === CloudToGlassesMessageType.MICROPHONE_STATE_CHANGE;\n}\n\nexport function isPhotoRequest(message: CloudToGlassesMessage): message is PhotoRequestToGlasses {\n  return message.type === CloudToGlassesMessageType.PHOTO_REQUEST;\n}\n\nexport function isRgbLedControl(message: CloudToGlassesMessage): message is RgbLedControlToGlasses {\n  return message.type === CloudToGlassesMessageType.RGB_LED_CONTROL;\n}\n\nexport function isSettingsUpdate(message: CloudToGlassesMessage): message is SettingsUpdate {\n  return message.type === CloudToGlassesMessageType.SETTINGS_UPDATE;\n}\n\nexport function isStartRtmpStream(message: CloudToGlassesMessage): message is StartRtmpStream {\n  return message.type === CloudToGlassesMessageType.START_RTMP_STREAM;\n}\n\nexport function isStopRtmpStream(message: CloudToGlassesMessage): message is StopRtmpStream {\n  return message.type === CloudToGlassesMessageType.STOP_RTMP_STREAM;\n}\n\nexport function isKeepRtmpStreamAlive(message: CloudToGlassesMessage): message is KeepRtmpStreamAlive {\n  return message.type === CloudToGlassesMessageType.KEEP_RTMP_STREAM_ALIVE;\n}\n\nexport function isAudioPlayRequestToGlasses(message: CloudToGlassesMessage): message is AudioPlayRequestToGlasses {\n  return message.type === CloudToGlassesMessageType.AUDIO_PLAY_REQUEST;\n}\n\nexport function isAudioStopRequestToGlasses(message: CloudToGlassesMessage): message is AudioStopRequestToGlasses {\n  return message.type === CloudToGlassesMessageType.AUDIO_STOP_REQUEST;\n}\n","// src/messages/app-to-cloud.ts\n\nimport {BaseMessage} from \"./base\"\nimport {AppToCloudMessageType} from \"../message-types\"\nimport {ExtendedStreamType, LocationStreamRequest} from \"../streams\"\nimport {DisplayRequest} from \"../layouts\"\nimport {DashboardContentUpdate, DashboardModeChange, DashboardSystemUpdate} from \"../dashboard\"\nimport type {VideoConfig, AudioConfig, StreamConfig} from \"../rtmp-stream\"\nimport {LedColor} from \"./cloud-to-glasses\"\n\n// a subscription can now be either a simple string or our new rich object\nexport type SubscriptionRequest = ExtendedStreamType | LocationStreamRequest\n\n/**\n * Connection initialization from App\n */\nexport interface AppConnectionInit extends BaseMessage {\n  type: AppToCloudMessageType.CONNECTION_INIT\n  packageName: string\n  sessionId: string\n  apiKey: string\n}\n\n/**\n * Subscription update from App\n */\nexport interface AppSubscriptionUpdate extends BaseMessage {\n  type: AppToCloudMessageType.SUBSCRIPTION_UPDATE\n  packageName: string\n  subscriptions: SubscriptionRequest[]\n}\n\n/**\n * Photo request from App\n */\nexport interface PhotoRequest extends BaseMessage {\n  type: AppToCloudMessageType.PHOTO_REQUEST\n  packageName: string\n  requestId: string // SDK-generated request ID to track the request\n  saveToGallery?: boolean\n  customWebhookUrl?: string // Custom webhook URL to override TPA's default\n  authToken?: string // Auth token for custom webhook authentication\n  /** Desired photo size sent by App. Defaults to 'medium' if omitted. */\n  size?: \"small\" | \"medium\" | \"large\" | \"full\"\n  /** Image compression level: none, medium, or heavy. Defaults to none. */\n  compress?: \"none\" | \"medium\" | \"heavy\"\n}\n\n/**\n * RGB LED control request from App\n */\nexport interface RgbLedControlRequest extends BaseMessage {\n  type: AppToCloudMessageType.RGB_LED_CONTROL\n  packageName: string\n  requestId: string // SDK-generated request ID to track the request\n  action: \"on\" | \"off\" // Only low-level on/off actions\n  color?: LedColor // LED color name\n  ontime?: number // LED on duration in ms\n  offtime?: number // LED off duration in ms\n  count?: number // Number of on/off cycles\n}\n\n// Video, Audio and Stream configuration interfaces are imported from '../rtmp-stream'\n\n/**\n * RTMP stream request from App\n */\nexport interface RtmpStreamRequest extends BaseMessage {\n  type: AppToCloudMessageType.RTMP_STREAM_REQUEST\n  packageName: string\n  rtmpUrl: string\n  video?: VideoConfig\n  audio?: AudioConfig\n  stream?: StreamConfig\n}\n\n/**\n * RTMP stream stop request from App\n */\nexport interface RtmpStreamStopRequest extends BaseMessage {\n  type: AppToCloudMessageType.RTMP_STREAM_STOP\n  packageName: string\n  streamId?: string // Optional stream ID to specify which stream to stop\n}\n\n// defines the structure for our new on-demand location poll command\nexport interface AppLocationPollRequest extends BaseMessage {\n  type: AppToCloudMessageType.LOCATION_POLL_REQUEST\n  packageName: string\n  sessionId: string\n  accuracy: string\n  correlationId: string\n}\n\n/**\n * Re-stream destination for managed streams\n */\nexport interface RestreamDestination {\n  /** RTMP URL like rtmp://youtube.com/live/STREAM-KEY */\n  url: string\n  /** Optional friendly name like \"YouTube\" or \"Twitch\" */\n  name?: string\n}\n\n/**\n * Managed RTMP stream request from App\n * The cloud handles the RTMP endpoint and returns HLS/DASH URLs\n */\nexport interface ManagedStreamRequest extends BaseMessage {\n  type: AppToCloudMessageType.MANAGED_STREAM_REQUEST\n  packageName: string\n  quality?: \"720p\" | \"1080p\"\n  enableWebRTC?: boolean\n  video?: VideoConfig\n  audio?: AudioConfig\n  stream?: StreamConfig\n  /** Optional RTMP destinations to re-stream to (YouTube, Twitch, etc) */\n  restreamDestinations?: RestreamDestination[]\n}\n\n/**\n * Managed RTMP stream stop request from App\n */\nexport interface ManagedStreamStopRequest extends BaseMessage {\n  type: AppToCloudMessageType.MANAGED_STREAM_STOP\n  packageName: string\n}\n\n/**\n * Stream status check request from App\n * Checks if there are any existing streams (managed or unmanaged) for the current user\n */\nexport interface StreamStatusCheckRequest extends BaseMessage {\n  type: AppToCloudMessageType.STREAM_STATUS_CHECK\n  packageName: string\n  sessionId: string\n}\n\n/**\n * Audio play request from App\n */\nexport interface AudioPlayRequest extends BaseMessage {\n  type: AppToCloudMessageType.AUDIO_PLAY_REQUEST\n  packageName: string\n  requestId: string // SDK-generated request ID to track the request\n  audioUrl: string // URL to audio file for download and play\n  volume?: number // Volume level 0.0-1.0, defaults to 1.0\n  stopOtherAudio?: boolean // Whether to stop other audio playback, defaults to true\n  /**\n   * Track ID for audio playback (defaults to 0)\n   * - 0: speaker (default audio playback)\n   * - 1: app_audio (app-specific audio)\n   * - 2: tts (text-to-speech audio)\n   * Use different track IDs to play multiple audio streams simultaneously (mixing)\n   */\n  trackId?: number\n}\n\n/**\n * Audio stop request from App\n */\nexport interface AudioStopRequest extends BaseMessage {\n  type: AppToCloudMessageType.AUDIO_STOP_REQUEST\n  packageName: string\n  /**\n   * Track ID to stop (optional)\n   * 0: speaker (default audio playback)\n   * 1: app_audio (app-specific audio)\n   * 2: tts (text-to-speech audio)\n   * If omitted, stops all tracks\n   */\n  trackId?: number\n  sessionId?: string // Session ID for routing\n}\n\n/**\n * WiFi setup request from App\n */\nexport interface RequestWifiSetup extends BaseMessage {\n  type: AppToCloudMessageType.REQUEST_WIFI_SETUP\n  packageName: string\n  sessionId: string\n  reason?: string\n}\n\n/**\n * Ownership release message from App\n * Sent before intentional disconnect to signal clean handoff (no resurrection needed)\n */\nexport interface OwnershipReleaseMessage extends BaseMessage {\n  type: AppToCloudMessageType.OWNERSHIP_RELEASE\n  packageName: string\n  sessionId: string\n  reason: \"switching_clouds\" | \"clean_shutdown\" | \"user_logout\"\n}\n\n/**\n * Union type for all messages from Apps to cloud\n */\nexport type AppToCloudMessage =\n  | AppConnectionInit\n  | AppSubscriptionUpdate\n  | AppLocationPollRequest\n  | DisplayRequest\n  | PhotoRequest\n  | RgbLedControlRequest\n  | AudioPlayRequest\n  | AudioStopRequest\n  | RtmpStreamRequest\n  | RtmpStreamStopRequest\n  | ManagedStreamRequest\n  | ManagedStreamStopRequest\n  | StreamStatusCheckRequest\n  | DashboardContentUpdate\n  | DashboardModeChange\n  | DashboardSystemUpdate\n  | RequestWifiSetup\n  // Session lifecycle\n  | OwnershipReleaseMessage\n  // New App-to-App communication messages\n  | AppBroadcastMessage\n  | AppDirectMessage\n  | AppUserDiscovery\n  | AppRoomJoin\n  | AppRoomLeave\n\n/**\n * Type guard to check if a message is a App connection init\n */\nexport function isAppConnectionInit(message: AppToCloudMessage): message is AppConnectionInit {\n  return message.type === AppToCloudMessageType.CONNECTION_INIT\n}\n\n/**\n * Type guard to check if a message is a App subscription update\n */\nexport function isAppSubscriptionUpdate(message: AppToCloudMessage): message is AppSubscriptionUpdate {\n  return message.type === AppToCloudMessageType.SUBSCRIPTION_UPDATE\n}\n\n/**\n * Type guard to check if a message is a App display request\n */\nexport function isDisplayRequest(message: AppToCloudMessage): message is DisplayRequest {\n  return message.type === AppToCloudMessageType.DISPLAY_REQUEST\n}\n\n/**\n * Type guard to check if a message is a App photo request\n */\nexport function isPhotoRequest(message: AppToCloudMessage): message is PhotoRequest {\n  return message.type === AppToCloudMessageType.PHOTO_REQUEST\n}\n\n/**\n * Type guard to check if a message is a RGB LED control request\n */\nexport function isRgbLedControlRequest(message: AppToCloudMessage): message is RgbLedControlRequest {\n  return message.type === AppToCloudMessageType.RGB_LED_CONTROL\n}\n\n/**\n * Type guard to check if a message is a App audio play request\n */\nexport function isAudioPlayRequest(message: AppToCloudMessage): message is AudioPlayRequest {\n  return message.type === AppToCloudMessageType.AUDIO_PLAY_REQUEST\n}\n\n/**\n * Type guard to check if a message is a App audio stop request\n */\nexport function isAudioStopRequest(message: AppToCloudMessage): message is AudioStopRequest {\n  return message.type === AppToCloudMessageType.AUDIO_STOP_REQUEST\n}\n\n/**\n * Type guard to check if a message is a dashboard content update\n */\nexport function isDashboardContentUpdate(message: AppToCloudMessage): message is DashboardContentUpdate {\n  return message.type === AppToCloudMessageType.DASHBOARD_CONTENT_UPDATE\n}\n\n/**\n * Type guard to check if a message is a dashboard mode change\n */\nexport function isDashboardModeChange(message: AppToCloudMessage): message is DashboardModeChange {\n  return message.type === AppToCloudMessageType.DASHBOARD_MODE_CHANGE\n}\n\n/**\n * Type guard to check if a message is a dashboard system update\n */\nexport function isDashboardSystemUpdate(message: AppToCloudMessage): message is DashboardSystemUpdate {\n  return message.type === AppToCloudMessageType.DASHBOARD_SYSTEM_UPDATE\n}\n\n/**\n * Type guard to check if a message is a managed stream request\n */\nexport function isManagedStreamRequest(message: AppToCloudMessage): message is ManagedStreamRequest {\n  return message.type === AppToCloudMessageType.MANAGED_STREAM_REQUEST\n}\n\n/**\n * Type guard to check if a message is a managed stream stop request\n */\nexport function isManagedStreamStopRequest(message: AppToCloudMessage): message is ManagedStreamStopRequest {\n  return message.type === AppToCloudMessageType.MANAGED_STREAM_STOP\n}\n\n//===========================================================\n// App-to-App Communication Messages\n//===========================================================\n\n/**\n * Broadcast message to all users with the same App active\n */\nexport interface AppBroadcastMessage extends BaseMessage {\n  type: AppToCloudMessageType.APP_BROADCAST_MESSAGE\n  packageName: string\n  sessionId: string\n  payload: any\n  messageId: string\n  senderUserId: string\n}\n\n/**\n * Direct message to a specific user with the same App active\n */\nexport interface AppDirectMessage extends BaseMessage {\n  type: AppToCloudMessageType.APP_DIRECT_MESSAGE\n  packageName: string\n  sessionId: string\n  targetUserId: string\n  payload: any\n  messageId: string\n  senderUserId: string\n}\n\n/**\n * Request to discover other users with the same App active\n */\nexport interface AppUserDiscovery extends BaseMessage {\n  type: AppToCloudMessageType.APP_USER_DISCOVERY\n  packageName: string\n  sessionId: string\n  includeUserProfiles?: boolean\n}\n\n/**\n * Join a communication room for group messaging\n */\nexport interface AppRoomJoin extends BaseMessage {\n  type: AppToCloudMessageType.APP_ROOM_JOIN\n  packageName: string\n  sessionId: string\n  roomId: string\n  roomConfig?: {\n    maxUsers?: number\n    isPrivate?: boolean\n    metadata?: any\n  }\n}\n\n/**\n * Leave a communication room\n */\nexport interface AppRoomLeave extends BaseMessage {\n  type: AppToCloudMessageType.APP_ROOM_LEAVE\n  packageName: string\n  sessionId: string\n  roomId: string\n}\n\n/**\n * Type guard to check if a message is an RTMP stream request\n */\nexport function isRtmpStreamRequest(message: AppToCloudMessage): message is RtmpStreamRequest {\n  return message.type === AppToCloudMessageType.RTMP_STREAM_REQUEST\n}\n\n/**\n * Type guard to check if a message is an RTMP stream stop request\n */\nexport function isRtmpStreamStopRequest(message: AppToCloudMessage): message is RtmpStreamStopRequest {\n  return message.type === AppToCloudMessageType.RTMP_STREAM_STOP\n}\n\n/**\n * Type guard to check if a message is an ownership release message\n */\nexport function isOwnershipRelease(message: AppToCloudMessage): message is OwnershipReleaseMessage {\n  return message.type === AppToCloudMessageType.OWNERSHIP_RELEASE\n}\n","// src/index.ts\nexport * from \"./types/token\";\n\n// Message type enums\nexport * from \"./types/message-types\";\n\n// Base message type\nexport * from \"./types/messages/base\";\n\n// Messages by direction - export everything except the conflicting type guards\nexport * from \"./types/messages/glasses-to-cloud\";\nexport * from \"./types/messages/cloud-to-glasses\";\nexport * from \"./types/messages/app-to-cloud\";\n\n// Utility exports\nexport * from \"./utils/bitmap-utils\";\nexport * from \"./utils/animation-utils\";\n\n// Export cloud-to-app but exclude the conflicting type guards\nexport {\n  // Types\n  AppConnectionAck,\n  AppConnectionError,\n  AppStopped,\n  SettingsUpdate as AppSettingsUpdate, // Alias to avoid conflict with cloud-to-glasses SettingsUpdate\n  CapabilitiesUpdate,\n  DataStream,\n  CloudToAppMessage,\n  TranslationData,\n  ToolCall,\n  StandardConnectionError,\n  CustomMessage,\n  ManagedStreamStatus,\n  StreamStatusCheckResponse,\n  OutputStatus,\n  MentraosSettingsUpdate,\n  TranscriptionData,\n  TranscriptionMetadata,\n  SonioxToken,\n  AudioChunk,\n  PermissionError,\n  PermissionErrorDetail,\n  AudioPlayResponse,\n  // Type guards (excluding isPhotoResponse and isRtmpStreamStatus which conflict)\n  isAppConnectionAck,\n  isAppConnectionError,\n  isAppStopped,\n  isSettingsUpdate,\n  isCapabilitiesUpdate,\n  isDataStream,\n  isAudioChunk,\n  isStreamStatusCheckResponse,\n  isDashboardModeChanged,\n  isDashboardAlwaysOnChanged,\n  isManagedStreamStatus,\n  // Re-export the cloud-to-app versions of these type guards since they're the ones\n  // that should be used when dealing with CloudToAppMessage types\n  isPhotoResponse as isPhotoResponseFromCloud,\n  isRgbLedControlResponse as isRgbLedControlResponseFromCloud,\n  isRtmpStreamStatus as isRtmpStreamStatusFromCloud,\n} from \"./types/messages/cloud-to-app\";\n\n// Stream types\nexport * from \"./types/streams\";\n\n// Layout types\nexport * from \"./types/layouts\";\n\n// Dashboard types\nexport * from \"./types/dashboard\";\n\n// RTMP streaming types\nexport * from \"./types/rtmp-stream\";\n\n// Other system enums\nexport { AppType, LayoutType, ViewType, AppSettingType, HardwareType, HardwareRequirementLevel } from \"./types/enums\";\n\n// Core model interfaces\nexport * from \"./types/models\";\n\n// Webhook interfaces\nexport * from \"./types/webhooks\";\n\n// Capability Discovery types\nexport * from \"./types/capabilities\";\n\n// App session and server exports\nexport * from \"./app/index\";\n\n// Logging exports\nexport * from \"./logging/logger\";\n\n// Re-export common types for convenience\n// This allows developers to import commonly used types directly from the package root\n// without having to know exactly which file they come from\n\n// From messages/glasses-to-cloud.ts\nexport {\n  ButtonPress,\n  HeadPosition,\n  TouchEvent,\n  GlassesBatteryUpdate,\n  PhoneBatteryUpdate,\n  GlassesConnectionState,\n  LocationUpdate,\n  CalendarEvent,\n  Vad,\n  PhoneNotification,\n  PhoneNotificationDismissed,\n  StartApp,\n  StopApp,\n  ConnectionInit,\n  DashboardState,\n  OpenDashboard,\n  GlassesToCloudMessage,\n  PhotoResponse,\n  RgbLedControlResponse,\n  PhotoErrorCode,\n  PhotoStage,\n  ConnectionState,\n  PhotoErrorDetails,\n  RtmpStreamStatus,\n  KeepAliveAck,\n} from \"./types/messages/glasses-to-cloud\";\n\n// From messages/cloud-to-glasses.ts\nexport {\n  ConnectionAck,\n  ConnectionError,\n  AuthError,\n  DisplayEvent,\n  AppStateChange,\n  MicrophoneStateChange,\n  CloudToGlassesMessage,\n  PhotoRequestToGlasses,\n  RgbLedControlToGlasses,\n  SettingsUpdate,\n  StartRtmpStream,\n  StopRtmpStream,\n  KeepRtmpStreamAlive,\n  LedColor,\n} from \"./types/messages/cloud-to-glasses\";\n\n// From messages/app-to-cloud.ts\nexport {\n  AppConnectionInit,\n  AppSubscriptionUpdate,\n  RtmpStreamRequest,\n  RtmpStreamStopRequest,\n  AppToCloudMessage,\n  PhotoRequest,\n  RgbLedControlRequest,\n} from \"./types/messages/app-to-cloud\";\n\n// From layout.ts\nexport {\n  TextWall,\n  DoubleTextWall,\n  DashboardCard,\n  ReferenceCard,\n  Layout,\n  DisplayRequest,\n  BitmapView,\n  ClearView,\n} from \"./types/layouts\";\n\n// Type guards - re-export the most commonly used ones for convenience\nexport {\n  isButtonPress,\n  isHeadPosition,\n  isConnectionInit,\n  isStartApp,\n  isStopApp,\n  isPhotoResponse as isPhotoResponseFromGlasses,\n  isRgbLedControlResponse as isRgbLedControlResponseFromGlasses,\n  isRtmpStreamStatus as isRtmpStreamStatusFromGlasses,\n  isKeepAliveAck,\n  isPhoneNotificationDismissed,\n} from \"./types/messages/glasses-to-cloud\";\n\nexport {\n  isConnectionAck,\n  isDisplayEvent,\n  isAppStateChange,\n  isPhotoRequest,\n  isSettingsUpdate as isSettingsUpdateToGlasses,\n  isStartRtmpStream,\n  isStopRtmpStream,\n  isKeepRtmpStreamAlive,\n  isRgbLedControl,\n} from \"./types/messages/cloud-to-glasses\";\n\nexport {\n  isAppConnectionInit,\n  isAppSubscriptionUpdate,\n  isDisplayRequest,\n  isRtmpStreamRequest,\n  isRtmpStreamStopRequest,\n  isPhotoRequest as isPhotoRequestFromApp,\n  isRgbLedControlRequest,\n  isOwnershipRelease,\n} from \"./types/messages/app-to-cloud\";\n\n// Export setting-related types\nexport {\n  BaseAppSetting,\n  AppSetting,\n  AppSettings,\n  AppConfig,\n  validateAppConfig,\n  ToolSchema,\n  ToolParameterSchema,\n  HardwareRequirement,\n} from \"./types/models\";\n\n// Export RTMP streaming types\nexport { VideoConfig, AudioConfig, StreamConfig, StreamStatusHandler } from \"./types/rtmp-stream\";\n\n// Export app session modules\nexport * from \"./app/session/modules\";\n\n// Export photo data types\nexport { PhotoData } from \"./types/photo-data\";\n\n// Export device state types (WebSocket-based observables)\nexport { DeviceState } from \"./app/session/device-state\";\nexport { Observable } from \"./utils/Observable\";\n\n// Re-export types from @mentra/types so SDK users don't need to install it separately\nexport type { GlassesInfo } from \"@mentra/types\";\n\n/**\n * WebSocket error information\n */\nexport interface WebSocketError {\n  code: string;\n  message: string;\n  details?: unknown;\n}\n\nexport { AuthenticatedRequest } from \"./types/index\";\n","/**\n * 🎨 Bitmap Utilities Module\n *\n * Provides helper functions for working with bitmap images in MentraOS applications.\n * Includes file loading, data validation, and format conversion utilities.\n *\n * @example\n * ```typescript\n * import { BitmapUtils } from '@mentra/sdk';\n *\n * // Load a single BMP file\n * const bmpHex = await BitmapUtils.loadBmpAsHex('./my-image.bmp');\n * session.layouts.showBitmapView(bmpHex);\n *\n * // Load multiple animation frames\n * const frames = await BitmapUtils.loadBmpFrames('./animations', 10);\n * session.layouts.showBitmapAnimation(frames, 1500, true);\n * ```\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport { Jimp } from \"jimp\";\n\n/**\n * Validation result for bitmap data\n */\nexport interface BitmapValidation {\n  /** Whether the bitmap data is valid */\n  isValid: boolean;\n  /** Total byte count of the bitmap */\n  byteCount: number;\n  /** Number of black (non-FF) pixels found */\n  blackPixels: number;\n  /** Array of validation error messages */\n  errors: string[];\n  /** Additional metadata about the bitmap */\n  metadata?: {\n    /** Expected bitmap dimensions (if detectable) */\n    dimensions?: { width: number; height: number };\n    /** Bitmap file format info */\n    format?: string;\n  };\n}\n\n/**\n * Options for loading bitmap frames\n */\nexport interface LoadFramesOptions {\n  /** File name pattern (default: 'animation_10_frame_{i}.bmp') */\n  filePattern?: string;\n  /** Starting frame number (default: 1) */\n  startFrame?: number;\n  /** Validate each frame during loading (default: true) */\n  validateFrames?: boolean;\n  /** Continue loading if a frame is missing (default: false) */\n  skipMissingFrames?: boolean;\n}\n\n/**\n * Utility class for working with bitmap images in MentraOS applications\n */\nexport class BitmapUtils {\n  static async convert24BitTo1BitBMP(input24BitBmp: Buffer): Promise<Buffer> {\n    // Read header information from 24-bit BMP\n    const width = input24BitBmp.readUInt32LE(18);\n    const height = Math.abs(input24BitBmp.readInt32LE(22)); // Height can be negative (top-down BMP)\n    const isTopDown = input24BitBmp.readInt32LE(22) < 0;\n    const bitsPerPixel = input24BitBmp.readUInt16LE(28);\n\n    if (bitsPerPixel !== 24) {\n      throw new Error(\"Input must be a 24-bit BMP\");\n    }\n\n    // Calculate row sizes (both must be 4-byte aligned)\n    const rowSize24 = Math.ceil((width * 3) / 4) * 4;\n    const rowSize1 = Math.ceil(width / 32) * 4; // 32 pixels per 4 bytes\n\n    // Calculate sizes for 1-bit BMP\n    const colorTableSize = 8; // 2 colors * 4 bytes each\n    const headerSize = 54 + colorTableSize;\n    const pixelDataSize = rowSize1 * height;\n    const fileSize = headerSize + pixelDataSize;\n\n    // Create new buffer for 1-bit BMP\n    const output1BitBmp = Buffer.alloc(fileSize);\n    let offset = 0;\n\n    // Write BMP file header (14 bytes)\n    output1BitBmp.write(\"BM\", offset);\n    offset += 2; // Signature\n    output1BitBmp.writeUInt32LE(fileSize, offset);\n    offset += 4; // File size\n    output1BitBmp.writeUInt16LE(0, offset);\n    offset += 2; // Reserved 1\n    output1BitBmp.writeUInt16LE(0, offset);\n    offset += 2; // Reserved 2\n    output1BitBmp.writeUInt32LE(headerSize, offset);\n    offset += 4; // Pixel data offset\n\n    // Write DIB header (40 bytes)\n    output1BitBmp.writeUInt32LE(40, offset);\n    offset += 4; // DIB header size\n    output1BitBmp.writeInt32LE(width, offset);\n    offset += 4; // Width\n    output1BitBmp.writeInt32LE(height, offset);\n    offset += 4; // Height (positive for bottom-up)\n    output1BitBmp.writeUInt16LE(1, offset);\n    offset += 2; // Planes\n    output1BitBmp.writeUInt16LE(1, offset);\n    offset += 2; // Bits per pixel (1-bit)\n    output1BitBmp.writeUInt32LE(0, offset);\n    offset += 4; // Compression (none)\n    output1BitBmp.writeUInt32LE(pixelDataSize, offset);\n    offset += 4; // Image size\n    output1BitBmp.writeInt32LE(2835, offset);\n    offset += 4; // X pixels per meter (72 DPI)\n    output1BitBmp.writeInt32LE(2835, offset);\n    offset += 4; // Y pixels per meter (72 DPI)\n    output1BitBmp.writeUInt32LE(2, offset);\n    offset += 4; // Colors used\n    output1BitBmp.writeUInt32LE(2, offset);\n    offset += 4; // Important colors\n\n    // Write color table (8 bytes)\n    // Black (index 0): B=0, G=0, R=0, Reserved=0\n    output1BitBmp.writeUInt32LE(0x00000000, offset);\n    offset += 4;\n    // White (index 1): B=255, G=255, R=255, Reserved=0\n    output1BitBmp.writeUInt8(255, offset++); // Blue\n    output1BitBmp.writeUInt8(255, offset++); // Green\n    output1BitBmp.writeUInt8(255, offset++); // Red\n    output1BitBmp.writeUInt8(0, offset++); // Reserved\n\n    // Convert pixel data from 24-bit to 1-bit\n    const pixelDataStart24 = 54; // 24-bit BMP has no color table\n\n    for (let y = 0; y < height; y++) {\n      // BMP files are usually stored bottom-up\n      const sourceY = isTopDown ? y : height - 1 - y;\n      const destY = height - 1 - y; // Always write bottom-up for compatibility\n\n      // Initialize the row with zeros\n      const rowData = Buffer.alloc(rowSize1);\n\n      for (let x = 0; x < width; x++) {\n        // Get pixel from 24-bit BMP\n        const offset24 = pixelDataStart24 + sourceY * rowSize24 + x * 3;\n        const blue = input24BitBmp[offset24];\n        const green = input24BitBmp[offset24 + 1];\n        const red = input24BitBmp[offset24 + 2];\n\n        // Determine if pixel is white (assuming pure black or white)\n        // White = 1, Black = 0\n        const isWhite = red > 128 || green > 128 || blue > 128 ? 1 : 0;\n\n        // Calculate bit position\n        const byteIndex = Math.floor(x / 8);\n        const bitPosition = 7 - (x % 8); // MSB first\n\n        // Set bit if white\n        if (isWhite) {\n          rowData[byteIndex] |= 1 << bitPosition;\n        }\n      }\n\n      // Write row to output buffer\n      const destOffset = offset + destY * rowSize1;\n      rowData.copy(output1BitBmp, destOffset);\n    }\n\n    return output1BitBmp;\n  }\n\n  /**\n   * Load a BMP file as hex string from filesystem\n   *\n   * @param filePath - Path to the BMP file\n   * @returns Promise resolving to hex-encoded bitmap data\n   * @throws Error if file cannot be read or is not a valid BMP\n   *\n   * @example\n   * ```typescript\n   * const bmpBase64 = await BitmapUtils.loadBmpFromFileAsBase64('./assets/icon.bmp');\n   * session.layouts.showBitmapView(bmpBase64);\n   * ```\n   */\n  static async fileToBase64(filePath: string): Promise<string> {\n    try {\n      const bmpData = await fs.readFile(filePath);\n\n      return this.bufferToBase64(bmpData);\n    } catch (error) {\n      if (error instanceof Error) {\n        throw new Error(\n          `Failed to load BMP file ${filePath}: ${error.message}`,\n        );\n      }\n      throw new Error(`Failed to load BMP file ${filePath}: Unknown error`);\n    }\n  }\n\n  static async bufferToBase64(bmpData: Buffer): Promise<string> {\n    return bmpData.toString(\"base64\");\n  }\n\n  static async padBase64Bitmap(\n    bmpBase64: string,\n    padding?: { left: number; top: number },\n  ): Promise<string> {\n    const buffer = Buffer.from(bmpBase64, \"base64\");\n    const paddedBuffer = await this.padBmpForGlasses(\n      buffer,\n      padding?.left,\n      padding?.top,\n    );\n    return paddedBuffer.toString(\"base64\");\n  }\n\n  static async padBmpForGlasses(\n    bmpData: Buffer,\n    leftPadding: number = 50,\n    topPadding: number = 35,\n  ): Promise<Buffer> {\n    try {\n      // Basic BMP validation - check for BMP signature\n      if (bmpData.length < 14 || bmpData[0] !== 0x42 || bmpData[1] !== 0x4d) {\n        throw new Error(\n          `Bmp data is not a valid BMP file (missing BM signature)`,\n        );\n      }\n\n      let finalBmpData = bmpData;\n\n      // Load the image with Jimp\n      const image = await Jimp.read(bmpData);\n\n      // Check if we need to add padding\n      if (image.width !== 576 || image.height !== 135) {\n        console.log(\n          `Adding padding to BMP since it isn't 576x135 (assuming it's 526x100!)(current: ${image.width}x${image.height})`,\n        );\n\n        // Create a new 576x135 white canvas\n        const paddedImage = new Jimp({\n          width: 576,\n          height: 135,\n          color: 0x00000000,\n        });\n\n        // // Calculate position to place the original image (with padding)\n        const leftPadding = 50; // 45px padding on left\n        const topPadding = 35; // 35px padding on top\n\n        // Composite the original image onto the white canvas\n        // paddedImage.composite(image, leftPadding, topPadding);\n        paddedImage.composite(image, leftPadding, topPadding);\n\n        finalBmpData = await this.convert24BitTo1BitBMP(\n          await paddedImage.getBuffer(\"image/bmp\"),\n        );\n      }\n      // No padding needed, just return as hex\n      console.log(`finalBmpData: ${finalBmpData.length} bytes`);\n      return finalBmpData;\n    } catch (error) {\n      if (error instanceof Error) {\n        throw new Error(`Failed to load BMP data: ${error.message}`);\n      }\n      throw new Error(`Failed to load BMP data: Unknown error`);\n    }\n  }\n\n  /**\n   * Load multiple BMP frames as hex array for animations\n   *\n   * @param basePath - Directory containing the frame files\n   * @param frameCount - Number of frames to load\n   * @param options - Loading options and configuration\n   * @returns Promise resolving to array of hex-encoded bitmap data\n   *\n   * @example\n   * ```typescript\n   * // Load 10 frames with default pattern\n   * const frames = await BitmapUtils.loadBmpFrames('./animations', 10);\n   *\n   * // Load with custom pattern\n   * const customFrames = await BitmapUtils.loadBmpFrames('./sprites', 8, {\n   *   filePattern: 'sprite_{i}.bmp',\n   *   startFrame: 0\n   * });\n   * ```\n   */\n  static async loadBmpFrames(\n    basePath: string,\n    frameCount: number,\n    options: LoadFramesOptions = {},\n  ): Promise<string[]> {\n    const {\n      filePattern = \"animation_10_frame_{i}.bmp\",\n      startFrame = 1,\n      validateFrames = true,\n      skipMissingFrames = false,\n    } = options;\n\n    const frames: string[] = [];\n    const errors: string[] = [];\n\n    for (let i = 0; i < frameCount; i++) {\n      const frameNumber = startFrame + i;\n      const fileName = filePattern.replace(\"{i}\", frameNumber.toString());\n      const filePath = path.join(basePath, fileName);\n\n      try {\n        const frameBase64 = await this.fileToBase64(filePath);\n\n        if (validateFrames) {\n          const validation = this.validateBase64Bitmap(frameBase64);\n          if (!validation.isValid) {\n            const errorMsg = `Frame ${frameNumber} validation failed: ${validation.errors.join(\n              \", \",\n            )}`;\n            if (skipMissingFrames) {\n              console.warn(`⚠️ ${errorMsg} - skipping`);\n              continue;\n            } else {\n              throw new Error(errorMsg);\n            }\n          }\n          console.log(\n            `✅ Frame ${frameNumber} validated (${validation.blackPixels} black pixels)`,\n          );\n        }\n\n        frames.push(frameBase64);\n      } catch (error) {\n        const errorMsg = `Failed to load frame ${frameNumber} (${fileName}): ${\n          error instanceof Error ? error.message : \"Unknown error\"\n        }`;\n\n        if (skipMissingFrames) {\n          console.warn(`⚠️ ${errorMsg} - skipping`);\n          continue;\n        } else {\n          errors.push(errorMsg);\n        }\n      }\n    }\n\n    if (errors.length > 0) {\n      throw new Error(`Failed to load frames:\\n${errors.join(\"\\n\")}`);\n    }\n\n    if (frames.length === 0) {\n      throw new Error(`No valid frames loaded from ${basePath}`);\n    }\n\n    console.log(`📚 Loaded ${frames.length} animation frames from ${basePath}`);\n    return frames;\n  }\n\n  /**\n   * Validate BMP hex data integrity and extract metadata\n   *\n   * @param hexString - Hex-encoded bitmap data\n   * @returns Validation result with detailed information\n   *\n   * @example\n   * ```typescript\n   * const validation = BitmapUtils.validateBmpHex(bmpHex);\n   * if (!validation.isValid) {\n   *   console.error('Invalid bitmap:', validation.errors);\n   * } else {\n   *   console.log(`Valid bitmap: ${validation.blackPixels} black pixels`);\n   * }\n   * ```\n   */\n  static validateBase64Bitmap(bmpFrame: string): BitmapValidation {\n    const errors: string[] = [];\n    let byteCount = 0;\n    let blackPixels = 0;\n    const metadata: BitmapValidation[\"metadata\"] = {};\n\n    try {\n      const hexString = Buffer.from(bmpFrame, \"base64\").toString(\"hex\");\n      // Basic hex validation\n      if (typeof hexString !== \"string\" || hexString.length === 0) {\n        errors.push(\"Hex string is empty or invalid\");\n        return { isValid: false, byteCount: 0, blackPixels: 0, errors };\n      }\n\n      if (hexString.length % 2 !== 0) {\n        errors.push(\"Hex string length must be even\");\n        return { isValid: false, byteCount: 0, blackPixels: 0, errors };\n      }\n\n      // Convert to buffer\n      const buffer = Buffer.from(hexString, \"hex\");\n      byteCount = buffer.length;\n\n      // BMP signature validation\n      if (buffer.length < 14) {\n        errors.push(\n          \"File too small to be a valid BMP (minimum 14 bytes for header)\",\n        );\n      } else {\n        if (buffer[0] !== 0x42 || buffer[1] !== 0x4d) {\n          errors.push('Invalid BMP signature (should start with \"BM\")');\n        }\n      }\n\n      // Size validation for MentraOS (576x135 = ~9782 bytes expected)\n      const expectedSize = 9782;\n      if (buffer.length < expectedSize - 100) {\n        // Allow some tolerance\n        errors.push(\n          `BMP too small (${buffer.length} bytes, expected ~${expectedSize})`,\n        );\n      } else if (buffer.length > expectedSize + 1000) {\n        // Allow some tolerance\n        errors.push(\n          `BMP too large (${buffer.length} bytes, expected ~${expectedSize})`,\n        );\n      }\n\n      // Extract BMP metadata if header is valid\n      if (buffer.length >= 54) {\n        try {\n          // BMP width and height are at offsets 18 and 22 (little-endian)\n          const width = buffer.readUInt32LE(18);\n          const height = buffer.readUInt32LE(22);\n          metadata.dimensions = { width, height };\n          metadata.format = \"BMP\";\n\n          // Validate dimensions for MentraOS glasses\n          if (width !== 576 || height !== 135) {\n            errors.push(\n              `Invalid dimensions (${width}x${height}, expected 576x135 for MentraOS)`,\n            );\n          }\n        } catch (e) {\n          errors.push(\"Failed to parse BMP header metadata\");\n        }\n      }\n\n      // Pixel data validation (assumes 54-byte header + pixel data)\n      if (buffer.length > 62) {\n        const pixelData = buffer.slice(62); // Skip BMP header\n        blackPixels = Array.from(pixelData).filter((b) => b !== 0xff).length;\n\n        if (blackPixels === 0) {\n          errors.push(\"No black pixels found (image appears to be all white)\");\n        }\n      } else {\n        errors.push(\"File too small to contain pixel data\");\n      }\n    } catch (error) {\n      errors.push(\n        `Failed to parse hex data: ${\n          error instanceof Error ? error.message : \"Unknown error\"\n        }`,\n      );\n    }\n\n    return {\n      isValid: errors.length === 0,\n      byteCount,\n      blackPixels,\n      errors,\n      metadata: Object.keys(metadata).length > 0 ? metadata : undefined,\n    };\n  }\n\n  /**\n   * Convert bitmap data between different formats\n   *\n   * @param data - Input bitmap data\n   * @param fromFormat - Source format ('hex' | 'base64' | 'buffer')\n   * @param toFormat - Target format ('hex' | 'base64' | 'buffer')\n   * @returns Converted bitmap data\n   *\n   * @example\n   * ```typescript\n   * const base64Data = BitmapUtils.convertFormat(hexData, 'hex', 'base64');\n   * const bufferData = BitmapUtils.convertFormat(base64Data, 'base64', 'buffer');\n   * ```\n   */\n  static convertFormat(\n    data: string | Buffer,\n    fromFormat: \"hex\" | \"base64\" | \"buffer\",\n    toFormat: \"hex\" | \"base64\" | \"buffer\",\n  ): string | Buffer {\n    let buffer: Buffer;\n\n    // Convert input to buffer\n    switch (fromFormat) {\n      case \"hex\":\n        buffer = Buffer.from(data as string, \"hex\");\n        break;\n      case \"base64\":\n        buffer = Buffer.from(data as string, \"base64\");\n        break;\n      case \"buffer\":\n        buffer = data as Buffer;\n        break;\n      default:\n        throw new Error(`Unsupported source format: ${fromFormat}`);\n    }\n\n    // Convert buffer to target format\n    switch (toFormat) {\n      case \"hex\":\n        return buffer.toString(\"hex\");\n      case \"base64\":\n        return buffer.toString(\"base64\");\n      case \"buffer\":\n        return buffer;\n      default:\n        throw new Error(`Unsupported target format: ${toFormat}`);\n    }\n  }\n\n  /**\n   * Get bitmap information without full validation\n   *\n   * @param hexString - Hex-encoded bitmap data\n   * @returns Basic bitmap information\n   *\n   * @example\n   * ```typescript\n   * const info = BitmapUtils.getBitmapInfo(bmpHex);\n   * console.log(`Bitmap: ${info.width}x${info.height}, ${info.blackPixels} black pixels`);\n   * ```\n   */\n  static getBitmapInfo(hexString: string): {\n    byteCount: number;\n    blackPixels: number;\n    width?: number;\n    height?: number;\n    isValidBmp: boolean;\n  } {\n    try {\n      const buffer = Buffer.from(hexString, \"hex\");\n      const isValidBmp =\n        buffer.length >= 14 && buffer[0] === 0x42 && buffer[1] === 0x4d;\n\n      let width: number | undefined;\n      let height: number | undefined;\n\n      if (isValidBmp && buffer.length >= 54) {\n        try {\n          width = buffer.readUInt32LE(18);\n          height = buffer.readUInt32LE(22);\n        } catch (e) {\n          // Ignore metadata parsing errors\n        }\n      }\n\n      const pixelData = buffer.slice(62);\n      const blackPixels = Array.from(pixelData).filter(\n        (b) => b !== 0xff,\n      ).length;\n\n      return {\n        byteCount: buffer.length,\n        blackPixels,\n        width,\n        height,\n        isValidBmp,\n      };\n    } catch (error) {\n      return {\n        byteCount: 0,\n        blackPixels: 0,\n        isValidBmp: false,\n      };\n    }\n  }\n}\n","/**\n * 🎬 Animation Utilities Module\n *\n * Provides helper functions for creating and managing bitmap animations in MentraOS applications.\n * Includes timing utilities, animation factories, and performance optimization helpers.\n *\n * @example\n * ```typescript\n * import { AnimationUtils } from '@mentra/sdk';\n *\n * // Create animation from files\n * const animation = await AnimationUtils.createBitmapAnimation(\n *   session, './frames', 10, 1750, true\n * );\n *\n * // Simple delay utility\n * await AnimationUtils.delay(2000);\n *\n * // Stop animation\n * animation.stop();\n * ```\n */\n\nimport { AppSession } from \"../app/session\";\nimport { BitmapUtils, LoadFramesOptions } from \"./bitmap-utils\";\n\n/**\n * Configuration options for bitmap animations\n */\nexport interface AnimationConfig {\n  /** Time between frames in milliseconds (default: 1750ms - optimized for MentraOS) */\n  intervalMs?: number;\n  /** Whether to loop the animation continuously (default: false) */\n  repeat?: boolean;\n  /** Validate frames before starting animation (default: true) */\n  validateFrames?: boolean;\n  /** Options for loading frames from files */\n  loadOptions?: LoadFramesOptions;\n  /** Callback fired when animation starts */\n  onStart?: () => void;\n  /** Callback fired when animation stops/completes */\n  onStop?: () => void;\n  /** Callback fired on each frame display */\n  onFrame?: (frameIndex: number, totalFrames: number) => void;\n  /** Callback fired if animation encounters an error */\n  onError?: (error: string) => void;\n}\n\n/**\n * Animation controller interface\n */\nexport interface AnimationController {\n  /** Stop the animation */\n  stop: () => void;\n  /** Check if animation is currently running */\n  isRunning: () => boolean;\n  /** Get current frame index */\n  getCurrentFrame: () => number;\n  /** Get total frame count */\n  getTotalFrames: () => number;\n}\n\n/**\n * Performance timing information\n */\nexport interface TimingInfo {\n  /** Target interval between frames */\n  targetInterval: number;\n  /** Actual measured interval between frames */\n  actualInterval: number;\n  /** Timing drift (difference between target and actual) */\n  drift: number;\n  /** Frame rate (frames per second) */\n  fps: number;\n}\n\n/**\n * Utility class for creating and managing animations in MentraOS applications\n */\nexport class AnimationUtils {\n  /**\n   * Simple async delay helper\n   *\n   * @param ms - Milliseconds to delay\n   * @returns Promise that resolves after the specified delay\n   *\n   * @example\n   * ```typescript\n   * console.log('Starting...');\n   * await AnimationUtils.delay(2000);\n   * console.log('2 seconds later!');\n   * ```\n   */\n  static delay(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  /**\n   * Create bitmap animation from files with advanced configuration\n   *\n   * @param session - MentraOS app session\n   * @param basePath - Directory containing animation frames\n   * @param frameCount - Number of frames to load\n   * @param config - Animation configuration options\n   * @returns Promise resolving to animation controller\n   *\n   * @example\n   * ```typescript\n   * // Simple animation\n   * const animation = await AnimationUtils.createBitmapAnimation(\n   *   session, './animations', 10\n   * );\n   *\n   * // Advanced configuration\n   * const advancedAnimation = await AnimationUtils.createBitmapAnimation(\n   *   session, './sprites', 8, {\n   *     intervalMs: 1000,\n   *     repeat: true,\n   *     loadOptions: { filePattern: 'sprite_{i}.bmp', startFrame: 0 },\n   *     onFrame: (frame, total) => console.log(`Frame ${frame}/${total}`),\n   *     onError: (error) => console.error('Animation error:', error)\n   *   }\n   * );\n   * ```\n   */\n  static async createBitmapAnimation(\n    session: AppSession,\n    basePath: string,\n    frameCount: number,\n    config: AnimationConfig = {},\n  ): Promise<AnimationController> {\n    const {\n      intervalMs = 1750, // Optimized for MentraOS hardware\n      repeat = false,\n      validateFrames = true,\n      loadOptions = {},\n      onStart,\n      onStop,\n      onFrame,\n      onError,\n    } = config;\n\n    try {\n      console.log(\n        `🎬 Loading ${frameCount} animation frames from ${basePath}...`,\n      );\n\n      // Load frames with validation\n      const frames = await BitmapUtils.loadBmpFrames(basePath, frameCount, {\n        validateFrames,\n        ...loadOptions,\n      });\n\n      if (frames.length === 0) {\n        throw new Error(\"No frames loaded for animation\");\n      }\n\n      console.log(\n        `📚 Animation ready: ${frames.length} frames at ${intervalMs}ms intervals`,\n      );\n\n      // Create enhanced animation with the loaded frames\n      return this.createBitmapAnimationFromFrames(session, frames, {\n        intervalMs,\n        repeat,\n        onStart,\n        onStop,\n        onFrame,\n        onError,\n      });\n    } catch (error) {\n      const errorMsg = `Failed to create animation: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n      console.error(`❌ ${errorMsg}`);\n      if (onError) {\n        onError(errorMsg);\n      }\n      throw new Error(errorMsg);\n    }\n  }\n\n  /**\n   * Create bitmap animation from pre-loaded frame data\n   *\n   * @param session - MentraOS app session\n   * @param frames - Array of hex-encoded bitmap data\n   * @param config - Animation configuration options\n   * @returns Animation controller\n   *\n   * @example\n   * ```typescript\n   * const frames = ['424d461a...', '424d461b...', '424d461c...'];\n   * const animation = AnimationUtils.createBitmapAnimationFromFrames(\n   *   session, frames, { intervalMs: 1500, repeat: true }\n   * );\n   * ```\n   */\n  static createBitmapAnimationFromFrames(\n    session: AppSession,\n    frames: string[],\n    config: Omit<AnimationConfig, \"loadOptions\" | \"validateFrames\"> = {},\n  ): AnimationController {\n    const {\n      intervalMs = 1750,\n      repeat = false,\n      onStart,\n      onStop,\n      onFrame,\n      onError,\n    } = config;\n\n    let isRunning = false;\n    const currentFrame = 0;\n    let animationController: { stop: () => void } | null = null;\n\n    const controller: AnimationController = {\n      stop: () => {\n        if (animationController) {\n          animationController.stop();\n          animationController = null;\n        }\n        isRunning = false;\n        if (onStop) {\n          onStop();\n        }\n        console.log(\"🛑 Animation stopped\");\n      },\n\n      isRunning: () => isRunning,\n\n      getCurrentFrame: () => currentFrame,\n\n      getTotalFrames: () => frames.length,\n    };\n\n    try {\n      // Start the animation using the session's built-in method\n      animationController = session.layouts.showBitmapAnimation(\n        frames,\n        intervalMs,\n        repeat,\n      );\n      isRunning = true;\n\n      if (onStart) {\n        onStart();\n      }\n\n      console.log(\n        `🎬 Animation started: ${frames.length} frames at ${intervalMs}ms${repeat ? \" (repeating)\" : \"\"}`,\n      );\n\n      // If we have frame callbacks, we need to track timing manually\n      // This is a limitation of the current SDK - we can't hook into individual frame displays\n      if (onFrame) {\n        let frameTracker = 0;\n\n        // Call onFrame for the first frame immediately\n        onFrame(frameTracker, frames.length);\n\n        const frameInterval = setInterval(() => {\n          if (!isRunning) {\n            clearInterval(frameInterval);\n            return;\n          }\n\n          frameTracker = (frameTracker + 1) % frames.length;\n          onFrame(frameTracker, frames.length);\n\n          // If not repeating and we've shown all frames, stop tracking\n          if (!repeat && frameTracker === frames.length - 1) {\n            clearInterval(frameInterval);\n          }\n        }, intervalMs);\n\n        // Override stop to also clear frame tracking\n        const originalStop = controller.stop;\n        controller.stop = () => {\n          clearInterval(frameInterval);\n          originalStop();\n        };\n      }\n    } catch (error) {\n      const errorMsg = `Failed to start animation: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n      console.error(`❌ ${errorMsg}`);\n      if (onError) {\n        onError(errorMsg);\n      }\n      throw new Error(errorMsg);\n    }\n\n    return controller;\n  }\n\n  /**\n   * Create a sequence of bitmap displays with custom timing\n   *\n   * @param session - MentraOS app session\n   * @param sequence - Array of frame data with individual timing\n   * @returns Promise that resolves when sequence completes\n   *\n   * @example\n   * ```typescript\n   * await AnimationUtils.createBitmapSequence(session, [\n   *   { frame: frame1Hex, duration: 1000 },\n   *   { frame: frame2Hex, duration: 500 },\n   *   { frame: frame3Hex, duration: 2000 }\n   * ]);\n   * ```\n   */\n  static async createBitmapSequence(\n    session: AppSession,\n    sequence: Array<{ frame: string; duration: number }>,\n  ): Promise<void> {\n    console.log(\n      `🎭 Starting bitmap sequence: ${sequence.length} frames with custom timing`,\n    );\n\n    for (let i = 0; i < sequence.length; i++) {\n      const { frame, duration } = sequence[i];\n\n      try {\n        console.log(\n          `📽️ Sequence frame ${i + 1}/${sequence.length} (${duration}ms)`,\n        );\n        session.layouts.showBitmapView(frame);\n\n        if (i < sequence.length - 1) {\n          // Don't delay after the last frame\n          await this.delay(duration);\n        }\n      } catch (error) {\n        console.error(`❌ Error in sequence frame ${i + 1}:`, error);\n        throw error;\n      }\n    }\n\n    console.log(\"✅ Bitmap sequence completed\");\n  }\n\n  /**\n   * Measure animation timing performance\n   *\n   * @param targetInterval - Expected interval between frames in ms\n   * @param measureDuration - How long to measure in ms (default: 10 seconds)\n   * @returns Promise resolving to timing performance data\n   *\n   * @example\n   * ```typescript\n   * const timing = await AnimationUtils.measureTiming(1750, 10000);\n   * console.log(`Target: ${timing.targetInterval}ms, Actual: ${timing.actualInterval}ms`);\n   * console.log(`Drift: ${timing.drift}ms, FPS: ${timing.fps.toFixed(1)}`);\n   * ```\n   */\n  static async measureTiming(\n    targetInterval: number,\n    measureDuration: number = 10000,\n  ): Promise<TimingInfo> {\n    return new Promise((resolve) => {\n      const timestamps: number[] = [];\n      const startTime = Date.now();\n\n      const measureInterval = setInterval(() => {\n        timestamps.push(Date.now());\n      }, targetInterval);\n\n      setTimeout(() => {\n        clearInterval(measureInterval);\n\n        if (timestamps.length < 2) {\n          resolve({\n            targetInterval,\n            actualInterval: targetInterval,\n            drift: 0,\n            fps: 1000 / targetInterval,\n          });\n          return;\n        }\n\n        // Calculate actual interval\n        const intervals = [];\n        for (let i = 1; i < timestamps.length; i++) {\n          intervals.push(timestamps[i] - timestamps[i - 1]);\n        }\n\n        const actualInterval =\n          intervals.reduce((a, b) => a + b, 0) / intervals.length;\n        const drift = actualInterval - targetInterval;\n        const fps = 1000 / actualInterval;\n\n        resolve({\n          targetInterval,\n          actualInterval,\n          drift,\n          fps,\n        });\n      }, measureDuration);\n    });\n  }\n\n  /**\n   * Create optimized animation settings for different hardware\n   *\n   * @param deviceType - Target device type\n   * @returns Recommended animation configuration\n   *\n   * @example\n   * ```typescript\n   * const config = AnimationUtils.getOptimizedConfig('even-realities-g1');\n   * const animation = await AnimationUtils.createBitmapAnimation(\n   *   session, './frames', 10, config\n   * );\n   * ```\n   */\n  static getOptimizedConfig(\n    deviceType: \"even-realities-g1\" | \"generic\",\n  ): AnimationConfig {\n    switch (deviceType) {\n      case \"even-realities-g1\":\n        return {\n          intervalMs: 1650, // Tested optimal timing for Even Realities G1\n          repeat: false,\n          validateFrames: true,\n          loadOptions: {\n            validateFrames: true,\n            skipMissingFrames: false,\n          },\n        };\n\n      case \"generic\":\n      default:\n        return {\n          intervalMs: 1000,\n          repeat: false,\n          validateFrames: true,\n          loadOptions: {\n            validateFrames: true,\n            skipMissingFrames: false,\n          },\n        };\n    }\n  }\n\n  /**\n   * Preload and cache animation frames for better performance\n   *\n   * @param basePath - Directory containing frames\n   * @param frameCount - Number of frames to preload\n   * @param options - Loading options\n   * @returns Promise resolving to cached frame data\n   *\n   * @example\n   * ```typescript\n   * // Preload frames\n   * const cachedFrames = await AnimationUtils.preloadFrames('./animations', 10);\n   *\n   * // Use cached frames multiple times\n   * const animation1 = AnimationUtils.createBitmapAnimationFromFrames(session, cachedFrames);\n   * const animation2 = AnimationUtils.createBitmapAnimationFromFrames(session, cachedFrames);\n   * ```\n   */\n  static async preloadFrames(\n    basePath: string,\n    frameCount: number,\n    options: LoadFramesOptions = {},\n  ): Promise<string[]> {\n    console.log(`📦 Preloading ${frameCount} frames from ${basePath}...`);\n\n    const frames = await BitmapUtils.loadBmpFrames(basePath, frameCount, {\n      validateFrames: true,\n      ...options,\n    });\n\n    console.log(\n      `✅ Preloaded ${frames.length} frames (${frames.reduce((total: number, frame: string) => total + frame.length, 0)} total characters)`,\n    );\n\n    return frames;\n  }\n}\n","// src/messages/cloud-to-app.ts\n\nimport {BaseMessage} from \"./base\"\nimport {CloudToAppMessageType, GlassesToCloudMessageType} from \"../message-types\"\nimport {ExtendedStreamType, StreamType} from \"../streams\"\nimport type {AppSettings, AppConfig} from \"../models\"\nimport type {DashboardMode} from \"../dashboard\"\nimport type {Capabilities} from \"../capabilities\"\nimport type {\n  LocationUpdate,\n  CalendarEvent,\n  RtmpStreamStatus,\n  PhotoResponse,\n  RgbLedControlResponse,\n} from \"./glasses-to-cloud\"\nimport type {AppSession} from \"../../app/session\"\nimport type {GlassesInfo} from \"@mentra/types\"\n\n//===========================================================\n// Responses\n//===========================================================\n\n/**\n * Connection acknowledgment to App\n */\nexport interface AppConnectionAck extends BaseMessage {\n  type: CloudToAppMessageType.CONNECTION_ACK\n  settings?: AppSettings\n  mentraosSettings?: Record<string, any> // MentraOS system settings\n  config?: AppConfig // App config sent from cloud\n  capabilities?: Capabilities // Device capability profile\n}\n\n/**\n * Connection error to App\n */\nexport interface AppConnectionError extends BaseMessage {\n  type: CloudToAppMessageType.CONNECTION_ERROR\n  message: string\n  code?: string\n}\n\n//===========================================================\n// Permission messages\n//===========================================================\n\n/**\n * Permission error detail for a specific stream\n */\nexport interface PermissionErrorDetail {\n  /** The stream type that was rejected */\n  stream: string\n  /** The permission required for this stream */\n  requiredPermission: string\n  /** Detailed message explaining the rejection */\n  message: string\n}\n\n/**\n * Permission error notification to App\n * Sent when subscriptions are rejected due to missing permissions\n */\nexport interface PermissionError extends BaseMessage {\n  type: CloudToAppMessageType.PERMISSION_ERROR\n  /** General error message */\n  message: string\n  /** Array of details for each rejected stream */\n  details: PermissionErrorDetail[]\n}\n\n//===========================================================\n// Updates\n//===========================================================\n\n/**\n * App stopped notification to App\n */\nexport interface AppStopped extends BaseMessage {\n  type: CloudToAppMessageType.APP_STOPPED\n  reason: \"user_disabled\" | \"system_stop\" | \"error\"\n  message?: string\n}\n\n/**\n * Settings update to App\n */\nexport interface SettingsUpdate extends BaseMessage {\n  type: CloudToAppMessageType.SETTINGS_UPDATE\n  packageName: string\n  settings: AppSettings\n}\n\n/**\n * Device capabilities update to App\n * Sent when the connected glasses model changes or capabilities are updated\n */\nexport interface CapabilitiesUpdate extends BaseMessage {\n  type: CloudToAppMessageType.CAPABILITIES_UPDATE\n  capabilities: Capabilities | null\n  modelName: string | null\n}\n\n/**\n * Device state update to App\n * Sent when any device state changes (WiFi, battery, hotspot, connection, etc.)\n * Apps receive this automatically - no subscription needed\n */\nexport interface DeviceStateUpdate extends BaseMessage {\n  type: CloudToAppMessageType.DEVICE_STATE_UPDATE\n  state: Partial<GlassesInfo> // Only changed fields (or full snapshot)\n  fullSnapshot?: boolean // True on initial connection or reconnection\n  timestamp: Date\n}\n\n/**\n * MentraOS settings update to App\n */\nexport interface MentraosSettingsUpdate extends BaseMessage {\n  type: \"augmentos_settings_update\"\n  sessionId: string\n  settings: Record<string, any>\n  timestamp: Date\n}\n\n//===========================================================\n// Audio-related data types\n//===========================================================\n/**\n * Transcription data\n */\nexport interface TranscriptionData extends BaseMessage {\n  type: StreamType.TRANSCRIPTION\n  text: string // The transcribed text\n  isFinal: boolean // Whether this is a final transcription\n  utteranceId?: string // Unique ID for this speech segment - interim and final for same utterance share the same ID\n  transcribeLanguage?: string // Subscription language code (used for routing, e.g., \"en-US\")\n  detectedLanguage?: string // Actual detected language from speech recognition (e.g., \"ja\", \"en\")\n  startTime: number // Start time in milliseconds\n  endTime: number // End time in milliseconds\n  speakerId?: string // ID of the speaker if available (from diarization)\n  duration?: number // Audio duration in milliseconds\n  provider?: string // The transcription provider (e.g., \"azure\", \"soniox\")\n  confidence?: number // Confidence score (0-1)\n  metadata?: TranscriptionMetadata // Token-level metadata (always included)\n}\n\n/**\n * Metadata for transcription containing token-level details\n */\nexport interface TranscriptionMetadata {\n  provider: \"soniox\" | \"azure\" | string\n  soniox?: {\n    tokens: SonioxToken[]\n  }\n  azure?: {\n    // Azure-specific metadata can be added later\n    tokens?: any[]\n  }\n  alibaba?: {\n    // Alibaba-specific metadata can be added later\n    tokens?: any[]\n  }\n}\n\n/**\n * Soniox token with word-level details\n */\nexport interface SonioxToken {\n  text: string\n  startMs?: number\n  endMs?: number\n  confidence: number\n  isFinal: boolean\n  speaker?: string\n}\n\n/**\n * Translation data\n */\nexport interface TranslationData extends BaseMessage {\n  type: StreamType.TRANSLATION\n  text: string // The transcribed text\n  originalText?: string // The original transcribed text before translation\n  isFinal: boolean // Whether this is a final transcription\n  startTime: number // Start time in milliseconds\n  endTime: number // End time in milliseconds\n  speakerId?: string // ID of the speaker if available\n  duration?: number // Audio duration in milliseconds\n  transcribeLanguage?: string // The language code of the transcribed text\n  translateLanguage?: string // The language code of the translated text\n  didTranslate?: boolean // Whether the text was translated\n  provider?: string // The translation provider (e.g., \"azure\", \"google\")\n  confidence?: number // Confidence score (0-1)\n}\n\n/**\n * Audio chunk data\n */\nexport interface AudioChunk extends BaseMessage {\n  type: StreamType.AUDIO_CHUNK\n  arrayBuffer: ArrayBufferLike // The audio data\n  sampleRate?: number // Audio sample rate (e.g., 16000 Hz)\n}\n\n/**\n * Tool call from cloud to App\n * Represents a tool invocation with filled parameters\n */\nexport interface ToolCall {\n  toolId: string // The ID of the tool that was called\n  toolParameters: Record<string, string | number | boolean> // The parameters of the tool that was called\n  timestamp: Date // Timestamp when the tool was called\n  userId: string // ID of the user who triggered the tool call\n  activeSession: AppSession | null\n}\n\n//===========================================================\n// Stream data\n//===========================================================\n\n/**\n * Stream data to App\n */\nexport interface DataStream extends BaseMessage {\n  type: CloudToAppMessageType.DATA_STREAM\n  streamType: ExtendedStreamType\n  data: unknown // Type depends on the streamType\n}\n\n//===========================================================\n// Dashboard messages\n//===========================================================\n\n/**\n * Dashboard mode changed notification\n */\nexport interface DashboardModeChanged extends BaseMessage {\n  type: CloudToAppMessageType.DASHBOARD_MODE_CHANGED\n  mode: DashboardMode\n}\n\n/**\n * Dashboard always-on state changed notification\n */\nexport interface DashboardAlwaysOnChanged extends BaseMessage {\n  type: CloudToAppMessageType.DASHBOARD_ALWAYS_ON_CHANGED\n  enabled: boolean\n}\n\n/**\n * Standard connection error (for server compatibility)\n */\nexport interface StandardConnectionError extends BaseMessage {\n  type: \"connection_error\"\n  message: string\n}\n\n/**\n * Custom message for general-purpose communication (cloud to App)\n */\nexport interface CustomMessage extends BaseMessage {\n  type: CloudToAppMessageType.CUSTOM_MESSAGE\n  action: string // Identifies the specific action/message type\n  payload: any // Custom data payload\n}\n\n/**\n * Output status for a re-stream destination\n */\nexport interface OutputStatus {\n  /** The destination URL */\n  url: string\n  /** Friendly name if provided */\n  name?: string\n  /** Status of this output */\n  status: \"active\" | \"error\" | \"stopped\"\n  /** Error message if status is error */\n  error?: string\n}\n\n/**\n * Managed RTMP stream status update\n * Sent when managed stream status changes or URLs are ready\n */\nexport interface ManagedStreamStatus extends BaseMessage {\n  type: CloudToAppMessageType.MANAGED_STREAM_STATUS\n  status: \"initializing\" | \"preparing\" | \"active\" | \"stopping\" | \"stopped\" | \"error\"\n  hlsUrl?: string\n  dashUrl?: string\n  webrtcUrl?: string\n  /** Cloudflare Stream player/preview URL for embedding */\n  previewUrl?: string\n  /** Thumbnail image URL */\n  thumbnailUrl?: string\n  message?: string\n  streamId?: string\n  /** Status of re-stream outputs if configured */\n  outputs?: OutputStatus[]\n}\n\n/**\n * Stream status check response\n * Returns information about any existing streams for the user\n */\nexport interface StreamStatusCheckResponse extends BaseMessage {\n  type: CloudToAppMessageType.STREAM_STATUS_CHECK_RESPONSE\n  hasActiveStream: boolean\n  streamInfo?: {\n    type: \"managed\" | \"unmanaged\"\n    streamId: string\n    status: string\n    createdAt: Date\n    // For managed streams\n    hlsUrl?: string\n    dashUrl?: string\n    webrtcUrl?: string\n    previewUrl?: string\n    thumbnailUrl?: string\n    activeViewers?: number\n    // For unmanaged streams\n    rtmpUrl?: string\n    requestingAppId?: string\n  }\n}\n\n/**\n * Audio play response to App\n */\nexport interface AudioPlayResponse extends BaseMessage {\n  type: CloudToAppMessageType.AUDIO_PLAY_RESPONSE\n  requestId: string\n  success: boolean\n  error?: string // Error message (if failed)\n  duration?: number // Duration of audio in milliseconds (if successful)\n}\n\n/**\n * Union type for all messages from cloud to Apps\n */\nexport type CloudToAppMessage =\n  | AppConnectionAck\n  | AppConnectionError\n  | StandardConnectionError\n  | DataStream\n  | AppStopped\n  | SettingsUpdate\n  | CapabilitiesUpdate\n  | DeviceStateUpdate\n  | TranscriptionData\n  | TranslationData\n  | AudioChunk\n  | LocationUpdate\n  | CalendarEvent\n  | PhotoResponse\n  | DashboardModeChanged\n  | DashboardAlwaysOnChanged\n  | CustomMessage\n  | ManagedStreamStatus\n  | StreamStatusCheckResponse\n  | MentraosSettingsUpdate\n  // New App-to-App communication response messages\n  | AppMessageReceived\n  | AppUserJoined\n  | AppUserLeft\n  | AppRoomUpdated\n  | AppDirectMessageResponse\n  | RtmpStreamStatus\n  | PhotoResponse\n  | RgbLedControlResponse\n  | PermissionError\n  | AudioPlayResponse\n\n//===========================================================\n// Type guards\n//===========================================================\n\nexport function isAppConnectionAck(message: CloudToAppMessage): message is AppConnectionAck {\n  return message.type === CloudToAppMessageType.CONNECTION_ACK\n}\n\nexport function isAppConnectionError(message: CloudToAppMessage): message is AppConnectionError {\n  return message.type === CloudToAppMessageType.CONNECTION_ERROR || (message as any).type === \"connection_error\"\n}\n\nexport function isAppStopped(message: CloudToAppMessage): message is AppStopped {\n  return message.type === CloudToAppMessageType.APP_STOPPED\n}\n\nexport function isSettingsUpdate(message: CloudToAppMessage): message is SettingsUpdate {\n  return message.type === CloudToAppMessageType.SETTINGS_UPDATE\n}\n\nexport function isCapabilitiesUpdate(message: CloudToAppMessage): message is CapabilitiesUpdate {\n  return message.type === CloudToAppMessageType.CAPABILITIES_UPDATE\n}\n\nexport function isDeviceStateUpdate(message: CloudToAppMessage): message is DeviceStateUpdate {\n  return message.type === CloudToAppMessageType.DEVICE_STATE_UPDATE\n}\n\nexport function isDataStream(message: CloudToAppMessage): message is DataStream {\n  return message.type === CloudToAppMessageType.DATA_STREAM\n}\n\nexport function isAudioChunk(message: CloudToAppMessage): message is AudioChunk {\n  return message.type === StreamType.AUDIO_CHUNK\n}\n\nexport function isDashboardModeChanged(message: CloudToAppMessage): message is DashboardModeChanged {\n  return message.type === CloudToAppMessageType.DASHBOARD_MODE_CHANGED\n}\n\nexport function isDashboardAlwaysOnChanged(message: CloudToAppMessage): message is DashboardAlwaysOnChanged {\n  return message.type === CloudToAppMessageType.DASHBOARD_ALWAYS_ON_CHANGED\n}\n\nexport function isManagedStreamStatus(message: CloudToAppMessage): message is ManagedStreamStatus {\n  return message.type === CloudToAppMessageType.MANAGED_STREAM_STATUS\n}\n\nexport function isRtmpStreamStatus(message: CloudToAppMessage): message is RtmpStreamStatus {\n  return message.type === GlassesToCloudMessageType.RTMP_STREAM_STATUS\n}\n\nexport function isPhotoResponse(message: CloudToAppMessage): message is PhotoResponse {\n  return message.type === GlassesToCloudMessageType.PHOTO_RESPONSE\n}\n\nexport function isRgbLedControlResponse(message: CloudToAppMessage): message is RgbLedControlResponse {\n  return message.type === GlassesToCloudMessageType.RGB_LED_CONTROL_RESPONSE\n}\n\nexport function isStreamStatusCheckResponse(message: CloudToAppMessage): message is StreamStatusCheckResponse {\n  return message.type === CloudToAppMessageType.STREAM_STATUS_CHECK_RESPONSE\n}\n\nexport function isAudioPlayResponse(message: CloudToAppMessage): message is AudioPlayResponse {\n  return message.type === CloudToAppMessageType.AUDIO_PLAY_RESPONSE\n}\n\n// New type guards for App-to-App communication\nexport function isAppMessageReceived(message: CloudToAppMessage): message is AppMessageReceived {\n  return message.type === CloudToAppMessageType.APP_MESSAGE_RECEIVED\n}\n\nexport function isAppUserJoined(message: CloudToAppMessage): message is AppUserJoined {\n  return message.type === CloudToAppMessageType.APP_USER_JOINED\n}\n\nexport function isAppUserLeft(message: CloudToAppMessage): message is AppUserLeft {\n  return message.type === CloudToAppMessageType.APP_USER_LEFT\n}\n\n//===========================================================\n// App-to-App Communication Response Messages\n//===========================================================\n\n/**\n * Message received from another App user\n */\nexport interface AppMessageReceived extends BaseMessage {\n  type: CloudToAppMessageType.APP_MESSAGE_RECEIVED\n  payload: any\n  messageId: string\n  senderUserId: string\n  senderSessionId: string\n  roomId?: string\n}\n\n/**\n * Notification that a user joined the App\n */\nexport interface AppUserJoined extends BaseMessage {\n  type: CloudToAppMessageType.APP_USER_JOINED\n  userId: string\n  sessionId: string\n  joinedAt: Date\n  userProfile?: any\n  roomId?: string\n}\n\n/**\n * Notification that a user left the App\n */\nexport interface AppUserLeft extends BaseMessage {\n  type: CloudToAppMessageType.APP_USER_LEFT\n  userId: string\n  sessionId: string\n  leftAt: Date\n  roomId?: string\n}\n\n/**\n * Room status update (members, config changes, etc.)\n */\nexport interface AppRoomUpdated extends BaseMessage {\n  type: CloudToAppMessageType.APP_ROOM_UPDATED\n  roomId: string\n  updateType: \"user_joined\" | \"user_left\" | \"config_changed\" | \"room_closed\"\n  roomData: {\n    memberCount: number\n    maxUsers?: number\n    isPrivate?: boolean\n    metadata?: any\n  }\n}\n\n/**\n * Response to a direct message attempt\n */\nexport interface AppDirectMessageResponse extends BaseMessage {\n  type: CloudToAppMessageType.APP_DIRECT_MESSAGE_RESPONSE\n  messageId: string\n  success: boolean\n  error?: string\n  targetUserId: string\n}\n\n//===========================================================\n// Cloud-to-Sdk Communication Response Messages\n//===========================================================\n\n/**\n * Permission data structures for permission fetch responses\n */\nexport interface Permission {\n  type: string // or a union/enum if you want stricter typing\n  description: string\n  _id: string\n}\n\n/**\n * Package permissions response structure\n */\nexport interface PackagePermissions {\n  packageName: string\n  permissions: Permission[]\n}\n","// src/webhook.ts\n\n/**\n * Types of webhook requests that can be sent to Apps\n */\nexport enum WebhookRequestType {\n  /** Request to start a App session */\n  SESSION_REQUEST = \"session_request\",\n\n  /** Request to stop a App session */\n  STOP_REQUEST = \"stop_request\",\n}\n\n/**\n * Base interface for all webhook requests\n */\nexport interface BaseWebhookRequest {\n  /** Type of webhook request */\n  type: WebhookRequestType;\n\n  /** Session ID for the request */\n  sessionId: string;\n\n  /** User ID associated with the session */\n  userId: string;\n\n  /** Timestamp of the request */\n  timestamp: string;\n}\n\n/**\n * Session request webhook\n *\n * Sent to a App when a user starts the App\n */\nexport interface SessionWebhookRequest extends BaseWebhookRequest {\n  type: WebhookRequestType.SESSION_REQUEST;\n  mentraOSWebsocketUrl?: string;\n  augmentOSWebsocketUrl?: string;\n}\n\n/**\n * Stop request webhook\n *\n * Sent to a App when a user or the system stops the App\n */\nexport interface StopWebhookRequest extends BaseWebhookRequest {\n  type: WebhookRequestType.STOP_REQUEST;\n  reason: \"user_disabled\" | \"system_stop\" | \"error\";\n}\n\n/**\n * Union type for all webhook requests\n */\nexport type WebhookRequest = SessionWebhookRequest | StopWebhookRequest;\n\n/**\n * Response to a webhook request\n */\nexport interface WebhookResponse {\n  status: \"success\" | \"error\";\n  message?: string;\n}\n\n/**\n * Type guard to check if a webhook request is a session request\n */\nexport function isSessionWebhookRequest(\n  request: WebhookRequest,\n): request is SessionWebhookRequest {\n  return request.type === WebhookRequestType.SESSION_REQUEST;\n}\n\n/**\n * Type guard to check if a webhook request is a stop request\n */\nexport function isStopWebhookRequest(\n  request: WebhookRequest,\n): request is StopWebhookRequest {\n  return request.type === WebhookRequestType.STOP_REQUEST;\n}\n","/**\n * 🚀 App Server Module\n *\n * Creates and manages a server for Apps in the MentraOS ecosystem.\n * Handles webhook endpoints, session management, and cleanup.\n */\nimport express, { type Express } from \"express\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport { AppSession } from \"../session/index\";\nimport { createAuthMiddleware } from \"../webview\";\nimport { newSDKUpdate } from \"../../constants/log-messages/updates\";\n\nimport {\n  WebhookRequest,\n  WebhookResponse,\n  SessionWebhookRequest,\n  StopWebhookRequest,\n  ToolCall,\n  WebhookRequestType,\n} from \"../../types\";\n\nimport { Logger } from \"pino\";\nimport { logger as rootLogger } from \"../../logging/logger\";\nimport axios from \"axios\";\nimport { PhotoData } from \"../../types/photo-data\";\n\nexport const GIVE_APP_CONTROL_OF_TOOL_RESPONSE: string = \"GIVE_APP_CONTROL_OF_TOOL_RESPONSE\";\n\n/**\n * Pending photo request stored at AppServer level\n * This allows O(1) lookup when photo uploads arrive via HTTP,\n * and survives session reconnections.\n * See: cloud/issues/019-sdk-photo-request-architecture\n */\nexport interface PendingPhotoRequest {\n  userId: string;\n  sessionId: string;\n  session: AppSession;\n  resolve: (photo: PhotoData) => void;\n  reject: (error: Error) => void;\n  timestamp: number;\n  timeoutId?: NodeJS.Timeout;\n}\n\n/**\n * 🔧 Configuration options for App Server\n *\n * @example\n * ```typescript\n * const config: AppServerConfig = {\n *   packageName: 'org.example.myapp',\n *   apiKey: 'your_api_key',\n *   port: 7010,\n *   publicDir: './public'\n * };\n * ```\n */\nexport interface AppServerConfig {\n  /** 📦 Unique identifier for your App (e.g., 'org.company.appname') must match what you specified at https://console.mentra.glass */\n  packageName: string;\n  /** 🔑 API key for authentication with MentraOS Cloud */\n  apiKey: string;\n  /** 🌐 Port number for the server (default: 7010) */\n  port?: number;\n\n  /** Cloud API URL (default: 'api.mentra.glass') */\n  cloudApiUrl?: string;\n\n  /** 🛣️ [DEPRECATED] do not set: The SDK will automatically expose an endpoint at '/webhook' */\n  webhookPath?: string;\n  /**\n   * 📂 Directory for serving static files (e.g., images, logos)\n   * Set to false to disable static file serving\n   */\n  publicDir?: string | false;\n\n  /** ❤️ Enable health check endpoint at /health (default: true) */\n  healthCheck?: boolean;\n  /**\n   * 🔐 Secret key used to sign session cookies\n   * This must be a strong, unique secret\n   */\n  cookieSecret?: string;\n  /** App instructions string shown to the user */\n  appInstructions?: string;\n}\n\n/**\n * 🎯 App Server Implementation\n *\n * Base class for creating App servers. Handles:\n * - 🔄 Session lifecycle management\n * - 📡 Webhook endpoints for MentraOS Cloud\n * - 📂 Static file serving\n * - ❤️ Health checks\n * - 🧹 Cleanup on shutdown\n *\n * @example\n * ```typescript\n * class MyAppServer extends AppServer {\n *   protected async onSession(session: AppSession, sessionId: string, userId: string) {\n *     // Handle new user sessions here\n *     session.events.onTranscription((data) => {\n *       session.layouts.showTextWall(data.text);\n *     });\n *   }\n * }\n *\n * const server = new MyAppServer({\n *   packageName: 'org.example.myapp',\n *   apiKey: 'your_api_key',\n *   publicDir: \"/public\",\n * });\n *\n * await server.start();\n * ```\n */\nexport class AppServer {\n  /** Express app instance */\n  private app: Express;\n  /** Map of active user sessions by sessionId */\n  private activeSessions = new Map<string, AppSession>();\n  /** Map of active user sessions by userId */\n  private activeSessionsByUserId = new Map<string, AppSession>();\n  /** Array of cleanup handlers to run on shutdown */\n  private cleanupHandlers: Array<() => void> = [];\n  /** App instructions string shown to the user */\n  private appInstructions: string | null = null;\n  /**\n   * Pending photo requests by requestId - owned by AppServer for HTTP endpoint access.\n   * This is the single source of truth for pending photo requests.\n   * Stored here (not on CameraModule) because:\n   * 1. Photo uploads arrive via HTTP to AppServer, not via WebSocket to session\n   * 2. Allows O(1) lookup by requestId instead of iterating all sessions\n   * 3. Survives session reconnections (session may be removed from activeSessions temporarily)\n   * See: cloud/issues/019-sdk-photo-request-architecture\n   */\n  private pendingPhotoRequests = new Map<string, PendingPhotoRequest>();\n\n  public readonly logger: Logger;\n\n  constructor(private config: AppServerConfig) {\n    // Set defaults and merge with provided config\n    this.config = {\n      port: 7010,\n      webhookPath: \"/webhook\",\n      publicDir: false,\n      healthCheck: true,\n      ...config,\n    };\n\n    this.logger = rootLogger.child({\n      app: this.config.packageName,\n      packageName: this.config.packageName,\n      service: \"app-server\",\n    });\n\n    // Initialize Express app\n    this.app = express();\n    this.app.use(express.json());\n\n    const cookieParser = require(\"cookie-parser\");\n    this.app.use(\n      cookieParser(this.config.cookieSecret || `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`),\n    );\n\n    // Apply authentication middleware\n    this.app.use(\n      createAuthMiddleware({\n        apiKey: this.config.apiKey,\n        packageName: this.config.packageName,\n        getAppSessionForUser: (userId: string) => {\n          return this.activeSessionsByUserId.get(userId) || null;\n        },\n        cookieSecret:\n          this.config.cookieSecret || `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`,\n      }) as any,\n    );\n\n    this.appInstructions = (config as any).appInstructions || null;\n\n    // Setup server features\n    this.setupWebhook();\n    this.setupSettingsEndpoint();\n    this.setupHealthCheck();\n    this.setupToolCallEndpoint();\n    this.setupPhotoUploadEndpoint();\n    this.setupMentraAuthRedirect();\n    this.setupPublicDir();\n    this.setupShutdown();\n  }\n\n  // Expose Express app for custom routes.\n  // This is useful for adding custom API routes or middleware.\n  public getExpressApp(): Express {\n    return this.app;\n  }\n\n  /**\n   * 👥 Session Handler\n   * Override this method to handle new App sessions.\n   * This is where you implement your app's core functionality.\n   *\n   * @param session - App session instance for the user\n   * @param sessionId - Unique identifier for this session\n   * @param userId - User's identifier\n   */\n  protected async onSession(session: AppSession, sessionId: string, userId: string): Promise<void> {\n    this.logger.info(`🚀 Starting new session handling for session ${sessionId} and user ${userId}`);\n    // Core session handling logic (onboarding removed)\n    this.logger.info(`✅ Session handling completed for session ${sessionId} and user ${userId}`);\n  }\n\n  /**\n   * 👥 Stop Handler\n   * Override this method to handle stop requests.\n   * This is where you can clean up resources when a session is stopped.\n   *\n   * @param sessionId - Unique identifier for this session\n   * @param userId - User's identifier\n   * @param reason - Reason for stopping\n   */\n  protected async onStop(sessionId: string, userId: string, reason: string): Promise<void> {\n    this.logger.debug(`Session ${sessionId} stopped for user ${userId}. Reason: ${reason}`);\n\n    // Default implementation: close the session if it exists\n    const session = this.activeSessions.get(sessionId);\n    if (session) {\n      session.disconnect();\n      this.activeSessions.delete(sessionId);\n      this.activeSessionsByUserId.delete(userId);\n    }\n  }\n\n  /**\n   * 🛠️ Tool Call Handler\n   * Override this method to handle tool calls from MentraOS Cloud.\n   * This is where you implement your app's tool functionality.\n   *\n   * @param toolCall - The tool call request containing tool details and parameters\n   * @returns Optional string response that will be sent back to MentraOS Cloud\n   */\n  protected async onToolCall(toolCall: ToolCall): Promise<string | undefined> {\n    this.logger.debug(`Tool call received: ${toolCall.toolId}`);\n    this.logger.debug(`Parameters: ${JSON.stringify(toolCall.toolParameters)}`);\n    return undefined;\n  }\n\n  /**\n   * 🚀 Start the Server\n   * Starts listening for incoming connections and webhook calls.\n   *\n   * @returns Promise that resolves when server is ready\n   */\n  public start(): Promise<void> {\n    return new Promise((resolve) => {\n      this.app.listen(this.config.port, async () => {\n        this.logger.info(`🎯 App server running at http://localhost:${this.config.port}`);\n        if (this.config.publicDir) {\n          this.logger.info(`📂 Serving static files from ${this.config.publicDir}`);\n        }\n\n        // 🔑 Grab SDK version\n        try {\n          // Look for the actual installed @mentra/sdk package.json in node_modules\n          const sdkPkgPath = path.resolve(process.cwd(), \"node_modules/@mentra/sdk/package.json\");\n\n          let currentVersion = \"unknown\";\n\n          if (fs.existsSync(sdkPkgPath)) {\n            const sdkPkg = JSON.parse(fs.readFileSync(sdkPkgPath, \"utf-8\"));\n\n            // Get the actual installed version\n            currentVersion = sdkPkg.version || \"not-found\"; // located in the node module\n          } else {\n            this.logger.debug({ sdkPkgPath }, \"No @mentra/sdk package.json found at path\");\n          }\n\n          // this.logger.debug(`Developer is using SDK version: ${currentVersion}`);\n\n          // Fetch latest SDK version from the API endpoint\n          let latest: string | null = null;\n          try {\n            const cloudHost = \"api.mentra.glass\";\n            const response = await axios.get(\n              `https://${cloudHost}/api/sdk/version`,\n              { timeout: 3000 }, // 3 second timeout\n            );\n            if (response.data && response.data.success && response.data.data) {\n              latest = response.data.data.latest;\n            }\n          } catch {\n            this.logger.debug(\n              \"Failed to fetch latest SDK version - skipping version check (offline or API unavailable)\",\n            );\n          }\n\n          if (currentVersion === \"not-found\") {\n            this.logger.warn(\n              `⚠️ @mentra/sdk not found in your project dependencies. Please install it with: npm install @mentra/sdk`,\n            );\n          } else if (latest && latest !== currentVersion) {\n            this.logger.warn(newSDKUpdate(latest));\n          }\n        } catch (err) {\n          this.logger.error(err, \"Version check failed\");\n        }\n\n        resolve();\n      });\n    });\n  }\n\n  /**\n   * 🛑 Stop the Server\n   * Gracefully shuts down the server and cleans up all sessions.\n   */\n  public async stop(): Promise<void> {\n    this.logger.info(\"\\n🛑 Shutting down...\");\n    await this.cleanup();\n    process.exit(0);\n  }\n\n  /**\n   * 🔐 Generate a App token for a user\n   * This should be called when handling a session webhook request.\n   *\n   * @param userId - User identifier\n   * @param sessionId - Session identifier\n   * @param secretKey - Secret key for signing the token\n   * @returns JWT token string\n   */\n  protected generateToken(userId: string, sessionId: string, secretKey: string): string {\n    const { createToken } = require(\"../token/utils\");\n    return createToken(\n      {\n        userId,\n        packageName: this.config.packageName,\n        sessionId,\n      },\n      { secretKey },\n    );\n  }\n\n  /**\n   * 🧹 Add Cleanup Handler\n   * Register a function to be called during server shutdown.\n   *\n   * @param handler - Function to call during cleanup\n   */\n  protected addCleanupHandler(handler: () => void): void {\n    this.cleanupHandlers.push(handler);\n  }\n\n  /**\n   * 🎯 Setup Webhook Endpoint\n   * Creates the webhook endpoint that MentraOS Cloud calls to start new sessions.\n   */\n  private setupWebhook(): void {\n    if (!this.config.webhookPath) {\n      this.logger.error(\"❌ Webhook path not set\");\n      throw new Error(\"Webhook path not set\");\n    }\n\n    this.app.post(this.config.webhookPath, async (req, res) => {\n      try {\n        const webhookRequest = req.body as WebhookRequest;\n\n        // Handle session request\n        if (webhookRequest.type === WebhookRequestType.SESSION_REQUEST) {\n          await this.handleSessionRequest(webhookRequest, res);\n        }\n        // Handle stop request\n        else if (webhookRequest.type === WebhookRequestType.STOP_REQUEST) {\n          await this.handleStopRequest(webhookRequest, res);\n        }\n        // Unknown webhook type\n        else {\n          this.logger.error(\"❌ Unknown webhook request type\");\n          res.status(400).json({\n            status: \"error\",\n            message: \"Unknown webhook request type\",\n          } as WebhookResponse);\n        }\n      } catch (error) {\n        this.logger.error(error, \"❌ Error handling webhook: \" + (error as Error).message);\n        res.status(500).json({\n          status: \"error\",\n          message: \"Error handling webhook: \" + (error as Error).message,\n        } as WebhookResponse);\n      }\n    });\n  }\n\n  /**\n   * 🛠️ Setup Tool Call Endpoint\n   * Creates a /tool endpoint for handling tool calls from MentraOS Cloud.\n   */\n  private setupToolCallEndpoint(): void {\n    this.app.post(\"/tool\", async (req, res) => {\n      try {\n        const toolCall = req.body as ToolCall;\n        if (this.activeSessionsByUserId.has(toolCall.userId)) {\n          toolCall.activeSession = this.activeSessionsByUserId.get(toolCall.userId) || null;\n        } else {\n          toolCall.activeSession = null;\n        }\n        this.logger.info({ body: req.body }, `🔧 Received tool call: ${toolCall.toolId}`);\n        // Call the onToolCall handler and get the response\n        const response = await this.onToolCall(toolCall);\n\n        // Send back the response if one was provided\n        if (response !== undefined) {\n          res.json({ status: \"success\", reply: response });\n        } else {\n          res.json({ status: \"success\", reply: null });\n        }\n      } catch (error) {\n        this.logger.error(error, \"❌ Error handling tool call:\");\n        res.status(500).json({\n          status: \"error\",\n          message: error instanceof Error ? error.message : \"Unknown error occurred calling tool\",\n        });\n      }\n    });\n    this.app.get(\"/tool\", async (req, res) => {\n      res.json({ status: \"success\", reply: \"Hello, world!\" });\n    });\n  }\n\n  /**\n   * Handle a session request webhook\n   */\n  private async handleSessionRequest(request: SessionWebhookRequest, res: express.Response): Promise<void> {\n    const { sessionId, userId, mentraOSWebsocketUrl, augmentOSWebsocketUrl } = request;\n    this.logger.info({ userId }, `🗣️ Received session request for user ${userId}, session ${sessionId}\\n\\n`);\n\n    // Check for existing session (user might be switching clouds)\n    // If an existing session exists, we need to clean it up properly to avoid:\n    // 1. Orphaned sessions with open WebSockets\n    // 2. Cleanup handlers that corrupt the new session's map entries\n    // See: cloud/issues/018-app-disconnect-resurrection\n    const existingSession = this.activeSessions.get(sessionId);\n    if (existingSession) {\n      this.logger.info(\n        { sessionId, userId },\n        `🔄 Existing session found for ${sessionId} - sending OWNERSHIP_RELEASE and disconnecting before new connection`,\n      );\n\n      try {\n        // Send OWNERSHIP_RELEASE to tell the old cloud not to resurrect this app\n        // The old cloud will mark the app as DORMANT instead of trying to restart it\n        await existingSession.releaseOwnership(\"switching_clouds\");\n      } catch (error) {\n        this.logger.warn(\n          { error, sessionId },\n          `⚠️ Failed to send OWNERSHIP_RELEASE to old session - continuing anyway`,\n        );\n      }\n\n      try {\n        // Disconnect the old session explicitly\n        existingSession.disconnect();\n      } catch (error) {\n        this.logger.warn({ error, sessionId }, `⚠️ Failed to disconnect old session - continuing anyway`);\n      }\n\n      // Remove from maps immediately (don't wait for cleanup handler)\n      this.activeSessions.delete(sessionId);\n      this.activeSessionsByUserId.delete(userId);\n\n      this.logger.info({ sessionId, userId }, `✅ Old session cleaned up, proceeding with new connection`);\n    }\n\n    // Create new App session\n    const session = new AppSession({\n      packageName: this.config.packageName,\n      apiKey: this.config.apiKey,\n      mentraOSWebsocketUrl: mentraOSWebsocketUrl || augmentOSWebsocketUrl, // The websocket URL for the specific MentraOS server that this userSession is connecting to.\n      appServer: this,\n      userId,\n    });\n\n    // Setup session event handlers\n    const cleanupDisconnect = session.events.onDisconnected((info) => {\n      // Determine if this is a permanent disconnect\n      // Permanent disconnects happen when:\n      // 1. User session ends (sessionEnded === true)\n      // 2. Reconnection attempts exhausted (permanent === true)\n      // 3. Clean WebSocket closure (1000/1001) - no reconnection will be attempted\n      // Temporary disconnects (abnormal closures like 1006 that trigger reconnection) should NOT remove session from maps\n      // See: cloud/issues/019-sdk-photo-request-architecture\n      let isPermanent = false;\n      let reason = \"unknown\";\n\n      // Handle different disconnect info formats (string or object)\n      if (typeof info === \"string\") {\n        this.logger.info(`👋 Session ${sessionId} disconnected: ${info}`);\n        reason = info;\n        // String-only disconnects are typically temporary (e.g., \"WebSocket closed\")\n        isPermanent = false;\n      } else {\n        // It's an object with detailed disconnect information\n        this.logger.info(\n          `👋 Session ${sessionId} disconnected: ${info.message} (code: ${info.code}, reason: ${info.reason})`,\n        );\n        reason = info.reason || info.message;\n\n        // Check if this is a user session end event\n        // This happens when the UserSession is disposed after 1 minute grace period\n        if (info.sessionEnded === true) {\n          this.logger.info(`🛑 User session ended for session ${sessionId}, calling onStop`);\n          isPermanent = true;\n\n          // Call onStop with session end reason\n          // This allows apps to clean up resources when the user's session ends\n          this.onStop(sessionId, userId, \"User session ended\").catch((error) => {\n            this.logger.error(error, `❌ Error in onStop handler for session end:`);\n          });\n        }\n        // Check if this is a permanent disconnection after exhausted reconnection attempts\n        else if (info.permanent === true) {\n          this.logger.info(`🛑 Permanent disconnection detected for session ${sessionId}, calling onStop`);\n          isPermanent = true;\n\n          // Call onStop with a reconnection failure reason\n          this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch((error) => {\n            this.logger.error(error, `❌ Error in onStop handler for permanent disconnection:`);\n          });\n        }\n        // Check if this is a clean WebSocket closure (1000/1001) that won't trigger reconnection\n        // These are intentional disconnects (app shutdown, manual stop, etc.)\n        // AppSession skips reconnection for these codes, so we must treat them as permanent\n        // to avoid zombie sessions in activeSessions map\n        else if (info.wasClean === true || info.code === 1000 || info.code === 1001) {\n          this.logger.info(\n            `🛑 Clean WebSocket closure for session ${sessionId} (code: ${info.code}), treating as permanent`,\n          );\n          isPermanent = true;\n\n          // Call onStop for clean disconnects too\n          this.onStop(sessionId, userId, `Clean disconnect: ${reason}`).catch((error) => {\n            this.logger.error(error, `❌ Error in onStop handler for clean disconnect:`);\n          });\n        }\n      }\n\n      // Only remove session and clean up photo requests on PERMANENT disconnects\n      // Temporary disconnects should leave the session in place so:\n      // 1. Photo uploads can still find the pending request\n      // 2. The session can be reused after reconnection\n      // See: cloud/issues/019-sdk-photo-request-architecture\n      if (isPermanent) {\n        // Remove the session from active sessions ONLY if this session is still the active one.\n        // This prevents a bug where an old session's cleanup handler deletes a newer session:\n        // 1. User switches from Cloud A to Cloud B\n        // 2. SDK creates sessionB, overwrites activeSessions[sessionId]\n        // 3. sessionA is orphaned but its cleanup handler still references sessionId\n        // 4. When Cloud A disposes, sessionA's cleanup fires and would delete sessionB\n        // By checking identity (===), we only delete if we're still the current session.\n        // See: cloud/issues/018-app-disconnect-resurrection\n        if (this.activeSessions.get(sessionId) === session) {\n          this.activeSessions.delete(sessionId);\n        } else {\n          this.logger.debug({ sessionId }, `🔄 Session ${sessionId} cleanup skipped - a newer session has taken over`);\n        }\n        if (this.activeSessionsByUserId.get(userId) === session) {\n          this.activeSessionsByUserId.delete(userId);\n        }\n\n        // Clean up any pending photo requests for this session\n        this.cleanupPhotoRequestsForSession(sessionId);\n      } else {\n        // Temporary disconnect - session stays in maps for reconnection\n        // Photo requests remain pending and can still be fulfilled\n        this.logger.debug(\n          { sessionId, reason },\n          `🔄 Temporary disconnect for session ${sessionId}, keeping in maps for reconnection`,\n        );\n      }\n    });\n\n    const cleanupError = session.events.onError((error) => {\n      this.logger.error(error, `❌ [Session ${sessionId}] Error:`);\n    });\n\n    // Start the session\n    try {\n      await session.connect(sessionId);\n      this.activeSessions.set(sessionId, session);\n      this.activeSessionsByUserId.set(userId, session);\n      await this.onSession(session, sessionId, userId);\n      res.status(200).json({ status: \"success\" } as WebhookResponse);\n    } catch (error) {\n      this.logger.error(error, \"❌ Failed to connect:\");\n      cleanupDisconnect();\n      cleanupError();\n      res.status(500).json({\n        status: \"error\",\n        message: \"Failed to connect\",\n      } as WebhookResponse);\n    }\n  }\n\n  /**\n   * Handle a stop request webhook\n   */\n  private async handleStopRequest(request: StopWebhookRequest, res: express.Response): Promise<void> {\n    const { sessionId, userId, reason } = request;\n    this.logger.info(`\\n\\n🛑 Received stop request for user ${userId}, session ${sessionId}, reason: ${reason}\\n\\n`);\n\n    try {\n      await this.onStop(sessionId, userId, reason);\n      res.status(200).json({ status: \"success\" } as WebhookResponse);\n    } catch (error) {\n      this.logger.error(error, \"❌ Error handling stop request:\");\n      res.status(500).json({\n        status: \"error\",\n        message: \"Failed to process stop request\",\n      } as WebhookResponse);\n    }\n  }\n\n  /**\n   * ❤️ Setup Health Check Endpoint\n   * Creates a /health endpoint for monitoring server status.\n   */\n  private setupHealthCheck(): void {\n    if (this.config.healthCheck) {\n      this.app.get(\"/health\", (req, res) => {\n        res.json({\n          status: \"healthy\",\n          app: this.config.packageName,\n          activeSessions: this.activeSessions.size,\n        });\n      });\n    }\n  }\n\n  /**\n   * ⚙️ Setup Settings Endpoint\n   * Creates a /settings endpoint that the MentraOS Cloud can use to update settings.\n   */\n  private setupSettingsEndpoint(): void {\n    this.app.post(\"/settings\", async (req, res) => {\n      try {\n        const { userIdForSettings, settings } = req.body;\n\n        if (!userIdForSettings || !Array.isArray(settings)) {\n          return res.status(400).json({\n            status: \"error\",\n            message: \"Missing userId or settings array in request body\",\n          });\n        }\n\n        this.logger.info(`⚙️ Received settings update for user ${userIdForSettings}`);\n\n        // Find all active sessions for this user\n        const userSessions: AppSession[] = [];\n\n        // Look through all active sessions\n        this.activeSessions.forEach((session, _sessionId) => {\n          // Check if the session has this userId (not directly accessible)\n          // We're relying on the webhook handler to have already verified this\n          if (session.userId === userIdForSettings) {\n            userSessions.push(session);\n          }\n        });\n\n        if (userSessions.length === 0) {\n          this.logger.warn(`⚠️ No active sessions found for user ${userIdForSettings}`);\n        } else {\n          this.logger.info(`🔄 Updating settings for ${userSessions.length} active sessions`);\n        }\n\n        // Update settings for all of the user's sessions\n        for (const session of userSessions) {\n          session.updateSettingsForTesting(settings);\n        }\n\n        // Allow subclasses to handle settings updates if they implement the method\n        if (typeof (this as any).onSettingsUpdate === \"function\") {\n          await (this as any).onSettingsUpdate(userIdForSettings, settings);\n        }\n\n        res.json({\n          status: \"success\",\n          message: \"Settings updated successfully\",\n          sessionsUpdated: userSessions.length,\n        });\n      } catch (error) {\n        this.logger.error(error, \"❌ Error handling settings update:\");\n        res.status(500).json({\n          status: \"error\",\n          message: \"Internal server error processing settings update\",\n        });\n      }\n    });\n  }\n\n  /**\n   * 📂 Setup Static File Serving\n   * Configures Express to serve static files from the specified directory.\n   */\n  private setupPublicDir(): void {\n    if (this.config.publicDir) {\n      const publicPath = path.resolve(this.config.publicDir);\n      this.app.use(express.static(publicPath));\n      this.logger.info(`📂 Serving static files from ${publicPath}`);\n    }\n  }\n\n  /**\n   * 🛑 Setup Shutdown Handlers\n   * Registers process signal handlers for graceful shutdown.\n   */\n  private setupShutdown(): void {\n    process.on(\"SIGTERM\", () => this.stop());\n    process.on(\"SIGINT\", () => this.stop());\n  }\n\n  /**\n   * 🧹 Cleanup\n   * Closes all active sessions and runs cleanup handlers.\n   * Does NOT release ownership - we want the cloud to resurrect when we come back up.\n   *\n   * OWNERSHIP_RELEASE should only be sent for:\n   * - switching_clouds: User moved to another cloud, don't compete\n   * - user_logout: User explicitly logged out\n   *\n   * NOT for clean_shutdown, because:\n   * - Server is restarting/redeploying\n   * - Cloud should resurrect the app (trigger webhook)\n   * - User expects their app to keep running\n   *\n   * See: cloud/issues/023-disposed-appsession-resurrection-bug\n   */\n  private async cleanup(): Promise<void> {\n    this.logger.info(`🔧 [LOCAL SDK] cleanup() called - NOT sending OWNERSHIP_RELEASE`);\n    // Close all active sessions WITHOUT releasing ownership\n    // This allows the cloud to resurrect apps when we come back up\n    for (const [sessionId, session] of this.activeSessions) {\n      this.logger.info(`👋 Closing session ${sessionId} (no ownership release - cloud will resurrect)`);\n      try {\n        // Just disconnect, don't release ownership\n        // The cloud will enter grace period and then resurrect via webhook\n        await session.disconnect({\n          releaseOwnership: false,\n        });\n      } catch (error) {\n        this.logger.error(error, `Error during cleanup of session ${sessionId}`);\n        // Still try to disconnect even if release fails\n        try {\n          await session.disconnect();\n        } catch {\n          // Ignore secondary errors\n        }\n      }\n    }\n    this.activeSessions.clear();\n    this.activeSessionsByUserId.clear();\n\n    // Run cleanup handlers\n    this.cleanupHandlers.forEach((handler) => handler());\n  }\n\n  /**\n   * 🎯 Setup Photo Upload Endpoint\n   * Creates a /photo-upload endpoint for receiving photos directly from ASG glasses\n   */\n  private setupPhotoUploadEndpoint(): void {\n    const multer = require(\"multer\");\n\n    // Configure multer for handling multipart form data\n    const upload = multer({\n      storage: multer.memoryStorage(),\n      limits: {\n        fileSize: 10 * 1024 * 1024, // 10MB limit\n      },\n      fileFilter: (req: any, file: any, cb: any) => {\n        // Accept image files only\n        if (file.mimetype && file.mimetype.startsWith(\"image/\")) {\n          cb(null, true);\n        } else {\n          cb(new Error(\"Only image files are allowed\"), false);\n        }\n      },\n    });\n\n    this.app.post(\"/photo-upload\", upload.single(\"photo\"), async (req: any, res: any) => {\n      try {\n        const { requestId, type, success, errorCode, errorMessage } = req.body;\n        const photoFile = req.file;\n\n        this.logger.info(\n          { requestId, type, success, errorCode },\n          `📸 Received photo response: ${requestId} (type: ${type})`,\n        );\n\n        if (!requestId) {\n          this.logger.error(\"No requestId in photo response\");\n          return res.status(400).json({\n            success: false,\n            error: \"No requestId provided\",\n          });\n        }\n\n        // Direct O(1) lookup from AppServer's pending photo requests map\n        // This is the new architecture that survives session reconnections\n        // See: cloud/issues/019-sdk-photo-request-architecture\n        const pending = this.completePhotoRequest(requestId);\n        if (!pending) {\n          this.logger.warn(\n            { requestId, pendingCount: this.pendingPhotoRequests.size },\n            \"📸 No pending request found for photo (may have timed out or session ended)\",\n          );\n          return res.status(404).json({\n            success: false,\n            error: \"No pending request found for this photo (may have timed out or session ended)\",\n          });\n        }\n\n        // Handle error response (no photo file, but has error info)\n        if (type === \"photo_error\" || success === false) {\n          const errorMsg = errorMessage || \"Unknown error occurred\";\n          this.logger.info({ requestId, errorCode, errorMessage: errorMsg }, \"📸 Photo error received\");\n          pending.reject(new Error(`Photo capture failed: ${errorMsg} (code: ${errorCode || \"UNKNOWN_ERROR\"})`));\n\n          // Respond to ASG client\n          return res.json({\n            success: true,\n            requestId,\n            message: \"Photo error received successfully\",\n          });\n        }\n\n        // Handle successful photo upload\n        if (!photoFile) {\n          this.logger.error({ requestId }, \"No photo file in successful upload\");\n          pending.reject(new Error(\"No photo file provided for successful upload\"));\n          return res.status(400).json({\n            success: false,\n            error: \"No photo file provided for successful upload\",\n          });\n        }\n\n        // Create photo data object and resolve the promise\n        const photoData: PhotoData = {\n          buffer: photoFile.buffer,\n          mimeType: photoFile.mimetype,\n          filename: photoFile.originalname || \"photo.jpg\",\n          requestId,\n          size: photoFile.size,\n          timestamp: new Date(),\n        };\n\n        this.logger.info(\n          { requestId, size: photoFile.size, mimeType: photoFile.mimetype },\n          \"📸 Photo received successfully, resolving promise\",\n        );\n        pending.resolve(photoData);\n\n        // Respond to ASG client\n        res.json({\n          success: true,\n          requestId,\n          message: \"Photo received successfully\",\n        });\n      } catch (error) {\n        this.logger.error(error, \"❌ Error handling photo response\");\n        res.status(500).json({\n          success: false,\n          error: \"Internal server error processing photo response\",\n        });\n      }\n    });\n  }\n\n  // =====================================\n  // 📸 Photo Request Management APIs\n  // =====================================\n\n  /**\n   * Register a pending photo request.\n   * Called by CameraModule when a photo is requested.\n   * Stores the request at AppServer level for O(1) lookup when HTTP response arrives.\n   *\n   * @param requestId - Unique identifier for this photo request\n   * @param request - Request details including session, resolve/reject callbacks\n   */\n  registerPhotoRequest(requestId: string, request: Omit<PendingPhotoRequest, \"timeoutId\">): void {\n    // Set timeout at AppServer level (single source of truth)\n    const timeoutMs = 30000; // 30 seconds\n    const timeoutId = setTimeout(() => {\n      const pending = this.pendingPhotoRequests.get(requestId);\n      if (pending) {\n        pending.reject(new Error(\"Photo request timed out\"));\n        this.pendingPhotoRequests.delete(requestId);\n        this.logger.warn({ requestId }, \"📸 Photo request timed out\");\n      }\n    }, timeoutMs);\n\n    this.pendingPhotoRequests.set(requestId, {\n      ...request,\n      timeoutId,\n    });\n\n    this.logger.debug(\n      { requestId, userId: request.userId, sessionId: request.sessionId },\n      \"📸 Photo request registered at AppServer level\",\n    );\n  }\n\n  /**\n   * Get a pending photo request by ID.\n   *\n   * @param requestId - The request ID to look up\n   * @returns The pending request, or undefined if not found\n   */\n  getPhotoRequest(requestId: string): PendingPhotoRequest | undefined {\n    return this.pendingPhotoRequests.get(requestId);\n  }\n\n  /**\n   * Complete a photo request (success or error).\n   * Clears the timeout and removes from the pending map.\n   *\n   * @param requestId - The request ID to complete\n   * @returns The pending request that was completed, or undefined if not found\n   */\n  completePhotoRequest(requestId: string): PendingPhotoRequest | undefined {\n    const pending = this.pendingPhotoRequests.get(requestId);\n    if (pending) {\n      if (pending.timeoutId) {\n        clearTimeout(pending.timeoutId);\n      }\n      this.pendingPhotoRequests.delete(requestId);\n      this.logger.debug({ requestId }, \"📸 Photo request completed\");\n    }\n    return pending;\n  }\n\n  /**\n   * Clean up all pending photo requests for a session.\n   * Called when a session permanently disconnects.\n   *\n   * @param sessionId - The session ID to clean up requests for\n   */\n  cleanupPhotoRequestsForSession(sessionId: string): void {\n    let cleanedCount = 0;\n    for (const [requestId, pending] of this.pendingPhotoRequests) {\n      if (pending.sessionId === sessionId) {\n        if (pending.timeoutId) {\n          clearTimeout(pending.timeoutId);\n        }\n        pending.reject(new Error(\"Session ended\"));\n        this.pendingPhotoRequests.delete(requestId);\n        cleanedCount++;\n        this.logger.debug({ requestId, sessionId }, \"📸 Photo request cleaned up (session ended)\");\n      }\n    }\n    if (cleanedCount > 0) {\n      this.logger.info({ sessionId, cleanedCount }, \"📸 Cleaned up photo requests for ended session\");\n    }\n  }\n\n  /**\n   * 🔐 Setup Mentra Auth Redirect Endpoint\n   * Creates a /mentra-auth endpoint that redirects to the MentraOS OAuth flow.\n   */\n  private setupMentraAuthRedirect(): void {\n    this.app.get(\"/mentra-auth\", (req, res) => {\n      // Redirect to the account.mentra.glass OAuth flow with the app's package name\n      const authUrl = `https://account.mentra.glass/auth?packagename=${encodeURIComponent(this.config.packageName)}`;\n\n      this.logger.info(`🔐 Redirecting to MentraOS OAuth flow: ${authUrl}`);\n\n      res.redirect(302, authUrl);\n    });\n  }\n}\n\n/**\n * @deprecated Use `AppServerConfig` instead. `TpaServerConfig` is deprecated and will be removed in a future version.\n * This is an alias for backward compatibility only.\n *\n * @example\n * ```typescript\n * // ❌ Deprecated - Don't use this\n * const config: TpaServerConfig = { ... };\n *\n * // ✅ Use this instead\n * const config: AppServerConfig = { ... };\n * ```\n */\nexport type TpaServerConfig = AppServerConfig;\n\n/**\n * @deprecated Use `AppServer` instead. `TpaServer` is deprecated and will be removed in a future version.\n * This is an alias for backward compatibility only.\n *\n * @example\n * ```typescript\n * // ❌ Deprecated - Don't use this\n * class MyServer extends TpaServer { ... }\n *\n * // ✅ Use this instead\n * class MyServer extends AppServer { ... }\n * ```\n */\nexport class TpaServer extends AppServer {\n  constructor(config: TpaServerConfig) {\n    super(config);\n    // Emit a deprecation warning to help developers migrate\n    console.warn(\n      \"⚠️  DEPRECATION WARNING: TpaServer is deprecated and will be removed in a future version. \" +\n        \"Please use AppServer instead. \" +\n        'Simply replace \"TpaServer\" with \"AppServer\" in your code.',\n    );\n  }\n}\n","/* eslint-disable import/order */\n \n/**\n * 🎯 App Session Module\n *\n * Manages an active Third Party App session with MentraOS Cloud.\n * Handles real-time communication, event subscriptions, and display management.\n */\n\n// Patch version for tracking Bug 007 fix (subscriptions derived from handlers)\n// v1: Derive subscriptions from handlers (single source of truth)\n// v2: Add 'terminated' flag to prevent reconnection after \"User session ended\"\n// This helps verify the correct SDK version is running in production\nconst SDK_SUBSCRIPTION_PATCH = \"bug007-fix-v2\";\nimport { WebSocket } from \"ws\";\nimport { EventManager, EventData } from \"./events\";\nimport { LayoutManager } from \"./layouts\";\nimport { SettingsManager } from \"./settings\";\nimport { LocationManager } from \"./modules/location\";\nimport { CameraModule } from \"./modules/camera\";\nimport { LedModule } from \"./modules/led\";\nimport { AudioManager } from \"./modules/audio\";\nimport { ResourceTracker } from \"../../utils/resource-tracker\";\nimport {\n  // Message types\n  AppToCloudMessage,\n  CloudToAppMessage,\n  AppConnectionInit,\n  AppSubscriptionUpdate,\n  AudioPlayResponse,\n  RequestWifiSetup,\n  OwnershipReleaseMessage,\n  AppToCloudMessageType,\n  CloudToAppMessageType,\n\n  // Event data types\n  StreamType,\n  ExtendedStreamType,\n  ButtonPress,\n  HeadPosition,\n  TouchEvent,\n  PhoneNotification,\n  PhoneNotificationDismissed,\n  TranscriptionData,\n  TranslationData,\n  createTouchEventStream,\n\n  // Type guards\n  isAppConnectionAck,\n  isAppConnectionError,\n  isDataStream,\n  isAppStopped,\n  isSettingsUpdate,\n  isDashboardModeChanged,\n  isDashboardAlwaysOnChanged,\n  isAudioPlayResponse,\n  isCapabilitiesUpdate,\n\n  // Other types\n  AppSettings,\n  AppSetting,\n  AppConfig,\n  validateAppConfig,\n  AudioChunk,\n  VpsCoordinates,\n  PhotoTaken,\n  SubscriptionRequest,\n  Capabilities,\n  CapabilitiesUpdate,\n} from \"../../types\";\nimport { DashboardAPI } from \"../../types/dashboard\";\nimport { MentraosSettingsUpdate } from \"../../types/messages/cloud-to-app\";\nimport { Logger } from \"pino\";\nimport { AppServer } from \"../server\";\nimport axios from \"axios\";\nimport EventEmitter from \"events\";\n\n// Import the cloud-to-app specific type guards\nimport {\n  isPhotoResponse,\n  isRgbLedControlResponse,\n  isRtmpStreamStatus,\n  isManagedStreamStatus,\n  isStreamStatusCheckResponse,\n  isDeviceStateUpdate,\n} from \"../../types/messages/cloud-to-app\";\nimport { SimpleStorage } from \"./modules/simple-storage\";\nimport { DeviceState } from \"./device-state\";\nimport { readNotificationWarnLog } from \"../../utils/permissions-utils\";\n\n/**\n * ⚙️ Configuration options for App Session\n *\n * @example\n * ```typescript\n * const config: AppSessionConfig = {\n *   packageName: 'org.example.myapp',\n *   apiKey: 'your_api_key',\n *   // Auto-reconnection is enabled by default\n *   // autoReconnect: true\n * };\n * ```\n */\nexport interface AppSessionConfig {\n  /** 📦 Unique identifier for your App (e.g., 'org.company.appname') */\n  packageName: string;\n  /** 🔑 API key for authentication with MentraOS Cloud */\n  apiKey: string;\n  /** 🔌 WebSocket server URL (default: 'ws://localhost:7002/app-ws') */\n  mentraOSWebsocketUrl?: string;\n  /** 🔄 Automatically attempt to reconnect on disconnect (default: true) */\n  autoReconnect?: boolean;\n  /** 🔁 Maximum number of reconnection attempts (default: 3) */\n  maxReconnectAttempts?: number;\n  /** ⏱️ Base delay between reconnection attempts in ms (default: 1000) */\n  reconnectDelay?: number;\n\n  userId: string; // user ID for tracking sessions (email of the user).\n  appServer: AppServer; // Optional App server instance for advanced features\n}\n\n// List of event types that should never be subscribed to as streams\nconst APP_TO_APP_EVENT_TYPES = [\n  \"app_message_received\",\n  \"app_user_joined\",\n  \"app_user_left\",\n  \"app_room_updated\",\n  \"app_direct_message_response\",\n];\n\n/**\n * 🚀 App Session Implementation\n *\n * Manages a live connection between your App and MentraOS Cloud.\n * Provides interfaces for:\n * - 🎮 Event handling (transcription, head position, etc.)\n * - 📱 Display management in AR view\n * - 🔌 Connection lifecycle\n * - 🔄 Automatic reconnection\n *\n * @example\n * ```typescript\n * const session = new AppSession({\n *   packageName: 'org.example.myapp',\n *   apiKey: 'your_api_key'\n * });\n *\n * // Handle events\n * session.onTranscription((data) => {\n *   session.layouts.showTextWall(data.text);\n * });\n *\n * // Connect to cloud\n * await session.connect('session_123');\n * ```\n */\nexport class AppSession {\n  /** WebSocket connection to MentraOS Cloud */\n  private ws: WebSocket | null = null;\n  /** Current session identifier */\n  private sessionId: string | null = null;\n  /** Number of reconnection attempts made */\n  private reconnectAttempts = 0;\n  /** Flag to prevent reconnection after session termination (e.g., \"User session ended\") */\n  private terminated = false;\n  // REMOVED: private subscriptions = new Set<ExtendedStreamType>()\n  // Subscriptions are now derived from EventManager.handlers (single source of truth)\n  // This prevents drift between handlers and subscriptions that caused Bug 007\n  // See: cloud/issues/006-captions-and-apps-stopping/011-sdk-subscription-architecture-mismatch.md\n  /** Map to store rate options for streams */\n  private streamRates = new Map<ExtendedStreamType, string>();\n  /** Resource tracker for automatic cleanup */\n  private resources = new ResourceTracker();\n  /** Internal settings storage - use public settings API instead */\n  private settingsData: AppSettings = [];\n  /** App configuration loaded from app_config.json */\n  private appConfig: AppConfig | null = null;\n  /** Whether to update subscriptions when settings change */\n  private shouldUpdateSubscriptionsOnSettingsChange = false;\n  /** Custom subscription handler for settings-based subscriptions */\n  private subscriptionSettingsHandler?: (settings: AppSettings) => ExtendedStreamType[];\n  /** Settings that should trigger subscription updates when changed */\n  private subscriptionUpdateTriggers: string[] = [];\n  /** Pending user discovery requests waiting for responses */\n  private pendingUserDiscoveryRequests = new Map<\n    string,\n    {\n      resolve: (userList: any) => void;\n      reject: (reason: any) => void;\n    }\n  >();\n  /** Pending direct message requests waiting for responses */\n  private pendingDirectMessages = new Map<\n    string,\n    {\n      resolve: (success: boolean) => void;\n      reject: (reason: any) => void;\n    }\n  >();\n\n  /** 🎮 Event management interface */\n  public readonly events: EventManager;\n  /** 📱 Layout management interface */\n  public readonly layouts: LayoutManager;\n  /** ⚙️ Settings management interface */\n  public readonly settings: SettingsManager;\n  /** 📊 Dashboard management interface */\n  public readonly dashboard: DashboardAPI;\n  /** 📍 Location management interface */\n  public readonly location: LocationManager;\n  /** 📷 Camera interface for photos and streaming */\n  public readonly camera: CameraModule;\n  /** 💡 LED interface for RGB LED control */\n  public readonly led: LedModule;\n  /** 🔊 Audio interface for audio playback */\n  public readonly audio: AudioManager;\n  /** 🔐 Simple key-value storage interface */\n  public readonly simpleStorage: SimpleStorage;\n  /** 📱 Reactive device state (WebSocket-based observables) */\n  public readonly device: { state: DeviceState };\n\n  public readonly appServer: AppServer;\n  public readonly logger: Logger;\n  public readonly userId: string;\n\n  /** 🔧 Device capabilities available for this session */\n  public capabilities: Capabilities | null = null;\n\n  /** 📡 Latest glasses connection state (includes WiFi status) */\n  private glassesConnectionState: any = null; // Using any for now since GlassesConnectionState is in glasses-to-cloud types\n\n  /** Dedicated emitter for App-to-App events */\n  private appEvents = new EventEmitter();\n\n  constructor(private config: AppSessionConfig) {\n    // Set defaults and merge with provided config\n    this.config = {\n      mentraOSWebsocketUrl: `ws://localhost:8002/app-ws`, // Use localhost as default\n      autoReconnect: true, // Enable auto-reconnection by default for better reliability\n      maxReconnectAttempts: 3, // Default to 3 reconnection attempts for better resilience\n      reconnectDelay: 1000, // Start with 1 second delay (uses exponential backoff)\n      ...config,\n    };\n\n    this.appServer = this.config.appServer;\n    this.logger = this.appServer.logger.child({\n      userId: this.config.userId,\n      service: \"app-session\",\n    });\n    this.userId = this.config.userId;\n\n    // Make sure the URL is correctly formatted to prevent double protocol issues\n    if (this.config.mentraOSWebsocketUrl) {\n      try {\n        const url = new URL(this.config.mentraOSWebsocketUrl);\n        if (![\"ws:\", \"wss:\"].includes(url.protocol)) {\n          // Fix URLs with incorrect protocol (e.g., 'ws://http://host')\n          const fixedUrl = this.config.mentraOSWebsocketUrl.replace(/^ws:\\/\\/http:\\/\\//, \"ws://\");\n          this.config.mentraOSWebsocketUrl = fixedUrl;\n          this.logger.warn(`⚠️ [${this.config.packageName}] Fixed malformed WebSocket URL: ${fixedUrl}`);\n        }\n      } catch (error) {\n        this.logger.error(\n          error,\n          `⚠️ [${this.config.packageName}] Invalid WebSocket URL format: ${this.config.mentraOSWebsocketUrl}`,\n        );\n      }\n    }\n\n    // Log initialization\n    this.logger.debug(`🚀 [${this.config.packageName}] App Session initialized`);\n    this.logger.debug(`🚀 [${this.config.packageName}] WebSocket URL: ${this.config.mentraOSWebsocketUrl}`);\n\n    // Validate URL format - give early warning for obvious issues\n    // Check URL format but handle undefined case\n    if (this.config.mentraOSWebsocketUrl) {\n      try {\n        const url = new URL(this.config.mentraOSWebsocketUrl);\n        if (![\"ws:\", \"wss:\"].includes(url.protocol)) {\n          this.logger.error(\n            { config: this.config },\n            `⚠️ [${this.config.packageName}] Invalid WebSocket URL protocol: ${url.protocol}. Should be ws: or wss:`,\n          );\n        }\n      } catch (error) {\n        this.logger.error(\n          error,\n          `⚠️ [${this.config.packageName}] Invalid WebSocket URL format: ${this.config.mentraOSWebsocketUrl}`,\n        );\n      }\n    }\n\n    this.events = new EventManager(\n      this.subscribe.bind(this),\n      this.unsubscribe.bind(this),\n      this.config.packageName,\n      this.getHttpsServerUrl() || \"\",\n    );\n    this.layouts = new LayoutManager(config.packageName, this.send.bind(this));\n\n    // Initialize settings manager with all necessary parameters, including subscribeFn for MentraOS settings\n    this.settings = new SettingsManager(\n      this.settingsData,\n      this.config.packageName,\n      this.config.mentraOSWebsocketUrl,\n      this.sessionId ?? undefined,\n      async (streams: string[]) => {\n        // NOTE: With Bug 007 fix, subscriptions are derived from EventManager.handlers\n        // This subscribeFn is called by SettingsManager to auto-subscribe to streams for MentraOS settings\n        // The actual subscription intent should be tracked via handlers, not a separate Set\n        this.logger.debug({ streams: JSON.stringify(streams) }, `[AppSession] subscribeFn called for streams`);\n\n        // Log current handler-based subscriptions for debugging\n        const currentHandlerStreams = this.events.getRegisteredStreams();\n        this.logger.debug(\n          {\n            requestedStreams: JSON.stringify(streams),\n            currentHandlerStreams: JSON.stringify(currentHandlerStreams),\n          },\n          `[AppSession] subscribeFn: requested streams vs current handler streams`,\n        );\n\n        // Send subscription update if connected\n        // Note: The actual subscriptions sent are derived from handlers\n        if (this.ws?.readyState === 1) {\n          this.updateSubscriptions();\n          this.logger.debug(`[AppSession] Sent updated subscriptions to cloud (derived from handlers).`);\n        } else {\n          this.logger.debug(`[AppSession] WebSocket not open, will send subscriptions when connected.`);\n        }\n      },\n    );\n\n    // Initialize dashboard API with this session instance\n    // Import DashboardManager dynamically to avoid circular dependency\n    const { DashboardManager } = require(\"./dashboard\");\n    this.dashboard = new DashboardManager(this);\n\n    // Initialize camera module with session reference\n    this.camera = new CameraModule(\n      this,\n      this.config.packageName,\n      this.sessionId || \"unknown-session-id\",\n      this.logger.child({ module: \"camera\" }),\n    );\n\n    // Initialize LED control module\n    this.led = new LedModule(\n      this,\n      this.config.packageName,\n      this.sessionId || \"unknown-session-id\",\n      this.logger.child({ module: \"led\" }),\n    );\n\n    // Initialize audio module with session reference\n    this.audio = new AudioManager(\n      this,\n      this.config.packageName,\n      this.sessionId || \"unknown-session-id\",\n      this.logger.child({ module: \"audio\" }),\n    );\n\n    this.simpleStorage = new SimpleStorage(this);\n    this.device = { state: new DeviceState(this) };\n\n    this.location = new LocationManager(this);\n  }\n\n  /**\n   * Get the current session ID\n   * @returns The current session ID or 'unknown-session-id' if not connected\n   */\n  getSessionId(): string {\n    return this.sessionId || \"unknown-session-id\";\n  }\n\n  /**\n   * Get the package name for this App\n   * @returns The package name\n   */\n  getPackageName(): string {\n    return this.config.packageName;\n  }\n\n  // =====================================\n  // 🎮 Direct Event Handling Interface\n  // =====================================\n\n  /**\n   * @deprecated Use session.events.onTranscription() instead\n   */\n  onTranscription(handler: (data: TranscriptionData) => void): () => void {\n    return this.events.onTranscription(handler);\n  }\n\n  /**\n   * 🌐 Listen for speech transcription events in a specific language\n   * @param language - Language code (e.g., \"en-US\")\n   * @param handler - Function to handle transcription data\n   * @returns Cleanup function to remove the handler\n   * @throws Error if language code is invalid\n   * @deprecated Use session.events.onTranscriptionForLanguage() instead\n   */\n  onTranscriptionForLanguage(\n    language: string,\n    handler: (data: TranscriptionData) => void,\n    disableLanguageIdentification = false,\n  ): () => void {\n    return this.events.onTranscriptionForLanguage(language, handler, disableLanguageIdentification);\n  }\n\n  /**\n   * 🌐 Listen for speech translation events for a specific language pair\n   * @param sourceLanguage - Source language code (e.g., \"es-ES\")\n   * @param targetLanguage - Target language code (e.g., \"en-US\")\n   * @param handler - Function to handle translation data\n   * @returns Cleanup function to remove the handler\n   * @throws Error if language codes are invalid\n   * @deprecated Use session.events.onTranslationForLanguage() instead\n   */\n  onTranslationForLanguage(\n    sourceLanguage: string,\n    targetLanguage: string,\n    handler: (data: TranslationData) => void,\n  ): () => void {\n    return this.events.ontranslationForLanguage(sourceLanguage, targetLanguage, handler);\n  }\n\n  /**\n   * 👤 Listen for head position changes\n   * @param handler - Function to handle head position updates\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onHeadPosition() instead\n   */\n  onHeadPosition(handler: (data: HeadPosition) => void): () => void {\n    return this.events.onHeadPosition(handler);\n  }\n\n  /**\n   * 🔘 Listen for hardware button press events\n   * @param handler - Function to handle button events\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onButtonPress() instead\n   */\n  onButtonPress(handler: (data: ButtonPress) => void): () => void {\n    return this.events.onButtonPress(handler);\n  }\n\n  /**\n   * 👆 Listen for touch gesture events\n   * @param gestureOrHandler - Gesture name or handler function\n   * @param handler - Handler function (if first param is gesture name)\n   * @returns Cleanup function\n   *\n   * @example\n   * // Subscribe to all touch events\n   * session.onTouchEvent((event) => console.log(event.gesture_name));\n   *\n   * // Subscribe to specific gesture\n   * session.onTouchEvent(\"forward_swipe\", (event) => console.log(\"Forward swipe!\"));\n   */\n  onTouchEvent(\n    gestureOrHandler: string | ((data: TouchEvent) => void),\n    handler?: (data: TouchEvent) => void,\n  ): () => void {\n    return this.events.onTouchEvent(gestureOrHandler as any, handler as any);\n  }\n\n  /**\n   * 👆 Subscribe to multiple touch gestures\n   * @param gestures - Array of gesture names\n   * @returns Cleanup function that unsubscribes from all\n   *\n   * @example\n   * session.subscribeToGestures([\"forward_swipe\", \"backward_swipe\"]);\n   */\n  subscribeToGestures(gestures: string[]): () => void {\n    gestures.forEach((gesture) => {\n      const stream = createTouchEventStream(gesture);\n      this.subscribe(stream);\n    });\n\n    return () => {\n      gestures.forEach((gesture) => {\n        const stream = createTouchEventStream(gesture);\n        this.unsubscribe(stream);\n      });\n    };\n  }\n\n  /**\n   * 📱 Listen for phone notification events\n   * @param handler - Function to handle notifications\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onPhoneNotifications() instead\n   */\n  onPhoneNotifications(handler: (data: PhoneNotification) => void): () => void {\n    readNotificationWarnLog(this.getHttpsServerUrl() || \"\", this.getPackageName(), \"onPhoneNotifications\");\n    return this.events.onPhoneNotifications(handler);\n  }\n\n  /**\n   * 📱 Listen for phone notification dismissed events\n   * @param handler - Function to handle notification dismissal data\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onPhoneNotificationDismissed() instead\n   */\n  onPhoneNotificationDismissed(handler: (data: PhoneNotificationDismissed) => void): () => void {\n    return this.events.onPhoneNotificationDismissed(handler);\n  }\n\n  /**\n   * 📡 Listen for VPS coordinates updates\n   * @param handler - Function to handle VPS coordinates\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onVpsCoordinates() instead\n   */\n  onVpsCoordinates(handler: (data: VpsCoordinates) => void): () => void {\n    this.subscribe(StreamType.VPS_COORDINATES);\n    return this.events.onVpsCoordinates(handler);\n  }\n\n  /**\n   * 📸 Listen for photo responses\n   * @param handler - Function to handle photo response data\n   * @returns Cleanup function to remove the handler\n   * @deprecated Use session.events.onPhotoTaken() instead\n   */\n  onPhotoTaken(handler: (data: PhotoTaken) => void): () => void {\n    this.subscribe(StreamType.PHOTO_TAKEN);\n    return this.events.onPhotoTaken(handler);\n  }\n\n  // =====================================\n  // 📡 Pub/Sub Interface\n  // =====================================\n\n  /**\n   * 📬 Subscribe to a specific event stream\n   * @param sub - A string or a rich subscription object\n   */\n  subscribe(sub: SubscriptionRequest): void {\n    let type: ExtendedStreamType;\n    let rate: string | undefined;\n\n    if (typeof sub === \"string\") {\n      type = sub;\n    } else {\n      // it's a LocationStreamRequest object\n      type = sub.stream;\n      rate = sub.rate;\n    }\n\n    if (APP_TO_APP_EVENT_TYPES.includes(type as string)) {\n      this.logger.warn(\n        `[AppSession] Attempted to subscribe to App-to-App event type '${type}', which is not a valid stream. Use the event handler (e.g., onAppMessage) instead.`,\n      );\n      return;\n    }\n\n    // NOTE: We no longer maintain this.subscriptions - subscriptions are derived from handlers\n    // This prevents drift between handlers and subscriptions (Bug 007 fix)\n    // The EventManager.addHandler() already tracks the subscription intent via handlers\n\n    if (rate) {\n      this.streamRates.set(type, rate);\n    }\n\n    if (this.ws?.readyState === 1) {\n      this.updateSubscriptions();\n    }\n  }\n\n  /**\n   * 📭 Unsubscribe from a specific event stream\n   * @param sub - The subscription to remove\n   */\n  unsubscribe(sub: SubscriptionRequest): void {\n    let type: ExtendedStreamType;\n    if (typeof sub === \"string\") {\n      type = sub;\n    } else {\n      type = sub.stream;\n    }\n\n    if (APP_TO_APP_EVENT_TYPES.includes(type as string)) {\n      this.logger.warn(\n        `[AppSession] Attempted to unsubscribe from App-to-App event type '${type}', which is not a valid stream.`,\n      );\n      return;\n    }\n    // NOTE: We no longer maintain this.subscriptions - subscriptions are derived from handlers\n    // The EventManager.removeHandler() already tracks the unsubscription intent\n\n    this.streamRates.delete(type); // also remove from our rate map\n    if (this.ws?.readyState === 1) {\n      this.updateSubscriptions();\n    }\n  }\n\n  /**\n   * 🎯 Generic event listener (pub/sub style)\n   * @param event - Event name to listen for\n   * @param handler - Event handler function\n   */\n  on<T extends ExtendedStreamType>(event: T, handler: (data: EventData<T>) => void): () => void {\n    return this.events.on(event, handler);\n  }\n\n  // =====================================\n  // 🔌 Connection Management\n  // =====================================\n\n  /**\n   * 🚀 Connect to MentraOS Cloud\n   * @param sessionId - Unique session identifier\n   * @returns Promise that resolves when connected\n   */\n  async connect(sessionId: string): Promise<void> {\n    this.sessionId = sessionId;\n\n    // Configure settings API client with the WebSocket URL and session ID\n    // This allows settings to be fetched from the correct server\n    this.settings.configureApiClient(this.config.packageName, this.config.mentraOSWebsocketUrl || \"\", sessionId);\n\n    // Update the sessionId in the camera module\n    if (this.camera) {\n      this.camera.updateSessionId(sessionId);\n    }\n\n    // Update the sessionId in the audio module\n    if (this.audio) {\n      this.audio.updateSessionId(sessionId);\n    }\n\n    return new Promise((resolve, reject) => {\n      try {\n        // Clear previous resources if reconnecting\n        if (this.ws) {\n          // Don't call full dispose() as that would clear subscriptions\n          if (this.ws.readyState !== 3) {\n            // 3 = CLOSED\n            this.ws.close();\n          }\n          this.ws = null;\n        }\n\n        // Validate WebSocket URL before attempting connection\n        if (!this.config.mentraOSWebsocketUrl) {\n          this.logger.error(\"WebSocket URL is missing or undefined\");\n          reject(new Error(\"WebSocket URL is required\"));\n          return;\n        }\n\n        // Add debug logging for connection attempts\n        this.logger.info(\n          `🔌🔌🔌 [${this.config.packageName}] Attempting to connect to: ${this.config.mentraOSWebsocketUrl} for session ${this.sessionId}`,\n        );\n\n        // Create connection with error handling\n        this.ws = new WebSocket(this.config.mentraOSWebsocketUrl);\n\n        // Track WebSocket for automatic cleanup\n        this.resources.track(() => {\n          if (this.ws && this.ws.readyState !== 3) {\n            // 3 = CLOSED\n            this.ws.close();\n          }\n        });\n\n        this.ws.on(\"open\", () => {\n          try {\n            this.sendConnectionInit();\n          } catch (error: unknown) {\n            this.logger.error(error, \"Error during connection initialization\");\n            const errorMessage = error instanceof Error ? error.message : String(error);\n            this.events.emit(\"error\", new Error(`Connection initialization failed: ${errorMessage}`));\n            reject(error);\n          }\n        });\n\n        // Message handler with comprehensive error recovery\n        const messageHandler = async (data: Buffer | string, isBinary: boolean) => {\n          try {\n            // Handle binary messages (typically audio data)\n            if (isBinary && Buffer.isBuffer(data)) {\n              try {\n                // Validate buffer before processing\n                if (data.length === 0) {\n                  this.events.emit(\"error\", new Error(\"Received empty binary data\"));\n                  return;\n                }\n\n                // Convert Node.js Buffer to ArrayBuffer safely\n                const arrayBuf: ArrayBufferLike = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);\n\n                // Create AUDIO_CHUNK event message with validation\n                const audioChunk: AudioChunk = {\n                  type: StreamType.AUDIO_CHUNK,\n                  arrayBuffer: arrayBuf,\n                  timestamp: new Date(), // Ensure timestamp is present\n                };\n\n                this.handleMessage(audioChunk);\n                return;\n              } catch (error: unknown) {\n                this.logger.error(error, \"Error processing binary message:\");\n                const errorMessage = error instanceof Error ? error.message : String(error);\n                this.events.emit(\"error\", new Error(`Failed to process binary message: ${errorMessage}`));\n                return;\n              }\n            }\n\n            // Handle ArrayBuffer data type directly\n            if (data instanceof ArrayBuffer) {\n              return;\n            }\n\n            // Handle JSON messages with validation\n            try {\n              // Convert string data to JSON safely\n              let jsonData: string;\n              if (typeof data === \"string\") {\n                jsonData = data;\n              } else if (Buffer.isBuffer(data)) {\n                jsonData = data.toString(\"utf8\");\n              } else {\n                throw new Error(\"Unknown message format\");\n              }\n\n              // Validate JSON before parsing\n              if (!jsonData || jsonData.trim() === \"\") {\n                this.events.emit(\"error\", new Error(\"Received empty JSON message\"));\n                return;\n              }\n\n              // Parse JSON with error handling\n              const message = JSON.parse(jsonData) as CloudToAppMessage;\n\n              // Basic schema validation\n              if (!message || typeof message !== \"object\" || !(\"type\" in message)) {\n                this.events.emit(\"error\", new Error(\"Malformed message: missing type property\"));\n                return;\n              }\n\n              // Process the validated message\n              this.handleMessage(message);\n            } catch (error: unknown) {\n              this.logger.error(error, \"JSON parsing error\");\n              const errorMessage = error instanceof Error ? error.message : String(error);\n              this.events.emit(\"error\", new Error(`Failed to parse JSON message: ${errorMessage}`));\n            }\n          } catch (error: unknown) {\n            // Final catch - should never reach here if individual handlers work correctly\n            this.logger.error({ error }, \"Unhandled message processing error\");\n            const errorMessage = error instanceof Error ? error.message : String(error);\n            this.events.emit(\"error\", new Error(`Unhandled message error: ${errorMessage}`));\n          }\n        };\n\n        this.ws.on(\"message\", messageHandler);\n\n        // Track event handler removal for automatic cleanup\n        this.resources.track(() => {\n          if (this.ws) {\n            this.ws.off(\"message\", messageHandler);\n          }\n        });\n\n        // Connection closure handler\n        const closeHandler = (code: number, reason: string) => {\n          const reasonStr = reason ? `: ${reason}` : \"\";\n          const closeInfo = `Connection closed (code: ${code})${reasonStr}`;\n\n          // Emit the disconnected event with structured data for better handling\n          this.events.emit(\"disconnected\", {\n            message: closeInfo,\n            code: code,\n            reason: reason || \"\",\n            wasClean: code === 1000 || code === 1001,\n          });\n\n          // Only attempt reconnection for abnormal closures\n          // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code\n          // 1000 (Normal Closure) and 1001 (Going Away) are normal\n          // 1002-1015 are abnormal, and reason \"App stopped\" means intentional closure\n          // 1008 usually when the userSession no longer exists on server. i.e user disconnected from cloud.\n          const isNormalClosure = code === 1000 || code === 1001 || code === 1008;\n          const isManualStop = reason && reason.includes(\"App stopped\");\n          const isUserSessionEnded = reason && reason.includes(\"User session ended\");\n\n          // Log closure details for diagnostics\n          this.logger.debug(`🔌 [${this.config.packageName}] WebSocket closed with code ${code}${reasonStr}`);\n          this.logger.debug(\n            `🔌 [${this.config.packageName}] isNormalClosure: ${isNormalClosure}, isManualStop: ${isManualStop}, isUserSessionEnded: ${isUserSessionEnded}`,\n          );\n\n          // If user session ended, mark as terminated to prevent any future reconnection\n          if (isUserSessionEnded) {\n            this.terminated = true;\n            this.logger.info(\n              `🛑 [${this.config.packageName}] User session ended - marking as terminated, no reconnection allowed`,\n            );\n          }\n\n          if (!isNormalClosure && !isManualStop && !this.terminated) {\n            this.logger.warn(`🔌 [${this.config.packageName}] Abnormal closure detected, attempting reconnection`);\n            this.handleReconnection();\n          } else {\n            this.logger.debug(\n              `🔌 [${this.config.packageName}] Normal/terminated closure detected, not attempting reconnection (terminated: ${this.terminated})`,\n            );\n          }\n\n          // if user session ended, then trigger onStop.\n          if (isUserSessionEnded) {\n            this.logger.info(\n              `🛑 [${this.config.packageName}] User session ended - emitting disconnected event with sessionEnded flag`,\n            );\n            // Emit a disconnected event with a special flag to indicate session end\n            // This will be caught by AppServer which will call the onStop callback\n            const disconnectInfo = {\n              message: \"User session ended\",\n              code: 1000, // Normal closure\n              reason: \"User session ended\",\n              wasClean: true,\n              permanent: true, // This is permanent - no reconnection\n              sessionEnded: true, // Special flag to indicate session disposal\n            };\n            this.events.emit(\"disconnected\", disconnectInfo);\n          }\n        };\n\n        this.ws.on(\"close\", closeHandler);\n\n        // Track event handler removal\n        this.resources.track(() => {\n          if (this.ws) {\n            this.ws.off(\"close\", closeHandler);\n          }\n        });\n\n        // Connection error handler\n        const errorHandler = (error: Error) => {\n          this.logger.error(error, \"WebSocket error\");\n          this.events.emit(\"error\", error);\n        };\n\n        // Enhanced error handler with detailed logging\n        this.ws.on(\"error\", (error: Error) => {\n          this.logger.error(\n            error,\n            `⛔️⛔️⛔️ [${this.config.packageName}] WebSocket connection error: ${error.message}`,\n          );\n\n          // Try to provide more context\n          const errMsg = error.message || \"\";\n          if (errMsg.includes(\"ECONNREFUSED\")) {\n            this.logger.error(\n              `⛔️⛔️⛔️ [${this.config.packageName}] Connection refused - Check if the server is running at the specified URL`,\n            );\n          } else if (errMsg.includes(\"ETIMEDOUT\")) {\n            this.logger.error(\n              `⛔️⛔️⛔️ [${this.config.packageName}] Connection timed out - Check network connectivity and firewall rules`,\n            );\n          }\n\n          errorHandler(error);\n        });\n\n        // Track event handler removal\n        this.resources.track(() => {\n          if (this.ws) {\n            this.ws.off(\"error\", errorHandler);\n          }\n        });\n\n        // Set up connection success handler\n        const connectedCleanup = this.events.onConnected(() => resolve());\n\n        // Track event handler removal\n        this.resources.track(connectedCleanup);\n\n        // Connection timeout with configurable duration\n        const timeoutMs = 5000; // 5 seconds default\n        const connectionTimeout = this.resources.setTimeout(() => {\n          // Use tracked timeout that will be auto-cleared\n          this.logger.error(\n            {\n              config: this.config,\n              sessionId: this.sessionId,\n              timeoutMs,\n            },\n            `⏱️⏱️⏱️ [${this.config.packageName}] Connection timeout after ${timeoutMs}ms`,\n          );\n\n          this.events.emit(\"error\", new Error(`Connection timeout after ${timeoutMs}ms`));\n          reject(new Error(\"Connection timeout\"));\n        }, timeoutMs);\n\n        // Clear timeout on successful connection\n        const timeoutCleanup = this.events.onConnected(() => {\n          clearTimeout(connectionTimeout);\n          resolve();\n        });\n\n        // Track event handler removal\n        this.resources.track(timeoutCleanup);\n      } catch (error: unknown) {\n        this.logger.error(error, \"Connection setup error\");\n        const errorMessage = error instanceof Error ? error.message : String(error);\n        reject(new Error(`Failed to setup connection: ${errorMessage}`));\n      }\n    });\n  }\n\n  /**\n   * 🔄 Release ownership of this session to allow clean handoff\n   * Call this before connecting to a different cloud instance or shutting down cleanly.\n   * This signals to the cloud that no resurrection is needed.\n   *\n   * @param reason - Why ownership is being released\n   */\n  async releaseOwnership(reason: \"switching_clouds\" | \"clean_shutdown\" | \"user_logout\"): Promise<void> {\n    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n      this.logger.debug(`[${this.config.packageName}] Cannot release ownership - WebSocket not open`);\n      return;\n    }\n\n    const message: OwnershipReleaseMessage = {\n      type: AppToCloudMessageType.OWNERSHIP_RELEASE,\n      packageName: this.config.packageName,\n      sessionId: this.sessionId || \"\",\n      reason,\n      timestamp: new Date(),\n    };\n\n    this.logger.info(\n      { reason, sessionId: this.sessionId },\n      `🔄 [${this.config.packageName}] Releasing ownership: ${reason}`,\n    );\n\n    this.send(message);\n\n    // Small delay to ensure message is sent before any subsequent disconnect\n    await new Promise((resolve) => setTimeout(resolve, 100));\n  }\n\n  /**\n   * 👋 Disconnect from MentraOS Cloud\n   * Flushes any pending SimpleStorage writes before closing\n   *\n   * @param options - Optional disconnect options\n   * @param options.releaseOwnership - If true, send OWNERSHIP_RELEASE before disconnecting (enables clean handoff)\n   * @param options.reason - Reason for ownership release (required if releaseOwnership is true)\n   */\n  async disconnect(options?: {\n    releaseOwnership?: boolean;\n    reason?: \"switching_clouds\" | \"clean_shutdown\" | \"user_logout\";\n  }): Promise<void> {\n    // Release ownership if requested (for clean handoffs)\n    if (options?.releaseOwnership && options?.reason) {\n      await this.releaseOwnership(options.reason);\n    }\n\n    // Flush any pending SimpleStorage writes before closing\n    try {\n      await this.simpleStorage.flush();\n      console.log(\"SimpleStorage flushed on disconnect\");\n    } catch (error) {\n      console.error(\"Error flushing SimpleStorage on disconnect:\", error);\n      // Continue with disconnect even if flush fails\n    }\n\n    // Clean up camera module first\n    if (this.camera) {\n      this.camera.cancelAllRequests();\n    }\n\n    // Clean up audio module\n    if (this.audio) {\n      this.audio.cancelAllRequests();\n    }\n\n    // Use the resource tracker to clean up everything\n    this.resources.dispose();\n\n    // Clean up additional resources not handled by the tracker\n    this.ws = null;\n    this.sessionId = null;\n    // REMOVED: this.subscriptions.clear()\n    // We no longer clear subscriptions here - they are derived from handlers\n    // This is the key fix for Bug 007: clearing subscriptions here caused\n    // empty subscription updates on reconnect when handlers still existed\n    // See: cloud/issues/006-captions-and-apps-stopping/011-sdk-subscription-architecture-mismatch.md\n    this.reconnectAttempts = 0;\n  }\n\n  /**\n   * 🛠️ Get all current user settings\n   * @returns A copy of the current settings array\n   * @deprecated Use session.settings.getAll() instead\n   */\n  getSettings(): AppSettings {\n    return this.settings.getAll();\n  }\n\n  /**\n   * 🔍 Get a specific setting value by key\n   * @param key The setting key to look for\n   * @returns The setting's value, or undefined if not found\n   * @deprecated Use session.settings.get(key) instead\n   */\n  getSetting<T>(key: string): T | undefined {\n    return this.settings.get<T>(key);\n  }\n\n  /**\n   * ⚙️ Configure settings-based subscription updates\n   * This allows Apps to automatically update their subscriptions when certain settings change\n   * @param options Configuration options for settings-based subscriptions\n   */\n  setSubscriptionSettings(options: {\n    updateOnChange: string[]; // Setting keys that should trigger subscription updates\n    handler: (settings: AppSettings) => ExtendedStreamType[]; // Handler that returns new subscriptions\n  }): void {\n    this.shouldUpdateSubscriptionsOnSettingsChange = true;\n    this.subscriptionUpdateTriggers = options.updateOnChange;\n    this.subscriptionSettingsHandler = options.handler;\n\n    // If we already have settings, update subscriptions immediately\n    if (this.settingsData.length > 0) {\n      this.updateSubscriptionsFromSettings();\n    }\n  }\n\n  /**\n   * 🔄 Update subscriptions based on current settings\n   * Called automatically when relevant settings change\n   */\n  private updateSubscriptionsFromSettings(): void {\n    if (!this.subscriptionSettingsHandler) return;\n\n    try {\n      // Get desired subscriptions from settings handler\n      const settingsSubscriptions = this.subscriptionSettingsHandler(this.settingsData);\n\n      // NOTE: Settings-based subscriptions work differently from handler-based subscriptions\n      // With the Bug 007 fix, subscriptions are now derived from EventManager.handlers\n      // Apps using setSubscriptionSettings() should ensure their settings correspond to\n      // registered handlers for the subscriptions to take effect.\n      //\n      // Log if there's a mismatch (for debugging during migration)\n      const handlerStreams = this.events.getRegisteredStreams();\n      if (settingsSubscriptions.length !== handlerStreams.length) {\n        this.logger.warn(\n          {\n            settingsSubscriptions: JSON.stringify(settingsSubscriptions),\n            handlerStreams: JSON.stringify(handlerStreams),\n          },\n          `[AppSession] Settings-based subscriptions (${settingsSubscriptions.length}) differ from handler-based subscriptions (${handlerStreams.length}). ` +\n            `Subscriptions are now derived from handlers. Ensure handlers are registered for desired streams.`,\n        );\n      }\n\n      // Send subscription update to cloud if connected\n      // Note: updateSubscriptions() derives from handlers, so settings-based apps\n      // should ensure their settings correspond to registered handlers\n      if (this.ws && this.ws.readyState === 1) {\n        this.updateSubscriptions();\n      }\n    } catch (error: unknown) {\n      this.logger.error(error, \"Error updating subscriptions from settings\");\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.events.emit(\"error\", new Error(`Failed to update subscriptions: ${errorMessage}`));\n    }\n  }\n\n  /**\n   * 🧪 For testing: Update settings locally\n   * In normal operation, settings come from the cloud\n   * @param newSettings The new settings to apply\n   */\n  updateSettingsForTesting(newSettings: AppSettings): void {\n    this.settingsData = newSettings;\n\n    // Update the settings manager with the new settings\n    this.settings.updateSettings(newSettings);\n\n    // Emit update event for backwards compatibility\n    this.events.emit(\"settings_update\", this.settingsData);\n\n    // Check if we should update subscriptions\n    if (this.shouldUpdateSubscriptionsOnSettingsChange) {\n      this.updateSubscriptionsFromSettings();\n    }\n  }\n\n  /**\n   * 📝 Load configuration from a JSON file\n   * @param jsonData JSON string containing App configuration\n   * @returns The loaded configuration\n   * @throws Error if the configuration is invalid\n   */\n  loadConfigFromJson(jsonData: string): AppConfig {\n    try {\n      const parsedConfig = JSON.parse(jsonData);\n\n      if (validateAppConfig(parsedConfig)) {\n        this.appConfig = parsedConfig;\n        return parsedConfig;\n      } else {\n        throw new Error(\"Invalid App configuration format\");\n      }\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      throw new Error(`Failed to load App configuration: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 📋 Get the loaded App configuration\n   * @returns The current App configuration or null if not loaded\n   */\n  getConfig(): AppConfig | null {\n    return this.appConfig;\n  }\n\n  /**\n   * 🔌 Get the WebSocket server URL for this session\n   * @returns The WebSocket server URL used by this session\n   */\n  getServerUrl(): string | undefined {\n    return this.config.mentraOSWebsocketUrl;\n  }\n\n  public getHttpsServerUrl(): string | undefined {\n    if (!this.config.mentraOSWebsocketUrl) {\n      return undefined;\n    }\n    return AppSession.convertToHttps(this.config.mentraOSWebsocketUrl);\n  }\n\n  private static convertToHttps(rawUrl: string | undefined): string {\n    if (!rawUrl) return \"\";\n    // Remove ws:// or wss://\n    let url = rawUrl.replace(/^wss?:\\/\\//, \"\");\n    // Remove trailing /app-ws\n    url = url.replace(/\\/app-ws$/, \"\");\n    // Prepend https://\n    return `https://${url}`;\n  }\n\n  /**\n   * 🔍 Get default settings from the App configuration\n   * @returns Array of settings with default values\n   * @throws Error if configuration is not loaded\n   */\n  getDefaultSettings(): AppSettings {\n    if (!this.appConfig) {\n      throw new Error(\"App configuration not loaded. Call loadConfigFromJson first.\");\n    }\n\n    return this.appConfig.settings\n      .filter((s: AppSetting | { type: \"group\"; title: string }): s is AppSetting => s.type !== \"group\")\n      .map((s: AppSetting) => ({\n        ...s,\n        value: s.defaultValue, // Set value to defaultValue\n      }));\n  }\n\n  /**\n   * 🔍 Get setting schema from configuration\n   * @param key Setting key to look up\n   * @returns The setting schema or undefined if not found\n   */\n  getSettingSchema(key: string): AppSetting | undefined {\n    if (!this.appConfig) return undefined;\n\n    const setting = this.appConfig.settings.find(\n      (s: AppSetting | { type: \"group\"; title: string }) => s.type !== \"group\" && \"key\" in s && s.key === key,\n    );\n\n    return setting as AppSetting | undefined;\n  }\n\n  /**\n   * 📡 Get WiFi connection status of glasses\n   * @returns WiFi status object or null if glasses don't support WiFi or status not available\n   */\n  getWifiStatus(): { connected: boolean; ssid?: string | null } | null {\n    if (!this.capabilities?.hasWifi) {\n      return null;\n    }\n    return this.glassesConnectionState?.wifi || null;\n  }\n\n  /**\n   * ✅ Check if glasses are connected to WiFi\n   * @returns true if connected to WiFi, false otherwise\n   */\n  isWifiConnected(): boolean {\n    return this.getWifiStatus()?.connected === true;\n  }\n\n  /**\n   * 🌐 Request WiFi setup from mobile app\n   * Triggers a popup on the mobile app that allows user to set up WiFi on glasses\n   * @param reason Optional reason message to display to the user\n   */\n  requestWifiSetup(reason?: string): void {\n    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n      throw new Error(\"Not connected to MentraOS Cloud\");\n    }\n\n    const message: RequestWifiSetup = {\n      type: AppToCloudMessageType.REQUEST_WIFI_SETUP,\n      packageName: this.config.packageName,\n      sessionId: this.sessionId || \"\",\n      reason,\n      timestamp: new Date(),\n    };\n\n    this.send(message);\n  }\n\n  /**\n   * 👂 Listen for glasses connection state changes (includes WiFi status)\n   * @param handler Callback function to handle connection state updates\n   * @returns Cleanup function to remove the listener\n   */\n  onGlassesConnectionState(handler: (state: any) => void): () => void {\n    return this.events.on(StreamType.GLASSES_CONNECTION_STATE, handler);\n  }\n\n  // =====================================\n  // 🔧 Private Methods\n  // =====================================\n\n  /**\n   * 📨 Handle incoming messages from cloud\n   */\n  private handleMessage(message: CloudToAppMessage): void {\n    try {\n      // Validate message before processing\n      if (!this.validateMessage(message)) {\n        this.events.emit(\"error\", new Error(\"Invalid message format received\"));\n        return;\n      }\n\n      // Handle binary data (audio or video)\n      if (message instanceof ArrayBuffer) {\n        this.handleBinaryMessage(message);\n        return;\n      }\n\n      // Using type guards to determine message type and safely handle each case\n      try {\n        if (isAppConnectionAck(message)) {\n          // Get settings from connection acknowledgment\n          const receivedSettings = message.settings || [];\n          this.settingsData = receivedSettings;\n\n          // Store config if provided\n          if (message.config && validateAppConfig(message.config)) {\n            this.appConfig = message.config;\n          }\n\n          // Use default settings from config if no settings were provided\n          if (receivedSettings.length === 0 && this.appConfig) {\n            try {\n              this.settingsData = this.getDefaultSettings();\n            } catch (error) {\n              this.logger.warn(error, \"Failed to load default settings from config:\");\n            }\n          }\n\n          // Update the settings manager with the new settings\n          this.settings.updateSettings(this.settingsData);\n\n          // Handle MentraOS system settings if provided\n          this.logger.debug(\n            { mentraosSettings: JSON.stringify(message.mentraosSettings) },\n            `[AppSession] CONNECTION_ACK mentraosSettings}`,\n          );\n          if (message.mentraosSettings) {\n            this.logger.info(\n              { mentraosSettings: JSON.stringify(message.mentraosSettings) },\n              `[AppSession] Calling updatementraosSettings with`,\n            );\n            this.settings.updateMentraosSettings(message.mentraosSettings);\n          } else {\n            this.logger.warn(`[AppSession] CONNECTION_ACK message missing mentraosSettings field`);\n          }\n\n          // Handle device capabilities if provided\n          if (message.capabilities) {\n            this.capabilities = message.capabilities;\n            this.logger.info(`[AppSession] Device capabilities loaded for model: ${message.capabilities.modelName}`);\n          } else {\n            this.logger.debug(`[AppSession] No capabilities provided in CONNECTION_ACK`);\n          }\n\n          // Emit connected event with settings\n          this.events.emit(\"connected\", this.settingsData);\n\n          // Log once to confirm Bug 007 fix is active (subscriptions derived from handlers)\n          const handlerCount = this.events.getRegisteredStreams().length;\n          this.logger.info(\n            { patch: SDK_SUBSCRIPTION_PATCH, handlerCount },\n            `[AppSession] 🔧 SDK Patch Active: ${SDK_SUBSCRIPTION_PATCH} - Subscriptions derived from ${handlerCount} handler(s)`,\n          );\n\n          // Update subscriptions (normal flow)\n          this.updateSubscriptions();\n\n          // If settings-based subscriptions are enabled, update those too\n          if (this.shouldUpdateSubscriptionsOnSettingsChange && this.settingsData.length > 0) {\n            this.updateSubscriptionsFromSettings();\n          }\n        } else if (isAppConnectionError(message) || message.type === \"connection_error\") {\n          // Handle both App-specific connection_error and standard connection_error\n          const errorMessage = message.message || \"Unknown connection error\";\n          this.events.emit(\"error\", new Error(errorMessage));\n        } else if (message.type === StreamType.AUDIO_CHUNK) {\n          // Check if we have a handler registered for AUDIO_CHUNK (derived from handlers)\n          const hasAudioHandler = this.events.getRegisteredStreams().includes(StreamType.AUDIO_CHUNK);\n          if (hasAudioHandler) {\n            // Only process if we're subscribed to avoid unnecessary processing\n            this.events.emit(StreamType.AUDIO_CHUNK, message);\n          }\n        } else if (isDataStream(message) && message.streamType === StreamType.GLASSES_CONNECTION_STATE) {\n          // Store latest glasses connection state (includes WiFi info)\n          this.glassesConnectionState = message.data;\n\n          // Emit to subscribed listeners (check derived from handlers)\n          const hasGlassesStateHandler = this.events\n            .getRegisteredStreams()\n            .includes(StreamType.GLASSES_CONNECTION_STATE);\n          if (hasGlassesStateHandler) {\n            const sanitizedData = this.sanitizeEventData(\n              StreamType.GLASSES_CONNECTION_STATE,\n              message.data,\n            ) as EventData<typeof StreamType.GLASSES_CONNECTION_STATE>;\n            this.events.emit(StreamType.GLASSES_CONNECTION_STATE, sanitizedData);\n          }\n        } else if (isDataStream(message)) {\n          // Ensure streamType exists before emitting the event\n          const messageStreamType = message.streamType as ExtendedStreamType;\n          // if (message.streamType === StreamType.TRANSCRIPTION) {\n          //   const transcriptionData = message.data as TranscriptionData;\n          //   if (transcriptionData.transcribeLanguage) {\n          //     messageStreamType = createTranscriptionStream(transcriptionData.transcribeLanguage) as ExtendedStreamType;\n          //   }\n          // } else if (message.streamType === StreamType.TRANSLATION) {\n          //   const translationData = message.data as TranslationData;\n          //   if (translationData.transcribeLanguage && translationData.translateLanguage) {\n          //     messageStreamType = createTranslationStream(translationData.transcribeLanguage, translationData.translateLanguage) as ExtendedStreamType;\n          //   }\n          // }\n\n          // Check if we have a handler registered for this stream type (derived from handlers)\n          const hasHandler = this.events.getRegisteredStreams().includes(messageStreamType);\n          if (messageStreamType && hasHandler) {\n            const sanitizedData = this.sanitizeEventData(messageStreamType, message.data) as EventData<\n              typeof messageStreamType\n            >;\n            this.events.emit(messageStreamType, sanitizedData);\n          }\n        } else if (isRtmpStreamStatus(message)) {\n          // Emit as a standard stream event if subscribed (check derived from handlers)\n          const hasRtmpHandler = this.events.getRegisteredStreams().includes(StreamType.RTMP_STREAM_STATUS);\n          if (hasRtmpHandler) {\n            this.events.emit(StreamType.RTMP_STREAM_STATUS, message);\n          }\n\n          // Update camera module's internal stream state\n          this.camera.updateStreamState(message);\n        } else if (isManagedStreamStatus(message)) {\n          // Emit as a standard stream event if subscribed (check derived from handlers)\n          const hasManagedStreamHandler = this.events.getRegisteredStreams().includes(StreamType.MANAGED_STREAM_STATUS);\n          if (hasManagedStreamHandler) {\n            this.events.emit(StreamType.MANAGED_STREAM_STATUS, message);\n          }\n\n          // Update camera module's managed stream state\n          this.camera.handleManagedStreamStatus(message);\n        } else if (isStreamStatusCheckResponse(message)) {\n          // Handle stream status check response\n          // This is a direct response, not a subscription-based event\n          this.camera.handleStreamCheckResponse(message);\n        } else if (isSettingsUpdate(message)) {\n          // Store previous settings to check for changes\n          const _prevSettings = [...this.settingsData];\n\n          // Update internal settings storage\n          this.settingsData = message.settings || [];\n\n          // Update the settings manager with the new settings\n          const changes = this.settings.updateSettings(this.settingsData);\n\n          // Emit settings update event (for backwards compatibility)\n          this.events.emit(\"settings_update\", this.settingsData);\n\n          // --- MentraOS settings update logic ---\n          // If the message.settings looks like MentraOS settings (object with known keys), update mentraosSettings\n          if (message.settings && typeof message.settings === \"object\") {\n            this.settings.updateMentraosSettings(message.settings);\n          }\n\n          // Check if we should update subscriptions\n          if (this.shouldUpdateSubscriptionsOnSettingsChange) {\n            // Check if any subscription trigger settings changed\n            const shouldUpdateSubs = this.subscriptionUpdateTriggers.some((key) => {\n              return key in changes;\n            });\n\n            if (shouldUpdateSubs) {\n              this.updateSubscriptionsFromSettings();\n            }\n          }\n        } else if (isCapabilitiesUpdate(message)) {\n          // Update device capabilities\n          const capabilitiesMessage = message as CapabilitiesUpdate;\n          this.capabilities = capabilitiesMessage.capabilities;\n          this.logger.info(\n            capabilitiesMessage.capabilities,\n            `[AppSession] Capabilities updated for model: ${capabilitiesMessage.modelName}`,\n          );\n\n          // Emit capabilities update event for applications to handle\n          this.events.emit(\"capabilities_update\", {\n            capabilities: capabilitiesMessage.capabilities,\n            modelName: capabilitiesMessage.modelName,\n            timestamp: capabilitiesMessage.timestamp,\n          });\n        } else if (isDeviceStateUpdate(message)) {\n          // Update device state observables\n          this.device.state.updateFromMessage(message.state);\n\n          this.logger.debug(\n            {\n              changedFields: Object.keys(message.state),\n              fullSnapshot: message.fullSnapshot,\n            },\n            `[AppSession] Device state updated via WebSocket`,\n          );\n        } else if (isAppStopped(message)) {\n          const reason = message.reason || \"unknown\";\n          const displayReason = `App stopped: ${reason}`;\n\n          // Don't emit disconnected event here - let the WebSocket close handler do it\n          // This prevents duplicate disconnected events when the session is disposed\n          this.logger.info(`📤 [${this.config.packageName}] Received APP_STOPPED message: ${displayReason}`);\n\n          // Clear reconnection state\n          this.reconnectAttempts = 0;\n        }\n        // Handle dashboard mode changes\n        else if (isDashboardModeChanged(message)) {\n          try {\n            // Use proper type\n            const mode = message.mode || \"none\";\n\n            // Update dashboard state in the API\n            if (this.dashboard && \"content\" in this.dashboard) {\n              (this.dashboard.content as any).setCurrentMode(mode);\n            }\n          } catch (error) {\n            this.logger.error(error, \"Error handling dashboard mode change\");\n          }\n        }\n        // Handle always-on dashboard state changes\n        else if (isDashboardAlwaysOnChanged(message)) {\n          try {\n            // Use proper type\n            const enabled = !!message.enabled;\n\n            // Update dashboard state in the API\n            if (this.dashboard && \"content\" in this.dashboard) {\n              (this.dashboard.content as any).setAlwaysOnEnabled(enabled);\n            }\n          } catch (error) {\n            this.logger.error(error, \"Error handling dashboard always-on change\");\n          }\n        }\n        // Handle custom messages\n        else if (message.type === CloudToAppMessageType.CUSTOM_MESSAGE) {\n          this.events.emit(\"custom_message\", message);\n          return;\n        }\n        // Handle App-to-App communication messages\n        else if ((message as any).type === \"app_message_received\") {\n          this.appEvents.emit(\"app_message_received\", message as any);\n        } else if ((message as any).type === \"app_user_joined\") {\n          this.appEvents.emit(\"app_user_joined\", message as any);\n        } else if ((message as any).type === \"app_user_left\") {\n          this.appEvents.emit(\"app_user_left\", message as any);\n        } else if ((message as any).type === \"app_room_updated\") {\n          this.appEvents.emit(\"app_room_updated\", message as any);\n        } else if ((message as any).type === \"app_direct_message_response\") {\n          const response = message as any;\n          if (response.messageId && this.pendingDirectMessages.has(response.messageId)) {\n            const { resolve } = this.pendingDirectMessages.get(response.messageId)!;\n            resolve(response.success);\n            this.pendingDirectMessages.delete(response.messageId);\n          }\n        } else if (message.type === \"augmentos_settings_update\") {\n          const mentraosMsg = message as MentraosSettingsUpdate;\n          if (mentraosMsg.settings && typeof mentraosMsg.settings === \"object\") {\n            this.settings.updateMentraosSettings(mentraosMsg.settings);\n          }\n        }\n        // Handle 'connection_error' as a specific case if cloud sends this string literal\n        else if ((message as any).type === \"connection_error\") {\n          // Treat 'connection_error' (string literal) like AppConnectionError\n          // This handles cases where the cloud might send the type as a direct string\n          // instead of the enum's 'tpa_connection_error' value.\n          const errorMessage = (message as any).message || \"Unknown connection error (type: connection_error)\";\n          this.logger.warn(\n            `Received 'connection_error' type directly. Consider aligning cloud to send 'tpa_connection_error'. Message: ${errorMessage}`,\n          );\n          this.events.emit(\"error\", new Error(errorMessage));\n        } else if (message.type === \"permission_error\") {\n          // Handle permission errors from cloud\n          this.logger.warn(\n            {\n              message: message.message,\n              details: message.details,\n              detailsCount: message.details?.length || 0,\n              rejectedStreams: message.details?.map((d) => d.stream) || [],\n            },\n            \"Permission error received:\",\n          );\n\n          // Emit permission error event for application handling\n          this.events.emit(\"permission_error\", {\n            message: message.message,\n            details: message.details,\n            timestamp: message.timestamp,\n          });\n\n          // Optionally emit individual permission denied events for each stream\n          message.details?.forEach((detail) => {\n            this.events.emit(\"permission_denied\", {\n              stream: detail.stream,\n              requiredPermission: detail.requiredPermission,\n              message: detail.message,\n            });\n          });\n        } else if (isAudioPlayResponse(message)) {\n          // Delegate audio play response handling to the audio module\n          if (this.audio) {\n            this.audio.handleAudioPlayResponse(message as AudioPlayResponse);\n          }\n        } else if (isPhotoResponse(message)) {\n          // Legacy photo response handling - now photos come directly via webhook\n          // This branch can be removed in the future as all photos now go through /photo-upload\n          this.logger.warn(\n            { message },\n            \"Received legacy photo response - photos should now come via /photo-upload webhook\",\n          );\n        } else if (isRgbLedControlResponse(message)) {\n          // LED control responses are no longer handled - fire-and-forget mode\n          this.logger.debug({ message }, \"Received LED control response (ignored - fire-and-forget mode)\");\n        }\n        // Handle unrecognized message types gracefully\n        else {\n          this.logger.warn(`Unrecognized message type: ${(message as any).type}`);\n          this.events.emit(\"error\", new Error(`Unrecognized message type: ${(message as any).type}`));\n        }\n      } catch (processingError: unknown) {\n        // Catch any errors during message processing to prevent App crashes\n        this.logger.error(processingError, \"Error processing message:\");\n        const errorMessage = processingError instanceof Error ? processingError.message : String(processingError);\n        this.events.emit(\"error\", new Error(`Error processing message: ${errorMessage}`));\n      }\n    } catch (error: unknown) {\n      // Final safety net to ensure the App doesn't crash on any unexpected errors\n      this.logger.error(error, \"Unexpected error in message handler\");\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.events.emit(\"error\", new Error(`Unexpected error in message handler: ${errorMessage}`));\n    }\n  }\n\n  /**\n   * 🧪 Validate incoming message structure\n   * @param message - Message to validate\n   * @returns boolean indicating if the message is valid\n   */\n  private validateMessage(message: CloudToAppMessage): boolean {\n    // Handle ArrayBuffer case separately\n    if (message instanceof ArrayBuffer) {\n      return true; // ArrayBuffers are always considered valid at this level\n    }\n\n    // Check if message is null or undefined\n    if (!message) {\n      return false;\n    }\n\n    // Check if message has a type property\n    if (!(\"type\" in message)) {\n      return false;\n    }\n\n    // All other message types should be objects with a type property\n    return true;\n  }\n\n  /**\n   * 📦 Handle binary message data (audio or video)\n   * @param buffer - Binary data as ArrayBuffer\n   */\n  private handleBinaryMessage(buffer: ArrayBuffer): void {\n    try {\n      // Safety check - only process if we have a handler registered (derived from handlers)\n      const hasAudioHandler = this.events.getRegisteredStreams().includes(StreamType.AUDIO_CHUNK);\n      if (!hasAudioHandler) {\n        return;\n      }\n\n      // Validate buffer has content before processing\n      if (!buffer || buffer.byteLength === 0) {\n        this.events.emit(\"error\", new Error(\"Received empty binary message\"));\n        return;\n      }\n\n      // Create a safety wrapped audio chunk with proper defaults\n      const audioChunk: AudioChunk = {\n        type: StreamType.AUDIO_CHUNK,\n        timestamp: new Date(),\n        arrayBuffer: buffer,\n        sampleRate: 16000, // Default sample rate\n      };\n\n      // Emit to subscribers\n      this.events.emit(StreamType.AUDIO_CHUNK, audioChunk);\n    } catch (error: unknown) {\n      this.logger.error(error, \"Error processing binary message\");\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.events.emit(\"error\", new Error(`Error processing binary message: ${errorMessage}`));\n    }\n  }\n\n  /**\n   * 🧹 Sanitize event data to prevent crashes from malformed data\n   * @param streamType - The type of stream data\n   * @param data - The potentially unsafe data to sanitize\n   * @returns Sanitized data safe for processing\n   */\n  private sanitizeEventData(streamType: ExtendedStreamType, data: unknown): any {\n    try {\n      // If data is null or undefined, return an empty object to prevent crashes\n      if (data === null || data === undefined) {\n        return {};\n      }\n\n      // For specific stream types, perform targeted sanitization\n      switch (streamType) {\n        case StreamType.TRANSCRIPTION: {\n          // Ensure text field exists and is a string\n          if (typeof (data as TranscriptionData).text !== \"string\") {\n            return {\n              text: \"\",\n              isFinal: true,\n              startTime: Date.now(),\n              endTime: Date.now(),\n            };\n          }\n          break;\n        }\n\n        case StreamType.HEAD_POSITION: {\n          // Ensure position data has required numeric fields\n          // Handle HeadPosition - Note the property position instead of x,y,z\n          const pos = data as any;\n          if (typeof pos?.position !== \"string\") {\n            return { position: \"up\", timestamp: new Date() };\n          }\n          break;\n        }\n\n        case StreamType.BUTTON_PRESS: {\n          // Ensure button type is valid\n          const btn = data as any;\n          if (!btn.buttonId || !btn.pressType) {\n            return {\n              buttonId: \"unknown\",\n              pressType: \"short\",\n              timestamp: new Date(),\n            };\n          }\n          break;\n        }\n      }\n\n      return data;\n    } catch (error: unknown) {\n      this.logger.error(error, `Error sanitizing ${streamType} data`);\n      // Return a safe empty object if something goes wrong\n      return {};\n    }\n  }\n\n  /**\n   * 🔐 Send connection initialization message\n   */\n  private sendConnectionInit(): void {\n    const message: AppConnectionInit = {\n      type: AppToCloudMessageType.CONNECTION_INIT,\n      sessionId: this.sessionId!,\n      packageName: this.config.packageName,\n      apiKey: this.config.apiKey,\n      timestamp: new Date(),\n    };\n    this.send(message);\n  }\n\n  /**\n   * 📝 Update subscription list with cloud\n   */\n  private updateSubscriptions(): void {\n    // CRITICAL FIX (Bug 007): Derive subscriptions from EventManager.handlers\n    // This ensures subscriptions can NEVER be empty if handlers exist\n    // Previously, this.subscriptions could drift out of sync with handlers\n    // See: cloud/issues/006-captions-and-apps-stopping/011-sdk-subscription-architecture-mismatch.md\n    const derivedSubscriptions = this.events.getRegisteredStreams();\n\n    this.logger.info(\n      { subscriptions: JSON.stringify(derivedSubscriptions) },\n      `[AppSession] updateSubscriptions: sending ${derivedSubscriptions.length} subscriptions to cloud (derived from handlers)`,\n    );\n\n    // Build the array of SubscriptionRequest objects to send to the cloud\n    const subscriptionPayload: SubscriptionRequest[] = derivedSubscriptions.map((stream) => {\n      const rate = this.streamRates.get(stream);\n      if (rate && stream === StreamType.LOCATION_STREAM) {\n        return { stream: \"location_stream\", rate: rate as any };\n      }\n      return stream;\n    });\n\n    const message: AppSubscriptionUpdate = {\n      type: AppToCloudMessageType.SUBSCRIPTION_UPDATE,\n      packageName: this.config.packageName,\n      subscriptions: subscriptionPayload,\n      sessionId: this.sessionId!,\n      timestamp: new Date(),\n    };\n    this.send(message);\n  }\n\n  /**\n   * 🔄 Handle reconnection with exponential backoff\n   */\n  private async handleReconnection(): Promise<void> {\n    // Check if session was terminated (e.g., \"User session ended\")\n    if (this.terminated) {\n      this.logger.info(\n        `🔄 Reconnection skipped: session was terminated (User session ended). ` +\n          `If cloud restarts app, onSession will be called with fresh handlers.`,\n      );\n      return;\n    }\n\n    // Check if reconnection is allowed\n    if (!this.config.autoReconnect || !this.sessionId) {\n      this.logger.debug(\n        `🔄 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${\n          this.sessionId ? \"valid\" : \"invalid\"\n        }`,\n      );\n      return;\n    }\n\n    // Check if we've exceeded the maximum attempts\n    const maxAttempts = this.config.maxReconnectAttempts || 3;\n    if (this.reconnectAttempts >= maxAttempts) {\n      this.logger.info(`🔄 Maximum reconnection attempts (${maxAttempts}) reached, giving up`);\n\n      // Emit a permanent disconnection event to trigger onStop in the App server\n      this.events.emit(\"disconnected\", {\n        message: `Connection permanently lost after ${maxAttempts} failed reconnection attempts`,\n        code: 4000, // Custom code for max reconnection attempts exhausted\n        reason: \"Maximum reconnection attempts exceeded\",\n        wasClean: false,\n        permanent: true, // Flag this as a permanent disconnection\n      });\n\n      return;\n    }\n\n    // Calculate delay with exponential backoff\n    const baseDelay = this.config.reconnectDelay || 1000;\n    const delay = baseDelay * Math.pow(2, this.reconnectAttempts);\n    this.reconnectAttempts++;\n\n    this.logger.debug(\n      `🔄 [${this.config.packageName}] Reconnection attempt ${this.reconnectAttempts}/${maxAttempts} in ${delay}ms`,\n    );\n\n    // Use the resource tracker for the timeout\n    await new Promise<void>((resolve) => {\n      this.resources.setTimeout(() => resolve(), delay);\n    });\n\n    try {\n      this.logger.debug(`🔄 [${this.config.packageName}] Attempting to reconnect...`);\n      await this.connect(this.sessionId);\n      this.logger.debug(`✅ [${this.config.packageName}] Reconnection successful!`);\n      this.reconnectAttempts = 0;\n    } catch (error) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.logger.error(error, `❌ [${this.config.packageName}] Reconnection failed for user ${this.userId}`);\n      this.events.emit(\"error\", new Error(`Reconnection failed: ${errorMessage}`));\n\n      // Check if this was the last attempt\n      if (this.reconnectAttempts >= maxAttempts) {\n        this.logger.debug(\n          `🔄 [${this.config.packageName}] Final reconnection attempt failed, emitting permanent disconnection`,\n        );\n\n        // Emit permanent disconnection event after the last failed attempt\n        this.events.emit(\"disconnected\", {\n          message: `Connection permanently lost after ${maxAttempts} failed reconnection attempts`,\n          code: 4000, // Custom code for max reconnection attempts exhausted\n          reason: \"Maximum reconnection attempts exceeded\",\n          wasClean: false,\n          permanent: true, // Flag this as a permanent disconnection\n        });\n      }\n    }\n  }\n\n  /**\n   * 📤 Public API for modules to send messages\n   * Always uses current WebSocket connection\n   */\n  public sendMessage(message: AppToCloudMessage): void {\n    return this.send(message);\n  }\n\n  /**\n   * 📤 Send message to cloud with validation and error handling\n   * @throws {Error} If WebSocket is not connected\n   */\n  private send(message: AppToCloudMessage): void {\n    try {\n      // Verify WebSocket connection is valid\n      if (!this.ws) {\n        throw new Error(\"WebSocket connection not established\");\n      }\n\n      if (this.ws.readyState !== 1) {\n        const stateMap: Record<number, string> = {\n          0: \"CONNECTING\",\n          1: \"OPEN\",\n          2: \"CLOSING\",\n          3: \"CLOSED\",\n        };\n        const stateName = stateMap[this.ws.readyState] || \"UNKNOWN\";\n        throw new Error(`WebSocket not connected (current state: ${stateName})`);\n      }\n\n      // Validate message before sending\n      if (!message || typeof message !== \"object\") {\n        throw new Error(\"Invalid message: must be an object\");\n      }\n\n      if (!(\"type\" in message)) {\n        throw new Error('Invalid message: missing \"type\" property');\n      }\n\n      // Ensure message format is consistent\n      if (!(\"timestamp\" in message) || !(message.timestamp instanceof Date)) {\n        message.timestamp = new Date();\n      }\n\n      // Try to send with error handling\n      try {\n        const serializedMessage = JSON.stringify(message);\n        this.ws.send(serializedMessage);\n      } catch (sendError: unknown) {\n        const errorMessage = sendError instanceof Error ? sendError.message : String(sendError);\n        throw new Error(`Failed to send message: ${errorMessage}`);\n      }\n    } catch (error: unknown) {\n      // Check if this is an expected disconnection error (not a real error)\n      const isDisconnectError =\n        error instanceof Error &&\n        (error.message.includes(\"WebSocket not connected\") ||\n          error.message.includes(\"CLOSED\") ||\n          error.message.includes(\"CLOSING\"));\n\n      if (isDisconnectError) {\n        // Don't log as error - this is expected when user disconnects\n        // Apps should handle this gracefully by checking session.isConnected\n        this.logger.debug(error, \"Message send skipped - session disconnected\");\n      } else {\n        // This is an actual error that needs attention\n        this.logger.error(error, \"Message send error\");\n      }\n\n      // Ensure we always emit an Error object\n      if (error instanceof Error) {\n        this.events.emit(\"error\", error);\n      } else {\n        this.events.emit(\"error\", new Error(String(error)));\n      }\n\n      // Re-throw to maintain the original function behavior\n      throw error;\n    }\n  }\n\n  /**\n   * Fetch the onboarding instructions for this session from the backend.\n   * @returns Promise resolving to the instructions string or null\n   */\n  public async getInstructions(): Promise<string | null> {\n    try {\n      const baseUrl = this.getServerUrl();\n      const response = await axios.get(`${baseUrl}/api/instructions`, {\n        params: { userId: this.userId },\n      });\n      return response.data.instructions || null;\n    } catch (err) {\n      this.logger.error(err, `Error fetching instructions from backend`);\n      return null;\n    }\n  }\n  // =====================================\n  // 👥 App-to-App Communication Interface\n  // =====================================\n\n  /**\n   * 👥 Discover other users currently using the same App\n   * @param includeProfiles - Whether to include user profile information\n   * @returns Promise that resolves with list of active users\n   */\n  async discoverAppUsers(domain: string, includeProfiles = false): Promise<any> {\n    // Use the domain argument as the base URL if provided\n    if (!domain) {\n      throw new Error(\"Domain (API base URL) is required for user discovery\");\n    }\n    const url = `${domain}/api/app-communication/discover-users`;\n    // Use the user's core token for authentication\n    const appApiKey = this.config.apiKey; // This may need to be updated if you store the core token elsewhere\n\n    if (!appApiKey) {\n      throw new Error(\"Core token (apiKey) is required for user discovery\");\n    }\n    const body = {\n      packageName: this.config.packageName,\n      userId: this.userId,\n      includeUserProfiles: includeProfiles,\n    };\n    const response = await fetch(url, {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${appApiKey}`,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(body),\n    });\n    if (!response.ok) {\n      const errorText = await response.text();\n      throw new Error(`Failed to discover users: ${response.status} ${response.statusText} - ${errorText}`);\n    }\n    return await response.json();\n  }\n\n  /**\n   * 🔍 Check if a specific user is currently active\n   * @param userId - User ID to check for\n   * @returns Promise that resolves with boolean indicating if user is active\n   */\n  async isUserActive(userId: string): Promise<boolean> {\n    try {\n      const userList = await this.discoverAppUsers(\"\", false);\n      return userList.users.some((user: any) => user.userId === userId);\n    } catch (error) {\n      this.logger.error({ error, userId }, \"Error checking if user is active\");\n      return false;\n    }\n  }\n\n  /**\n   * 📊 Get user count for this App\n   * @returns Promise that resolves with number of active users\n   */\n  async getUserCount(domain: string): Promise<number> {\n    try {\n      const userList = await this.discoverAppUsers(domain, false);\n      return userList.totalUsers;\n    } catch (error) {\n      this.logger.error(error, \"Error getting user count\");\n      return 0;\n    }\n  }\n\n  /**\n   * 📢 Send broadcast message to all users with same App active\n   * @param payload - Message payload to send\n   * @param roomId - Optional room ID for room-based messaging\n   * @returns Promise that resolves when message is sent\n   */\n  async broadcastToAppUsers(payload: any, _roomId?: string): Promise<void> {\n    try {\n      const messageId = this.generateMessageId();\n\n      const message = {\n        type: \"app_broadcast_message\",\n        packageName: this.config.packageName,\n        sessionId: this.sessionId!,\n        payload,\n        messageId,\n        senderUserId: this.userId,\n        timestamp: new Date(),\n      };\n\n      this.send(message as any);\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      throw new Error(`Failed to broadcast message: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 📤 Send direct message to specific user\n   * @param targetUserId - User ID to send message to\n   * @param payload - Message payload to send\n   * @returns Promise that resolves with success status\n   */\n  async sendDirectMessage(targetUserId: string, payload: any): Promise<boolean> {\n    return new Promise((resolve, reject) => {\n      try {\n        const messageId = this.generateMessageId();\n\n        // Store promise resolver\n        this.pendingDirectMessages.set(messageId, { resolve, reject });\n\n        const message = {\n          type: \"app_direct_message\",\n          packageName: this.config.packageName,\n          sessionId: this.sessionId!,\n          targetUserId,\n          payload,\n          messageId,\n          senderUserId: this.userId,\n          timestamp: new Date(),\n        };\n\n        this.send(message as any);\n\n        // Set timeout to avoid hanging promises\n        const timeoutMs = 15000; // 15 seconds\n        this.resources.setTimeout(() => {\n          if (this.pendingDirectMessages.has(messageId)) {\n            this.pendingDirectMessages.get(messageId)!.reject(new Error(\"Direct message timed out\"));\n            this.pendingDirectMessages.delete(messageId);\n          }\n        }, timeoutMs);\n      } catch (error: unknown) {\n        const errorMessage = error instanceof Error ? error.message : String(error);\n        reject(new Error(`Failed to send direct message: ${errorMessage}`));\n      }\n    });\n  }\n\n  /**\n   * 🏠 Join a communication room for group messaging\n   * @param roomId - Room ID to join\n   * @param roomConfig - Optional room configuration\n   * @returns Promise that resolves when room is joined\n   */\n  async joinAppRoom(\n    roomId: string,\n    roomConfig?: {\n      maxUsers?: number;\n      isPrivate?: boolean;\n      metadata?: any;\n    },\n  ): Promise<void> {\n    try {\n      const message = {\n        type: \"app_room_join\",\n        packageName: this.config.packageName,\n        sessionId: this.sessionId!,\n        roomId,\n        roomConfig,\n        timestamp: new Date(),\n      };\n\n      this.send(message as any);\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      throw new Error(`Failed to join room: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 🚪 Leave a communication room\n   * @param roomId - Room ID to leave\n   * @returns Promise that resolves when room is left\n   */\n  async leaveAppRoom(roomId: string): Promise<void> {\n    try {\n      const message = {\n        type: \"app_room_leave\",\n        packageName: this.config.packageName,\n        sessionId: this.sessionId!,\n        roomId,\n        timestamp: new Date(),\n      };\n\n      this.send(message as any);\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      throw new Error(`Failed to leave room: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 📨 Listen for messages from other App users\n   * @param handler - Function to handle incoming messages\n   * @returns Cleanup function to remove the handler\n   */\n  onAppMessage(handler: (message: any) => void): () => void {\n    this.appEvents.on(\"app_message_received\", handler);\n    return () => this.appEvents.off(\"app_message_received\", handler);\n  }\n\n  /**\n   * 👋 Listen for user join events\n   * @param handler - Function to handle user join events\n   * @returns Cleanup function to remove the handler\n   */\n  onAppUserJoined(handler: (data: any) => void): () => void {\n    this.appEvents.on(\"app_user_joined\", handler);\n    return () => this.appEvents.off(\"app_user_joined\", handler);\n  }\n\n  /**\n   * 🚪 Listen for user leave events\n   * @param handler - Function to handle user leave events\n   * @returns Cleanup function to remove the handler\n   */\n  onAppUserLeft(handler: (data: any) => void): () => void {\n    this.appEvents.on(\"app_user_left\", handler);\n    return () => this.appEvents.off(\"app_user_left\", handler);\n  }\n\n  /**\n   * 🏠 Listen for room update events\n   * @param handler - Function to handle room updates\n   * @returns Cleanup function to remove the handler\n   */\n  onAppRoomUpdated(handler: (data: any) => void): () => void {\n    this.appEvents.on(\"app_room_updated\", handler);\n    return () => this.appEvents.off(\"app_room_updated\", handler);\n  }\n\n  /**\n   * 🔧 Generate unique message ID\n   * @returns Unique message identifier\n   */\n  private generateMessageId(): string {\n    return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n  }\n}\n\n/**\n * @deprecated Use `AppSessionConfig` instead. `TpaSessionConfig` is deprecated and will be removed in a future version.\n * This is an alias for backward compatibility only.\n *\n * @example\n * ```typescript\n * // ❌ Deprecated - Don't use this\n * const config: TpaSessionConfig = { ... };\n *\n * // ✅ Use this instead\n * const config: AppSessionConfig = { ... };\n * ```\n */\nexport type TpaSessionConfig = AppSessionConfig;\n\n/**\n * @deprecated Use `AppSession` instead. `TpaSession` is deprecated and will be removed in a future version.\n * This is an alias for backward compatibility only.\n *\n * @example\n * ```typescript\n * // ❌ Deprecated - Don't use this\n * const session = new TpaSession(config);\n *\n * // ✅ Use this instead\n * const session = new AppSession(config);\n * ```\n */\nexport class TpaSession extends AppSession {\n  constructor(config: TpaSessionConfig) {\n    super(config);\n    // Emit a deprecation warning to help developers migrate\n    console.warn(\n      \"⚠️  DEPRECATION WARNING: TpaSession is deprecated and will be removed in a future version. \" +\n        \"Please use AppSession instead. \" +\n        'Simply replace \"TpaSession\" with \"AppSession\" in your code.',\n    );\n  }\n}\n\n// Export module types for developers\nexport { CameraModule, PhotoRequestOptions, RtmpStreamOptions } from \"./modules/camera\";\nexport { LedModule, LedControlOptions } from \"./modules/led\";\nexport { AudioManager, AudioPlayOptions, AudioPlayResult, SpeakOptions } from \"./modules/audio\";\nexport { SimpleStorage } from \"./modules/simple-storage\";\n","/**\n * 🎮 Event Manager Module\n */\nimport EventEmitter from \"events\"\nimport {\n  StreamType,\n  ExtendedStreamType,\n  AppSettings,\n  WebSocketError,\n  // Event data types\n  ButtonPress,\n  HeadPosition,\n  PhoneNotification,\n  TranscriptionData,\n  TranslationData,\n  GlassesBatteryUpdate,\n  PhoneBatteryUpdate,\n  GlassesConnectionState,\n  LocationUpdate,\n  Vad,\n  AudioChunk,\n  CalendarEvent,\n  VpsCoordinates,\n  // Language stream helpers\n  createTranscriptionStream,\n  isValidLanguageCode,\n  createTranslationStream,\n  CustomMessage,\n  RtmpStreamStatus,\n  PhotoTaken,\n  ManagedStreamStatus,\n  PhoneNotificationDismissed,\n  Capabilities,\n  TouchEvent,\n  createTouchEventStream,\n} from \"../../types\"\nimport {DashboardMode} from \"../../types/dashboard\"\nimport {PermissionErrorDetail} from \"../../types/messages/cloud-to-app\"\nimport {calendarWarnLog, microPhoneWarnLog} from \"../../utils/permissions-utils\"\n\n/** 🎯 Type-safe event handler function */\ntype Handler<T> = (data: T) => void\n\n/** 🔄 System events not tied to streams */\ninterface SystemEvents {\n  connected: AppSettings | undefined\n  disconnected:\n    | string\n    | {\n        message: string // Human-readable close message\n        code: number // WebSocket close code (1000 = normal)\n        reason: string // Reason provided by server\n        wasClean: boolean // Whether this was a clean closure\n        permanent?: boolean // Whether this is a permanent disconnection (no more reconnection attempts)\n        sessionEnded?: boolean // Whether this disconnection is due to user session ending\n      }\n  error: WebSocketError | Error\n  settings_update: AppSettings\n  capabilities_update: {\n    capabilities: Capabilities | null\n    modelName: string | null\n    timestamp?: Date\n  }\n  dashboard_mode_change: {mode: DashboardMode | \"none\"}\n  dashboard_always_on_change: {enabled: boolean}\n  custom_message: CustomMessage\n  permission_error: {\n    message: string\n    details: PermissionErrorDetail[]\n    timestamp?: Date\n  }\n  permission_denied: {\n    stream: string\n    requiredPermission: string\n    message: string\n  }\n}\n\n/** 📡 All possible event types */\ntype EventType = ExtendedStreamType | keyof SystemEvents\n\n/** 📦 Map of stream types to their data types */\nexport interface StreamDataTypes {\n  [StreamType.BUTTON_PRESS]: ButtonPress\n  [StreamType.HEAD_POSITION]: HeadPosition\n  [StreamType.PHONE_NOTIFICATION]: PhoneNotification\n  [StreamType.TRANSCRIPTION]: TranscriptionData\n  [StreamType.TRANSLATION]: TranslationData\n  [StreamType.GLASSES_BATTERY_UPDATE]: GlassesBatteryUpdate\n  [StreamType.PHONE_BATTERY_UPDATE]: PhoneBatteryUpdate\n  [StreamType.GLASSES_CONNECTION_STATE]: GlassesConnectionState\n  [StreamType.LOCATION_UPDATE]: LocationUpdate\n  [StreamType.CALENDAR_EVENT]: CalendarEvent\n  [StreamType.VAD]: Vad\n  [StreamType.PHONE_NOTIFICATION_DISMISSED]: PhoneNotificationDismissed\n  [StreamType.AUDIO_CHUNK]: AudioChunk\n  [StreamType.VIDEO]: ArrayBuffer\n  [StreamType.RTMP_STREAM_STATUS]: RtmpStreamStatus\n  [StreamType.MANAGED_STREAM_STATUS]: ManagedStreamStatus\n  [StreamType.VPS_COORDINATES]: VpsCoordinates\n  [StreamType.PHOTO_TAKEN]: PhotoTaken\n  [StreamType.OPEN_DASHBOARD]: never\n  [StreamType.START_APP]: never\n  [StreamType.STOP_APP]: never\n  [StreamType.ALL]: never\n  [StreamType.WILDCARD]: never\n}\n\n/** 📦 Data type for an event */\nexport type EventData<T extends EventType> = T extends keyof StreamDataTypes\n  ? StreamDataTypes[T]\n  : T extends keyof SystemEvents\n    ? SystemEvents[T]\n    : T extends string\n      ? T extends `${StreamType.TRANSCRIPTION}:${string}`\n        ? TranscriptionData\n        : T extends `${StreamType.TRANSLATION}:${string}`\n          ? TranslationData\n          : never\n      : never\n\nexport class EventManager {\n  private emitter: EventEmitter\n  private handlers: Map<EventType, Set<Handler<unknown>>>\n  private lastLanguageTranscriptioCleanupHandler: () => void\n  private lastLanguageTranslationCleanupHandler: () => void\n\n  constructor(\n    private subscribe: (type: ExtendedStreamType) => void,\n    private unsubscribe: (type: ExtendedStreamType) => void,\n    private packageName: string,\n    private baseUrl: string,\n  ) {\n    this.emitter = new EventEmitter()\n    this.handlers = new Map()\n    this.lastLanguageTranscriptioCleanupHandler = () => {}\n    this.lastLanguageTranslationCleanupHandler = () => {}\n  }\n\n  // Convenience handlers for common event types\n\n  onTranscription(handler: Handler<TranscriptionData>) {\n    // Only make the API call if we have a base URL (server-side environment)\n    microPhoneWarnLog(this.baseUrl, this.packageName, this.onTranscription.name)\n\n    return this.addHandler(createTranscriptionStream(\"en-US\"), handler)\n  }\n\n  /**\n   * 🎤 Listen for transcription events in a specific language\n   * @param language - Language code (e.g., \"en-US\") or \"auto\" for automatic detection\n   * @param handler - Function to handle transcription data\n   * @param optionsOrBoolean - Optional configuration object or boolean (backward compatible)\n   * @param optionsOrBoolean.disableLanguageIdentification - Disable language identification (defaults to false/enabled)\n   * @param optionsOrBoolean.hints - Array of language code hints to improve detection (e.g., [\"es\", \"fr\"])\n   * @returns Cleanup function to remove the handler\n   * @throws Error if language code is invalid\n   */\n  onTranscriptionForLanguage(\n    language: string,\n    handler: Handler<TranscriptionData>,\n    optionsOrBoolean?:\n      | boolean\n      | {\n          disableLanguageIdentification?: boolean\n          hints?: string[]\n        },\n  ): () => void {\n    if (language !== \"auto\" && !isValidLanguageCode(language)) {\n      throw new Error(`Invalid language code: ${language}`)\n    }\n    this.lastLanguageTranscriptioCleanupHandler()\n\n    // Handle backward compatibility: boolean or options object\n    const options =\n      typeof optionsOrBoolean === \"boolean\" ? {disableLanguageIdentification: optionsOrBoolean} : optionsOrBoolean\n\n    const streamType = createTranscriptionStream(language, options)\n    this.lastLanguageTranscriptioCleanupHandler = this.addHandler(streamType, handler)\n    return this.lastLanguageTranscriptioCleanupHandler\n  }\n\n  /**\n   * 🌐 Listen for translation events for a specific language pair\n   * @param sourceLanguage - Source language code (e.g., \"es-ES\")\n   * @param targetLanguage - Target language code (e.g., \"en-US\")\n   * @param handler - Function to handle translation data\n   * @returns Cleanup function to remove the handler\n   * @throws Error if language codes are invalid\n   */\n  ontranslationForLanguage(\n    sourceLanguage: string,\n    targetLanguage: string,\n    handler: Handler<TranslationData>,\n  ): () => void {\n    microPhoneWarnLog(this.baseUrl || \"\", this.packageName, this.ontranslationForLanguage.name)\n    if (!isValidLanguageCode(sourceLanguage)) {\n      throw new Error(`Invalid source language code: ${sourceLanguage}`)\n    }\n    if (!isValidLanguageCode(targetLanguage)) {\n      throw new Error(`Invalid target language code: ${targetLanguage}`)\n    }\n\n    this.lastLanguageTranslationCleanupHandler()\n    const streamType = createTranslationStream(sourceLanguage, targetLanguage)\n    this.lastLanguageTranslationCleanupHandler = this.addHandler(streamType, handler)\n\n    return this.lastLanguageTranslationCleanupHandler\n  }\n\n  onHeadPosition(handler: Handler<HeadPosition>) {\n    return this.addHandler(StreamType.HEAD_POSITION, handler)\n  }\n\n  onButtonPress(handler: Handler<ButtonPress>) {\n    return this.addHandler(StreamType.BUTTON_PRESS, handler)\n  }\n\n  onTouchEvent(gestureOrHandler: string | Handler<TouchEvent>, handler?: Handler<TouchEvent>): () => void {\n    // Handle both: onTouchEvent(handler) and onTouchEvent(\"forward_swipe\", handler)\n    if (typeof gestureOrHandler === \"function\") {\n      // Subscribe to all touch events\n      return this.addHandler(StreamType.TOUCH_EVENT, gestureOrHandler)\n    } else {\n      // Subscribe to specific gesture\n      const gestureStream = createTouchEventStream(gestureOrHandler)\n      return this.addHandler(gestureStream, handler!)\n    }\n  }\n\n  onPhoneNotifications(handler: Handler<PhoneNotification>) {\n    return this.addHandler(StreamType.PHONE_NOTIFICATION, handler)\n  }\n\n  onPhoneNotificationDismissed(handler: Handler<PhoneNotificationDismissed>) {\n    return this.addHandler(StreamType.PHONE_NOTIFICATION_DISMISSED, handler)\n  }\n\n  onGlassesBattery(handler: Handler<GlassesBatteryUpdate>) {\n    return this.addHandler(StreamType.GLASSES_BATTERY_UPDATE, handler)\n  }\n\n  onPhoneBattery(handler: Handler<PhoneBatteryUpdate>) {\n    return this.addHandler(StreamType.PHONE_BATTERY_UPDATE, handler)\n  }\n\n  onVoiceActivity(handler: Handler<Vad>) {\n    microPhoneWarnLog(this.baseUrl || \"\", this.packageName, this.onVoiceActivity.name)\n    return this.addHandler(StreamType.VAD, handler)\n  }\n\n  onLocation(handler: Handler<LocationUpdate>) {\n    return this.addHandler(StreamType.LOCATION_UPDATE, handler)\n  }\n\n  onCalendarEvent(handler: Handler<CalendarEvent>) {\n    return this.addHandler(StreamType.CALENDAR_EVENT, handler)\n  }\n\n  /**\n   * 🎤 Listen for audio chunk data\n   * @param handler - Function to handle audio chunks\n   * @returns Cleanup function to remove the handler\n   */\n  onAudioChunk(handler: Handler<AudioChunk>) {\n    return this.addHandler(StreamType.AUDIO_CHUNK, handler)\n  }\n\n  // System event handlers\n\n  onConnected(handler: Handler<SystemEvents[\"connected\"]>) {\n    this.emitter.on(\"connected\", handler)\n    return () => this.emitter.off(\"connected\", handler)\n  }\n\n  onDisconnected(handler: Handler<SystemEvents[\"disconnected\"]>) {\n    this.emitter.on(\"disconnected\", handler)\n    return () => this.emitter.off(\"disconnected\", handler)\n  }\n\n  onError(handler: Handler<SystemEvents[\"error\"]>) {\n    this.emitter.on(\"error\", handler)\n    return () => this.emitter.off(\"error\", handler)\n  }\n\n  onSettingsUpdate(handler: Handler<SystemEvents[\"settings_update\"]>) {\n    this.emitter.on(\"settings_update\", handler)\n    return () => this.emitter.off(\"settings_update\", handler)\n  }\n\n  /**\n   * 🔧 Listen for device capabilities updates\n   * @param handler - Function to handle capabilities updates\n   * @returns Cleanup function to remove the handler\n   */\n  onCapabilitiesUpdate(handler: Handler<SystemEvents[\"capabilities_update\"]>) {\n    this.emitter.on(\"capabilities_update\", handler)\n    return () => this.emitter.off(\"capabilities_update\", handler)\n  }\n\n  /**\n   * 🌐 Listen for dashboard mode changes\n   * @param handler - Function to handle dashboard mode changes\n   * @returns Cleanup function to remove the handler\n   */\n  onDashboardModeChange(handler: Handler<SystemEvents[\"dashboard_mode_change\"]>) {\n    this.emitter.on(\"dashboard_mode_change\", handler)\n    return () => this.emitter.off(\"dashboard_mode_change\", handler)\n  }\n\n  /**\n   * 🌐 Listen for dashboard always-on mode changes\n   * @param handler - Function to handle dashboard always-on mode changes\n   * @returns Cleanup function to remove the handler\n   */\n  onDashboardAlwaysOnChange(handler: Handler<SystemEvents[\"dashboard_always_on_change\"]>) {\n    this.emitter.on(\"dashboard_always_on_change\", handler)\n    return () => this.emitter.off(\"dashboard_always_on_change\", handler)\n  }\n\n  /**\n   * 🚫 Listen for permission errors when subscriptions are rejected\n   * @param handler - Function to handle permission errors\n   * @returns Cleanup function to remove the handler\n   */\n  onPermissionError(handler: Handler<SystemEvents[\"permission_error\"]>) {\n    this.emitter.on(\"permission_error\", handler)\n    return () => this.emitter.off(\"permission_error\", handler)\n  }\n\n  /**\n   * 🚫 Listen for individual permission denied events for specific streams\n   * @param handler - Function to handle permission denied events\n   * @returns Cleanup function to remove the handler\n   */\n  onPermissionDenied(handler: Handler<SystemEvents[\"permission_denied\"]>) {\n    this.emitter.on(\"permission_denied\", handler)\n    return () => this.emitter.off(\"permission_denied\", handler)\n  }\n\n  /**\n   * 🔄 Listen for changes to a specific setting\n   * @param key - Setting key to monitor\n   * @param handler - Function to handle setting value changes\n   * @returns Cleanup function to remove the handler\n   */\n  onSettingChange<T>(key: string, handler: (value: T, previousValue: T | undefined) => void): () => void {\n    let previousValue: T | undefined = undefined\n\n    const settingsHandler = (settings: AppSettings) => {\n      try {\n        const setting = settings.find((s) => s.key === key)\n        if (setting) {\n          // Only call handler if value has changed\n          if (setting.value !== previousValue) {\n            const newValue = setting.value as T\n            handler(newValue, previousValue)\n            previousValue = newValue\n          }\n        }\n      } catch (error: unknown) {\n        console.error(`Error in onSettingChange handler for key \"${key}\":`, error)\n      }\n    }\n\n    this.emitter.on(\"settings_update\", settingsHandler)\n    this.emitter.on(\"connected\", settingsHandler) // Also check when first connected\n\n    return () => {\n      this.emitter.off(\"settings_update\", settingsHandler)\n      this.emitter.off(\"connected\", settingsHandler)\n    }\n  }\n\n  /**\n   * 🔄 Generic event handler\n   *\n   * Use this for stream types without specific handler methods\n   */\n  on<T extends ExtendedStreamType>(type: T, handler: Handler<EventData<T>>): () => void {\n    // Check permissions for specific stream types\n    if (type === StreamType.CALENDAR_EVENT) {\n      calendarWarnLog(this.baseUrl, this.packageName, \"on\")\n    }\n    return this.addHandler(type, handler)\n  }\n\n  /**\n   * ➕ Add an event handler and subscribe if needed\n   */\n  private addHandler<T extends ExtendedStreamType>(type: T, handler: Handler<EventData<T>>): () => void {\n    const handlers = this.handlers.get(type) ?? new Set()\n\n    if (handlers.size === 0) {\n      this.handlers.set(type, handlers)\n      this.subscribe(type)\n    }\n    handlers.add(handler as Handler<unknown>)\n    return () => this.removeHandler(type, handler)\n  }\n\n  /**\n   * ➖ Remove an event handler\n   */\n  private removeHandler<T extends ExtendedStreamType>(type: T, handler: Handler<EventData<T>>): void {\n    const handlers = this.handlers.get(type)\n    if (!handlers) return\n\n    handlers.delete(handler as Handler<unknown>)\n    if (handlers.size === 0) {\n      this.handlers.delete(type)\n      this.unsubscribe(type)\n    }\n  }\n\n  /**\n   * 🔍 Get all currently registered stream types\n   * Returns the streams that have at least one handler registered.\n   * Used to derive subscriptions from handlers (single source of truth).\n   *\n   * This is the fix for Bug 007: subscriptions are now derived from handlers\n   * instead of being stored separately, preventing drift between the two.\n   */\n  getRegisteredStreams(): ExtendedStreamType[] {\n    return Array.from(this.handlers.keys()) as ExtendedStreamType[]\n  }\n\n  /**\n   * 📡 Emit an event to all registered handlers with error isolation\n   */\n  emit<T extends EventType>(event: T, data: EventData<T>): void {\n    try {\n      // Emit to EventEmitter handlers (system events)\n      // console.log(`#### Emitting to ${event}`);\n      this.emitter.emit(event, data)\n\n      // Emit to stream handlers if applicable\n      const handlers = this.handlers.get(event)\n      // console.log(`#### Handlers: ${JSON.stringify(handlers)}`);\n\n      if (handlers) {\n        // Create array of handlers to prevent modification during iteration\n        const handlersArray = Array.from(handlers)\n        // console.log(`((())) HandlersArray: ${JSON.stringify(handlersArray)}`);\n\n        // Execute each handler in isolated try/catch to prevent one handler\n        // from crashing the entire App\n        handlersArray.forEach((handler) => {\n          try {\n            ;(handler as Handler<EventData<T>>)(data)\n          } catch (handlerError: unknown) {\n            // Log the error but don't let it propagate\n            console.error(`Error in handler for event '${String(event)}':`, handlerError)\n\n            // Emit an error event for tracking purposes\n            if (event !== \"error\") {\n              // Prevent infinite recursion\n              const errorMessage = handlerError instanceof Error ? handlerError.message : String(handlerError)\n\n              this.emitter.emit(\"error\", new Error(`Handler error for event '${String(event)}': ${errorMessage}`))\n            }\n          }\n        })\n      }\n    } catch (emitError: unknown) {\n      // Catch any errors in the emission process itself\n      console.error(`Fatal error emitting event '${String(event)}':`, emitError)\n\n      // Try to emit an error event if we're not already handling an error\n      if (event !== \"error\") {\n        try {\n          const errorMessage = emitError instanceof Error ? emitError.message : String(emitError)\n\n          this.emitter.emit(\"error\", new Error(`Event emission error for '${String(event)}': ${errorMessage}`))\n        } catch (nestedError) {\n          // If even this fails, just log it - nothing more we can do\n          console.error(\"Failed to emit error event:\", nestedError)\n        }\n      }\n    }\n  }\n\n  /**\n   * 📨 Listen for custom messages with a specific action\n   * @param action - The action identifier to filter by\n   * @param handler - Function to handle the message\n   * @returns Cleanup function to remove the handler\n   */\n  onCustomMessage(action: string, handler: (payload: any) => void): () => void {\n    const messageHandler = (message: CustomMessage) => {\n      if (message.action === action) {\n        handler(message.payload)\n      }\n    }\n\n    this.emitter.on(\"custom_message\", messageHandler)\n    return () => this.emitter.off(\"custom_message\", messageHandler)\n  }\n\n  onVpsCoordinates(handler: Handler<VpsCoordinates>) {\n    return this.addHandler(StreamType.VPS_COORDINATES, handler)\n  }\n\n  /**\n   * 📸 Listen for photo responses\n   * @param handler - Function to handle photo response data\n   * @returns Cleanup function to remove the handler\n   */\n  onPhotoTaken(handler: Handler<PhotoTaken>) {\n    return this.addHandler(StreamType.PHOTO_TAKEN, handler)\n  }\n}\n","// src/index.ts\n\nexport * from \"./token\"\n\n// Message type enums\nexport * from \"./message-types\"\n\n// Base message type\nexport * from \"./messages/base\"\n\n// Messages by direction - export everything except the conflicting type guards\nexport * from \"./messages/glasses-to-cloud\"\nexport * from \"./messages/cloud-to-glasses\"\n\n// Export from app-to-cloud excluding isPhotoRequest which conflicts with cloud-to-glasses\nexport {\n  // Types\n  SubscriptionRequest,\n  AppConnectionInit,\n  AppSubscriptionUpdate,\n  PhotoRequest,\n  RgbLedControlRequest,\n  RtmpStreamRequest,\n  RtmpStreamStopRequest,\n  AppLocationPollRequest,\n  RestreamDestination,\n  ManagedStreamRequest,\n  ManagedStreamStopRequest,\n  StreamStatusCheckRequest,\n  AudioPlayRequest,\n  AudioStopRequest,\n  AppToCloudMessage,\n  AppBroadcastMessage,\n  AppDirectMessage,\n  AppUserDiscovery,\n  AppRoomJoin,\n  AppRoomLeave,\n  RequestWifiSetup,\n  OwnershipReleaseMessage,\n  // Type guards - all except isPhotoRequest\n  isAppConnectionInit,\n  isAppSubscriptionUpdate,\n  isDisplayRequest,\n  isRgbLedControlRequest,\n  isAudioPlayRequest,\n  isAudioStopRequest,\n  isDashboardContentUpdate,\n  isDashboardModeChange,\n  isDashboardSystemUpdate,\n  isManagedStreamRequest,\n  isManagedStreamStopRequest,\n  isRtmpStreamRequest,\n  isRtmpStreamStopRequest,\n  isOwnershipRelease,\n  // Export with alias to avoid conflict\n  isPhotoRequest as isPhotoRequestFromApp,\n} from \"./messages/app-to-cloud\"\n\n// Export cloud-to-app but exclude the conflicting type guards\nexport {\n  // Types\n  AppConnectionAck,\n  AppConnectionError,\n  AppStopped,\n  SettingsUpdate as AppSettingsUpdate, // Alias to avoid conflict with cloud-to-glasses SettingsUpdate\n  CapabilitiesUpdate,\n  DataStream,\n  CloudToAppMessage,\n  AudioPlayResponse,\n  TranslationData,\n  ToolCall,\n  StandardConnectionError,\n  CustomMessage,\n  ManagedStreamStatus,\n  StreamStatusCheckResponse,\n  OutputStatus,\n  MentraosSettingsUpdate,\n  TranscriptionData,\n  TranscriptionMetadata,\n  SonioxToken,\n  AudioChunk,\n  PermissionError,\n  PermissionErrorDetail,\n  // Type guards (excluding isPhotoResponse and isRtmpStreamStatus which conflict)\n  isAppConnectionAck,\n  isAppConnectionError,\n  isAppStopped,\n  isSettingsUpdate,\n  isCapabilitiesUpdate,\n  isDataStream,\n  isAudioChunk,\n  isAudioPlayResponse,\n  isDashboardModeChanged,\n  isDashboardAlwaysOnChanged,\n  isManagedStreamStatus,\n  isStreamStatusCheckResponse,\n  // Re-export the cloud-to-app versions of these type guards since they're the ones\n  // that should be used when dealing with CloudToAppMessage types\n  isPhotoResponse as isPhotoResponseFromCloud,\n  isRtmpStreamStatus as isRtmpStreamStatusFromCloud,\n  isRgbLedControlResponse as isRgbLedControlResponseFromCloud,\n} from \"./messages/cloud-to-app\"\n\n// Stream types\nexport * from \"./streams\"\n\n// Layout types\nexport * from \"./layouts\"\n\n// Dashboard types\nexport * from \"./dashboard\"\n\n// RTMP streaming types\nexport * from \"./rtmp-stream\"\n\n// Other system enums\nexport * from \"./enums\"\n\n// Core model interfaces\nexport * from \"./models\"\n\n// Webhook interfaces\nexport * from \"./webhooks\"\n\n// Capability Discovery types\nexport * from \"./capabilities\"\n\n// Photo data types\nexport * from \"./photo-data\"\n\n/**\n * WebSocket error information\n */\nexport interface WebSocketError {\n  code: string\n  message: string\n  details?: unknown\n}\n\nimport type {Request} from \"express\"\nimport type {AppSession} from \"../app/session\"\n\nexport interface AuthenticatedRequest extends Request {\n  authUserId?: string\n  activeSession: AppSession | null\n}\n","/**\n * warning.ts\n *\n * This file defines styled warning messages that are displayed when an app\n * attempts to use functionality requiring permissions it hasn't declared.\n *\n * Each function generates a bordered terminal warning box with:\n * - ASCII art logo (left side)\n * - Permission requirement details (right side)\n * - Link to developer portal for adding permissions\n *\n * The warnings use chalk for terminal colors and boxen for bordered output,\n * creating a professional side-by-side layout that alerts developers to\n * missing permissions in their app configuration.\n *\n * These are shown during SDK runtime when permission checks fail, helping\n * developers identify and fix permission issues quickly.\n */\nimport chalk from \"chalk\";\nimport boxen from \"boxen\";\nimport { warnLog } from \"./logos\";\n\nconst createPermissionWarning = (\n  permissionName: string,\n  funcName?: string,\n  packageName?: string,\n): string => {\n  // Strip ANSI codes for width calculation\n  // eslint-disable-next-line no-control-regex\n  const stripAnsi = (str: string) => str.replace(/\\u001b\\[\\d+m/g, \"\");\n\n  const title = chalk.bold.yellow(\"⚠️  Permission Required\");\n  const message = `${chalk.yellow(funcName || \"This function\")} requires ${chalk.bold(permissionName)} permission.`;\n  const instructions = chalk.dim(\n    \"Please enable this permission in the developer portal at:\",\n  );\n  const url = chalk.cyan.underline(\n    `https://console.mentra.glass/apps/${packageName}/edit`,\n  );\n  const hint = chalk.dim(\"under *Required Permissions*.\");\n\n  // Split logo into lines (without color for width calculation)\n  const logoLines = warnLog.split(\"\\n\");\n  const coloredLogoLines = logoLines.map((line) => chalk.yellow(line));\n\n  const textContent = [title, \"\", message, \"\", instructions, url, \"\", hint];\n\n  // Find actual logo width (max line length without ANSI codes)\n  const logoWidth = Math.max(\n    ...logoLines.map((line) => stripAnsi(line).length),\n  );\n\n  // Create side-by-side layout\n  const maxLines = Math.max(coloredLogoLines.length, textContent.length);\n  const combinedLines: string[] = [];\n\n  for (let i = 0; i < maxLines; i++) {\n    const rawLogoLine = logoLines[i] || \"\";\n    const coloredLogoLine = coloredLogoLines[i] || \"\";\n    const actualLogoLength = stripAnsi(rawLogoLine).length;\n    const padding = \" \".repeat(Math.max(0, logoWidth - actualLogoLength));\n    const textLine = textContent[i] || \"\";\n    combinedLines.push(`${coloredLogoLine}${padding}    ${textLine}`);\n  }\n\n  return boxen(combinedLines.join(\"\\n\"), {\n    padding: 1,\n    margin: 1,\n    borderStyle: \"round\",\n    borderColor: \"yellow\",\n    float: \"left\",\n  });\n};\n\nexport const noMicrophoneWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"microphone\", funcName, packageName);\n};\n\nexport const locationWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"location\", funcName, packageName);\n};\n\nexport const baackgroundLocationWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"background location\", funcName, packageName);\n};\n\nexport const calendarWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"calendar\", funcName, packageName);\n};\n\nexport const readNotficationWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"read notification\", funcName, packageName);\n};\n\nexport const postNotficationWarn = (\n  funcName?: string,\n  packageName?: string,\n): string => {\n  return createPermissionWarning(\"post notification\", funcName, packageName);\n};\n\nexport const cameraWarn = (funcName?: string, packageName?: string): string => {\n  return createPermissionWarning(\"camera\", funcName, packageName);\n};\n","export const warnLog = String.raw`\n__/\\\\\\\\____________/\\\\\\\\_        \n _\\/\\\\\\\\\\\\________/\\\\\\\\\\\\_       \n  _\\/\\\\\\//\\\\\\____/\\\\\\//\\\\\\_      \n   _\\/\\\\\\\\///\\\\\\/\\\\\\/_\\/\\\\\\_     \n    _\\/\\\\\\__\\///\\\\\\/___\\/\\\\\\_    \n     _\\/\\\\\\____\\///_____\\/\\\\\\_   \n      _\\/\\\\\\_____________\\/\\\\\\_  \n       _\\/\\\\\\_____________\\/\\\\\\_ \n        _\\///______________\\///__`;\n\nexport const mentraLogo_1 = String.raw`\n __       __  ________  __    __  ________  _______    ______  \n|  \\     /  \\|        \\|  \\  |  \\|        \\|       \\  /      \\ \n| $$\\   /  $$| $$$$$$$$| $$\\ | $$ \\$$$$$$$$| $$$$$$$\\|  $$$$$$\\\n| $$$\\ /  $$$| $$__    | $$$\\| $$   | $$   | $$__| $$| $$__| $$\n| $$$$\\  $$$$| $$  \\   | $$$$\\ $$   | $$   | $$    $$| $$    $$\n| $$\\$$ $$ $$| $$$$$   | $$\\$$ $$   | $$   | $$$$$$$\\| $$$$$$$$\n| $$ \\$$$| $$| $$_____ | $$ \\$$$$   | $$   | $$  | $$| $$  | $$\n| $$  \\$ | $$| $$     \\| $$  \\$$$   | $$   | $$  | $$| $$  | $$\n \\$$      \\$$ \\$$$$$$$$ \\$$   \\$$    \\$$    \\$$   \\$$ \\$$   \\$$\n`;\n\nexport const newUpdateText = `\n┬╔╗╔╔═╗╦ ╦  ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗┬\n│║║║║╣ ║║║  ║ ║╠═╝ ║║╠═╣ ║ ║╣ │\no╝╚╝╚═╝╚╩╝  ╚═╝╩  ═╩╝╩ ╩ ╩ ╚═╝o\n`;\n// export const newSDKUpdate = (versionNumb: string): string => {\n//   return `\n\n//  /$$      /$$ /$$$$$$$$ /$$   /$$ /$$$$$$$$ /$$$$$$$   /$$$$$$\n// | $$$    /$$$| $$_____/| $$$ | $$|__  $$__/| $$__  $$ /$$__  $$\n// | $$$$  /$$$$| $$      | $$$$| $$   | $$   | $$  \\ $$|  $$  \\  $$\n// | $$ $$/$$ $$| $$$$$   | $$ $$ $$   | $$   | $$$$$$$/| $$$$$$$$\n// | $$  $$$| $$| $$__/   | $$  $$$$   | $$   | $$__  $$| $$__  $$\n// | $$\\  $  | $$| $$      | $$\\   $$$   | $$   | $$   \\ $$| $$  | $$\n// | $$ \\/   | $$| $$$$$$$$| $$ \\   $$   | $$   | $$  | $$| $$  | $$\n// |__/     |__/|________/|__/   \\__/   |__/   |__/  |__/|__/  |__/\n\n// ┬╔╗╔╔═╗╦ ╦  ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗┬\n// │║║║║╣ ║║║  ║ ║╠═╝ ║║╠═╣ ║ ║╣ │\n// o╝╚╝╚═╝╚╩╝  ╚═╝╩  ═╩╝╩ ╩ ╩ ╚═╝o\n// -------------------------------\n// SDK VERSION V${versionNumb} is out!\n// -------------------------------\n// bun install @mentra/sdk@latest\n//   `;\n// };\n","/**\n * permissions-utils.ts\n *\n * This file provides runtime permission validation utilities for the MentraOS SDK.\n *\n * Each function queries the public permissions API endpoint to check if an app\n * has declared the required permission for a specific feature. If the permission\n * is missing, a styled warning message is displayed in the terminal.\n *\n * Key features:\n * - Fetches app permissions from /api/public/permissions/:packageName\n * - Gracefully handles offline/unreachable endpoints (silent failure)\n * - Displays professional bordered warnings when permissions are missing\n * - Non-blocking - allows app execution to continue even if checks fail\n *\n * These functions are called automatically by SDK methods that require specific\n * permissions (e.g., microphone access, location tracking, camera, etc.) to help\n * developers identify missing permission declarations during development.\n */\nimport {\n  noMicrophoneWarn,\n  locationWarn,\n  baackgroundLocationWarn,\n  calendarWarn,\n  readNotficationWarn,\n  postNotficationWarn,\n  cameraWarn,\n} from \"../constants/log-messages/warning\";\nimport {\n  PackagePermissions,\n  Permission,\n} from \"../../src/types/messages/cloud-to-app\";\n// Check if app has microphone permission, warn if missing\nexport const microPhoneWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  // console.log(`Fetching permissions from: ${permissionsUrl}`);\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasMic = data.permissions.some(\n          (p: Permission) => p.type === \"MICROPHONE\",\n        );\n\n        if (!hasMic) {\n          console.log(noMicrophoneWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      // Silently fail if endpoint is unreachable - don't block execution\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has location permission, warn if missing\nexport const locationWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasLocation = data.permissions.some(\n          (p: Permission) => p.type === \"LOCATION\",\n        );\n\n        if (!hasLocation) {\n          console.log(locationWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has background location permission, warn if missing\nexport const backgroundLocationWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasBackgroundLocation = data.permissions.some(\n          (p: Permission) => p.type === \"BACKGROUND_LOCATION\",\n        );\n\n        if (!hasBackgroundLocation) {\n          console.log(baackgroundLocationWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has calendar permission, warn if missing\nexport const calendarWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasCalendar = data.permissions.some(\n          (p: Permission) => p.type === \"CALENDAR\",\n        );\n\n        if (!hasCalendar) {\n          console.log(calendarWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has read notifications permission, warn if missing\nexport const readNotificationWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasReadNotifications = data.permissions.some(\n          (p: Permission) => p.type === \"READ_NOTIFICATIONS\",\n        );\n\n        if (!hasReadNotifications) {\n          console.log(readNotficationWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has post notifications permission, warn if missing\nexport const postNotificationWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasPostNotifications = data.permissions.some(\n          (p: Permission) => p.type === \"POST_NOTIFICATIONS\",\n        );\n\n        if (!hasPostNotifications) {\n          console.log(postNotficationWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n\n// Check if app has camera permission, warn if missing\nexport const cameraWarnLog = (\n  cloudServerUrl: string,\n  packageName: string,\n  funcName?: string,\n) => {\n  if (!cloudServerUrl) return;\n\n  const permissionsUrl = `${cloudServerUrl}/api/public/permissions/${encodeURIComponent(packageName)}`;\n\n  fetch(permissionsUrl)\n    .then(async (res) => {\n      const contentType = res.headers.get(\"content-type\");\n      if (!res.ok) {\n        console.warn(\n          `Permission API returned ${res.status}: ${res.statusText}`,\n        );\n        return null;\n      }\n\n      if (contentType && contentType.includes(\"application/json\")) {\n        return (await res.json()) as PackagePermissions;\n      } else {\n        const text = await res.text();\n        console.warn(`Permission API returned non-JSON response: ${text}`);\n        return null;\n      }\n    })\n    .then((data: PackagePermissions | null) => {\n      if (data) {\n        const hasCamera = data.permissions.some(\n          (p: Permission) => p.type === \"CAMERA\",\n        );\n\n        if (!hasCamera) {\n          console.log(cameraWarn(funcName, packageName));\n        }\n      }\n    })\n    .catch((err) => {\n      console.debug(\n        \"Permission check skipped - endpoint unreachable:\",\n        err.message,\n      );\n    });\n};\n","/**\n * 🎨 Layout Manager Module\n *\n * Manages AR display layouts for Apps. This class provides an easy-to-use interface\n * for showing different types of content in the user's AR view.\n *\n * @example\n * ```typescript\n * const layouts = new LayoutManager('org.example.myapp', sendMessage);\n *\n * // Show a simple message\n * layouts.showTextWall('Hello AR World!');\n *\n * // Show a card with title\n * layouts.showReferenceCard('Weather', 'Sunny and 75°F');\n * ```\n */\nimport { BitmapUtils } from \"../../utils/bitmap-utils\";\nimport {\n  DisplayRequest,\n  Layout,\n  TextWall,\n  DoubleTextWall,\n  ReferenceCard,\n  DashboardCard,\n  LayoutType,\n  ViewType,\n  AppToCloudMessageType,\n  BitmapView,\n  BitmapAnimation,\n  ClearView,\n} from \"../../types\";\n\nexport class LayoutManager {\n  /**\n   * 🎯 Creates a new LayoutManager instance\n   *\n   * @param packageName - App package identifier\n   * @param sendMessage - Function to send display requests to MentraOS\n   */\n  constructor(\n    private packageName: string,\n    private sendMessage: (message: DisplayRequest) => void,\n  ) {}\n\n  /**\n   * 📦 Creates a display event request with validation\n   *\n   * @param layout - Layout configuration to display\n   * @param view - View type (main or dashboard)\n   * @param durationMs - How long to show the layout (optional)\n   * @returns Formatted display request\n   * @throws Error if layout is invalid\n   */\n  private createDisplayEvent(\n    layout: Layout,\n    view: ViewType = ViewType.MAIN,\n    durationMs?: number,\n  ): DisplayRequest {\n    try {\n      // Validate layout data before sending\n      if (!layout) {\n        throw new Error(\"Layout cannot be null or undefined\");\n      }\n\n      if (!layout.layoutType) {\n        throw new Error(\"Layout must have a layoutType property\");\n      }\n\n      // Layout-specific validations\n      switch (layout.layoutType) {\n        case LayoutType.TEXT_WALL:\n          if (typeof (layout as TextWall).text !== \"string\") {\n            throw new Error(\"TextWall layout must have a text property\");\n          }\n          // Ensure text is not too long (prevent performance issues)\n          if ((layout as TextWall).text.length > 1000) {\n            console.warn(\n              \"TextWall text is very long, this may cause performance issues\",\n            );\n          }\n          break;\n\n        case LayoutType.DOUBLE_TEXT_WALL:\n          const doubleText = layout as DoubleTextWall;\n          if (typeof doubleText.topText !== \"string\") {\n            throw new Error(\n              \"DoubleTextWall layout must have a topText property\",\n            );\n          }\n          if (typeof doubleText.bottomText !== \"string\") {\n            throw new Error(\n              \"DoubleTextWall layout must have a bottomText property\",\n            );\n          }\n          break;\n\n        case LayoutType.REFERENCE_CARD:\n          const refCard = layout as ReferenceCard;\n          if (typeof refCard.title !== \"string\") {\n            throw new Error(\"ReferenceCard layout must have a title property\");\n          }\n          if (typeof refCard.text !== \"string\") {\n            throw new Error(\"ReferenceCard layout must have a text property\");\n          }\n          break;\n\n        case LayoutType.DASHBOARD_CARD:\n          const dashCard = layout as DashboardCard;\n          if (typeof dashCard.leftText !== \"string\") {\n            throw new Error(\n              \"DashboardCard layout must have a leftText property\",\n            );\n          }\n          if (typeof dashCard.rightText !== \"string\") {\n            throw new Error(\n              \"DashboardCard layout must have a rightText property\",\n            );\n          }\n          break;\n\n        case LayoutType.BITMAP_VIEW:\n          const bitmapView = layout as BitmapView;\n          if (typeof bitmapView.data !== \"string\") {\n            throw new Error(\"BitmapView layout must have a data property\");\n          }\n          // Check if data is too large (prevent OOM errors)\n          if (bitmapView.data.length > 1000000) {\n            // 1MB limit\n            throw new Error(\n              \"Bitmap data is too large (>1MB), please reduce size\",\n            );\n          }\n          break;\n\n        case LayoutType.CLEAR_VIEW:\n          // ClearView has no additional validation needed\n          break;\n      }\n\n      // Validate view type\n      if (view !== ViewType.MAIN && view !== ViewType.DASHBOARD) {\n        console.warn(`Invalid view type: ${view}, defaulting to MAIN`);\n        view = ViewType.MAIN;\n      }\n\n      // Validate duration if provided\n      if (durationMs !== undefined) {\n        if (typeof durationMs !== \"number\" || durationMs < 0) {\n          console.warn(`Invalid duration: ${durationMs}, ignoring`);\n          durationMs = undefined;\n        }\n      }\n\n      // Create the display request with validated data\n      return {\n        timestamp: new Date(),\n        sessionId: \"\", // Will be filled by session\n        type: AppToCloudMessageType.DISPLAY_REQUEST,\n        packageName: this.packageName,\n        view,\n        layout,\n        durationMs,\n      };\n    } catch (error) {\n      console.error(\"Error creating display event:\", error);\n      throw error; // Re-throw to notify caller\n    }\n  }\n\n  /**\n   * 📝 Shows a single block of text\n   *\n   * Best for:\n   * - Simple messages\n   * - Status updates\n   * - Notifications\n   *\n   * @param text - Text content to display\n   * @param options - Optional parameters (view, duration, priority)\n   *   - priority: If true, this display will not be overridden by other requests (default: false)\n   *\n   * @example\n   * ```typescript\n   * layouts.showTextWall('Connected to server');\n   * layouts.showTextWall('Onboarding!', { priority: true });\n   * ```\n   */\n  showTextWall(\n    text: string,\n    options?: { view?: ViewType; durationMs?: number },\n  ) {\n    try {\n      // Validate input before processing\n      if (text === undefined || text === null) {\n        text = \"\"; // Default to empty string instead of crashing\n        console.warn(\"showTextWall called with null/undefined text\");\n      }\n\n      // Ensure text is a string\n      if (typeof text !== \"string\") {\n        text = String(text); // Convert to string\n        console.warn(\"showTextWall: Non-string input converted to string\");\n      }\n\n      // Create layout with validated text\n      const layout: TextWall = {\n        layoutType: LayoutType.TEXT_WALL,\n        text,\n      };\n\n      // Create and send display event with error handling\n      try {\n        const displayEvent = this.createDisplayEvent(\n          layout,\n          options?.view,\n          options?.durationMs,\n        );\n        this.sendMessage(displayEvent);\n      } catch (error) {\n        console.error(\"Failed to display text wall:\", error);\n        // Don't re-throw - prevent app crashes\n      }\n    } catch (error) {\n      console.error(\"Error in showTextWall:\", error);\n      // Don't crash the App - fail gracefully\n    }\n  }\n\n  /**\n   * ↕️ Shows two sections of text, one above the other\n   *\n   * Best for:\n   * - Before/After content\n   * - Question/Answer displays\n   * - Two-part messages\n   * - Comparisons\n   *\n   * @param topText - Text to show in top section\n   * @param bottomText - Text to show in bottom section\n   * @param options - Optional parameters (view, duration)\n   *\n   * @example\n   * ```typescript\n   * layouts.showDoubleTextWall(\n   *   'Original: Hello',\n   *   'Translated: Bonjour'\n   * );\n   * ```\n   */\n  showDoubleTextWall(\n    topText: string,\n    bottomText: string,\n    options?: { view?: ViewType; durationMs?: number },\n  ) {\n    const layout: DoubleTextWall = {\n      layoutType: LayoutType.DOUBLE_TEXT_WALL,\n      topText,\n      bottomText,\n    };\n    this.sendMessage(\n      this.createDisplayEvent(layout, options?.view, options?.durationMs),\n    );\n  }\n\n  /**\n   * 📇 Shows a card with a title and content\n   *\n   * Best for:\n   * - Titled content\n   * - Important information\n   * - Structured data\n   * - Notifications with context\n   *\n   * @param title - Card title\n   * @param text - Main content text\n   * @param options - Optional parameters (view, duration)\n   *\n   * @example\n   * ```typescript\n   * layouts.showReferenceCard(\n   *   'Meeting Reminder',\n   *   'Team standup in 5 minutes'\n   * );\n   * ```\n   */\n  showReferenceCard(\n    title: string,\n    text: string,\n    options?: { view?: ViewType; durationMs?: number },\n  ) {\n    const layout: ReferenceCard = {\n      layoutType: LayoutType.REFERENCE_CARD,\n      title,\n      text,\n    };\n    this.sendMessage(\n      this.createDisplayEvent(layout, options?.view, options?.durationMs),\n    );\n  }\n\n  /**\n   * 📇 Shows a bitmap\n   *\n   * Uses the proven animation system internally for proper L/R eye synchronization.\n   * This ensures single bitmap displays work consistently with the same\n   * hardware-optimized timing as animations.\n   *\n   * @param data - hex or base64 encoded bitmap data\n   * @param options - Optional parameters (view, duration)\n   *\n   * @example\n   * ```typescript\n   * layouts.showBitmapView(\n   *   yourHexOrBase64EncodedBitmapDataString\n   * );\n   * ```\n   */\n  async showBitmapView(\n    base64Bitmap: string,\n    options?: { view?: ViewType; padding?: { left: number; top: number } },\n  ) {\n    const padding = options?.padding ?? { left: 50, top: 35 };\n    const bitmapFrame = await BitmapUtils.padBase64Bitmap(\n      base64Bitmap,\n      padding,\n    );\n    const validation = BitmapUtils.validateBase64Bitmap(bitmapFrame);\n    if (!validation.isValid) {\n      throw new Error(\n        `❌ Frame validation failed: ${validation.errors.join(\", \")}`,\n      );\n    }\n    const layout: BitmapView = {\n      layoutType: LayoutType.BITMAP_VIEW,\n      data: bitmapFrame,\n    };\n    this.sendMessage(this.createDisplayEvent(layout, options?.view));\n  }\n\n  /**\n   * 📊 Shows a dashboard card with left and right text\n   *\n   * Best for:\n   * - Key-value pairs\n   * - Dashboard displays\n   * - Metrics\n   *\n   * @param leftText - Left side text (typically label/key)\n   * @param rightText - Right side text (typically value)\n   * @param options - Optional parameters (view, duration)\n   *\n   * @example\n   * ```typescript\n   * layouts.showDashboardCard('Weather', '72°F');\n   * ```\n   */\n  showDashboardCard(\n    leftText: string,\n    rightText: string,\n    options?: { view?: ViewType; durationMs?: number },\n  ) {\n    const layout: DashboardCard = {\n      layoutType: LayoutType.DASHBOARD_CARD,\n      leftText,\n      rightText,\n    };\n    this.sendMessage(\n      this.createDisplayEvent(\n        layout,\n        options?.view || ViewType.DASHBOARD,\n        options?.durationMs,\n      ),\n    );\n  }\n\n  /**\n   * 🧹 Clears the display\n   *\n   * Best for:\n   * - Clearing previous content\n   * - Resetting display state\n   * - Starting fresh\n   *\n   * @param options - Optional parameters (view)\n   *\n   * @example\n   * ```typescript\n   * layouts.clearView();\n   * layouts.clearView({ view: ViewType.DASHBOARD });\n   * ```\n   */\n  clearView(options?: { view?: ViewType }) {\n    const layout: ClearView = {\n      layoutType: LayoutType.CLEAR_VIEW,\n    };\n    this.sendMessage(this.createDisplayEvent(layout, options?.view));\n  }\n\n  /**\n   * 🎬 Shows an animated sequence of bitmap images (iOS-controlled timing)\n   *\n   * Sends complete animation package to iOS for device-controlled timing.\n   * This provides superior performance and synchronization by letting\n   * the device control the display timing directly.\n   *\n   * Best for:\n   * - Smooth high-performance animations\n   * - Precise timing control\n   * - Synchronized left/right display\n   * - EvenDemo-quality performance\n   *\n   * @param bitmapDataArray - Array of bitmap data strings (hex or base64 encoded)\n   * @param intervalMs - Time between frames in milliseconds (default: 1650ms)\n   * @param repeat - Whether to loop the animation continuously (default: false)\n   * @param options - Optional parameters (view)\n   *\n   * @example\n   * ```typescript\n   * // Device-controlled animation (recommended)\n   * const frames = ['hexdata1', 'hexdata2', 'hexdata3'];\n   * layouts.showBitmapAnimation(frames, 1650, true);\n   *\n   * // Single-cycle animation\n   * layouts.showBitmapAnimation(loadingFrames, 1650, false);\n   * ```\n   *\n   * @returns Animation controller object with stop() method\n   */\n  showBitmapAnimation(\n    bitmapDataArray: string[],\n    intervalMs: number = 1650,\n    repeat: boolean = false,\n    options?: { view?: ViewType },\n  ): { stop: () => void } {\n    // Validation\n    if (!Array.isArray(bitmapDataArray) || bitmapDataArray.length === 0) {\n      throw new Error(\n        \"showBitmapAnimation requires a non-empty array of bitmap data\",\n      );\n    }\n\n    // Send complete animation package to iOS for device-controlled timing\n    const layout: BitmapAnimation = {\n      layoutType: LayoutType.BITMAP_ANIMATION,\n      frames: bitmapDataArray,\n      interval: intervalMs,\n      repeat: repeat,\n    };\n\n    this.sendMessage(this.createDisplayEvent(layout, options?.view));\n\n    console.log(\n      `🎬 Sent batched animation to iOS: ${\n        bitmapDataArray.length\n      } frames at ${intervalMs}ms${repeat ? \" (repeating)\" : \"\"}`,\n    );\n\n    // Return controller for compatibility\n    return {\n      stop: () => {\n        // Send stop command to iOS\n        this.clearView();\n        console.log(\"🛑 Animation stop requested\");\n      },\n    };\n  }\n}\n","/**\n * 🔧 Settings Manager Module\n *\n * Manages App settings with automatic synchronization and change notifications.\n * Provides type-safe access to settings with default values.\n */\nimport EventEmitter from \"events\";\nimport { AppSetting, AppSettings } from \"../../types\";\nimport { ApiClient } from \"./api-client\";\nimport { logger } from \"../../logging/logger\"; // Adjust import path as needed\n// Note(Isaiah): Let's not import @mentra/utils in the SDK to avoid circular dependencies. Also i'm deprecating it in favor of the new logging system.\n\n/**\n * Change information for a single setting\n */\nexport interface SettingChange {\n  oldValue: any;\n  newValue: any;\n}\n\n/**\n * Map of setting keys to their change information\n */\nexport type SettingsChangeMap = Record<string, SettingChange>;\n\n/**\n * Callback for when any setting changes\n */\nexport type SettingsChangeHandler = (changes: SettingsChangeMap) => void;\n\n/**\n * Callback for when a specific setting changes\n */\nexport type SettingValueChangeHandler<T = any> = (\n  newValue: T,\n  oldValue: T,\n) => void;\n\n/**\n * Internal event names\n */\nenum SettingsEvents {\n  CHANGE = \"settings:change\",\n  VALUE_CHANGE = \"settings:value:\",\n}\n\n/**\n * 🔧 Settings Manager\n *\n * Provides a type-safe interface for accessing and reacting to App settings.\n * Automatically synchronizes with MentraOS Cloud.\n */\nexport class SettingsManager {\n  // Current settings values\n  private settings: AppSettings = [];\n\n  // Event emitter for change notifications\n  private emitter = new EventEmitter();\n\n  // API client for fetching settings\n  private apiClient?: ApiClient;\n\n  // --- MentraOS settings event system ---\n  private mentraosSettings: Record<string, any> = {};\n  private mentraosEmitter = new EventEmitter();\n  private subscribeFn?: (streams: string[]) => Promise<void>; // Added for auto-subscriptions\n\n  /**\n   * Create a new settings manager\n   *\n   * @param initialSettings Initial settings values (if available)\n   * @param packageName Package name for the App\n   * @param wsUrl WebSocket URL (for deriving HTTP API URL)\n   * @param userId User ID (for authenticated requests)\n   * @param subscribeFn Optional function to call to subscribe to streams\n   */\n  constructor(\n    initialSettings: AppSettings = [],\n    packageName?: string,\n    wsUrl?: string,\n    userId?: string,\n    subscribeFn?: (streams: string[]) => Promise<void>, // Added parameter\n  ) {\n    this.settings = [...initialSettings];\n    this.subscribeFn = subscribeFn; // Store the subscribe function\n\n    // Create API client if we have enough information\n    if (packageName) {\n      this.apiClient = new ApiClient(packageName, wsUrl, userId);\n    }\n  }\n\n  /**\n   * Configure the API client\n   *\n   * @param packageName Package name for the App\n   * @param wsUrl WebSocket URL\n   * @param userId User ID\n   */\n  configureApiClient(packageName: string, wsUrl: string, userId: string): void {\n    if (!this.apiClient) {\n      this.apiClient = new ApiClient(packageName, wsUrl, userId);\n    } else {\n      this.apiClient.setWebSocketUrl(wsUrl);\n      this.apiClient.setUserId(userId);\n    }\n  }\n\n  /**\n   * Update the current settings\n   * This is called internally when settings are loaded or changed\n   *\n   * @param newSettings New settings values\n   * @returns Map of changed settings\n   */\n  updateSettings(newSettings: AppSettings): SettingsChangeMap {\n    const changes: SettingsChangeMap = {};\n\n    // Copy the new settings\n    const updatedSettings = [...newSettings];\n\n    // Detect changes comparing old and new settings\n    for (const newSetting of updatedSettings) {\n      const oldSetting = this.settings.find((s) => s.key === newSetting.key);\n\n      // Skip if value hasn't changed\n      if (oldSetting && this.areEqual(oldSetting.value, newSetting.value)) {\n        continue;\n      }\n\n      // Record change\n      changes[newSetting.key] = {\n        oldValue: oldSetting?.value,\n        newValue: newSetting.value,\n      };\n    }\n\n    // Check for removed settings\n    for (const oldSetting of this.settings) {\n      const stillExists = updatedSettings.some((s) => s.key === oldSetting.key);\n\n      if (!stillExists) {\n        changes[oldSetting.key] = {\n          oldValue: oldSetting.value,\n          newValue: undefined,\n        };\n      }\n    }\n\n    // If there are changes, update the settings and emit events\n    if (Object.keys(changes).length > 0) {\n      this.settings = updatedSettings;\n      this.emitChanges(changes);\n    }\n\n    return changes;\n  }\n\n  /**\n   * Check if two setting values are equal\n   *\n   * @param a First value\n   * @param b Second value\n   * @returns True if the values are equal\n   */\n  private areEqual(a: any, b: any): boolean {\n    // Simple equality check - for objects, this won't do a deep equality check\n    // but for most setting values (strings, numbers, booleans) it works\n    return a === b;\n  }\n\n  /**\n   * Emit change events for updated settings\n   *\n   * @param changes Map of changed settings\n   */\n  private emitChanges(changes: SettingsChangeMap): void {\n    // Emit the general change event\n    this.emitter.emit(SettingsEvents.CHANGE, changes);\n\n    // Emit individual value change events\n    for (const [key, change] of Object.entries(changes)) {\n      this.emitter.emit(\n        `${SettingsEvents.VALUE_CHANGE}${key}`,\n        change.newValue,\n        change.oldValue,\n      );\n    }\n  }\n\n  /**\n   * 🔄 Listen for changes to any setting\n   *\n   * @param handler Function to call when settings change\n   * @returns Function to remove the listener\n   *\n   * @example\n   * ```typescript\n   * settings.onChange((changes) => {\n   *   console.log('Settings changed:', changes);\n   * });\n   * ```\n   */\n  onChange(handler: SettingsChangeHandler): () => void {\n    this.emitter.on(SettingsEvents.CHANGE, handler);\n    return () => this.emitter.off(SettingsEvents.CHANGE, handler);\n  }\n\n  /**\n   * 🔄 Listen for changes to a specific setting\n   *\n   * @param key Setting key to monitor\n   * @param handler Function to call when the setting changes\n   * @returns Function to remove the listener\n   *\n   * @example\n   * ```typescript\n   * settings.onValueChange('transcribe_language', (newValue, oldValue) => {\n   *   console.log(`Language changed from ${oldValue} to ${newValue}`);\n   * });\n   * ```\n   */\n  onValueChange<T = any>(\n    key: string,\n    handler: SettingValueChangeHandler<T>,\n  ): () => void {\n    const eventName = `${SettingsEvents.VALUE_CHANGE}${key}`;\n    this.emitter.on(eventName, handler);\n    return () => this.emitter.off(eventName, handler);\n  }\n\n  /**\n   * 🔍 Check if a setting exists\n   *\n   * @param key Setting key to check\n   * @returns True if the setting exists\n   */\n  has(key: string): boolean {\n    return this.settings.some((s) => s.key === key);\n  }\n\n  /**\n   * 🔍 Get all settings\n   *\n   * @returns Copy of all settings\n   */\n  getAll(): AppSettings {\n    return [...this.settings];\n  }\n\n  /**\n   * 🔍 Get a setting value with type safety\n   *\n   * @param key Setting key to get\n   * @param defaultValue Default value if setting doesn't exist or is undefined\n   * @returns Setting value or default value\n   *\n   * @example\n   * ```typescript\n   * const lineWidth = settings.get<number>('line_width', 30);\n   * const language = settings.get<string>('transcribe_language', 'English');\n   * ```\n   */\n  get<T = any>(key: string, defaultValue?: T): T {\n    const setting = this.settings.find((s) => s.key === key);\n\n    if (setting && setting.value !== undefined) {\n      return setting.value as T;\n    }\n\n    return defaultValue as T;\n  }\n\n  /**\n   * 🎛️ Get an MentraOS system setting value with optional default\n   *\n   * @param key MentraOS setting key (e.g., 'metricSystemEnabled', 'brightness')\n   * @param defaultValue Default value to return if the setting is not found\n   * @returns The setting value or the default value\n   *\n   * @example\n   * ```typescript\n   * const isMetric = settings.getMentraOS<boolean>('metricSystemEnabled', false);\n   * const brightness = settings.getMentraOS<number>('brightness', 50);\n   * ```\n   */\n  getMentraOS<T = any>(key: string, defaultValue?: T): T {\n    const value = this.mentraosSettings[key];\n\n    if (value !== undefined) {\n      return value as T;\n    }\n\n    return defaultValue as T;\n  }\n\n  /**\n   * 🔍 Find a setting by key\n   *\n   * @param key Setting key to find\n   * @returns Setting object or undefined\n   */\n  getSetting(key: string): AppSetting | undefined {\n    return this.settings.find((s) => s.key === key);\n  }\n\n  /**\n   * 🔄 Fetch settings from the cloud\n   * This is generally not needed since settings are automatically kept in sync,\n   * but can be used to force a refresh if needed.\n   *\n   * @returns Promise that resolves to the updated settings\n   * @throws Error if the API client is not configured or the request fails\n   */\n  async fetch(): Promise<AppSettings> {\n    if (!this.apiClient) {\n      throw new Error(\"Settings API client is not configured\");\n    }\n\n    try {\n      const newSettings = await this.apiClient.fetchSettings();\n      this.updateSettings(newSettings);\n      return this.settings;\n    } catch (error) {\n      console.error(\"Error fetching settings:\", error);\n      throw error;\n    }\n  }\n\n  /**\n   * 🎛️ Listen for changes to a specific MentraOS setting (e.g., metricSystemEnabled)\n   *\n   * @param key The mentraosSettings key to listen for (e.g., 'metricSystemEnabled')\n   * @param handler Function to call when the value changes\n   * @returns Function to remove the listener\n   *\n   * @example\n   * ```typescript\n   * settings.onMentraosChange('metricSystemEnabled', (isMetric, wasMetric) => {\n   *   console.log(`Units changed: ${wasMetric ? 'metric' : 'imperial'} → ${isMetric ? 'metric' : 'imperial'}`);\n   * });\n   * ```\n   */\n  onMentraosChange<T = any>(\n    key: string,\n    handler: SettingValueChangeHandler<T>,\n  ): () => void {\n    return this.onMentraosSettingChange(key, handler);\n  }\n\n  /**\n   * Listen for changes to a specific MentraOS setting (e.g., metricSystemEnabled)\n   * This is a convenience wrapper for onValueChange for well-known mentraosSettings keys.\n   * @param key The mentraosSettings key to listen for (e.g., 'metricSystemEnabled')\n   * @param handler Function to call when the value changes\n   * @returns Function to remove the listener\n   * @deprecated Use onMentraosChange instead\n   */\n  onMentraosSettingsChange<T = any>(\n    key: string,\n    handler: SettingValueChangeHandler<T>,\n  ): () => void {\n    return this.onMentraosSettingChange(key, handler);\n  }\n\n  /**\n   * Update the current MentraOS settings\n   * Compares new and old values, emits per-key events, and updates stored values.\n   * @param newSettings The new MentraOS settings object\n   */\n  updateMentraosSettings(newSettings: Record<string, any>): void {\n    const oldSettings = this.mentraosSettings;\n    logger.debug(\n      { newSettings },\n      `[SettingsManager] Updating MentraOS settings. New settings`,\n    );\n    for (const key of Object.keys(newSettings)) {\n      const oldValue = oldSettings[key];\n      const newValue = newSettings[key];\n      if (oldValue !== newValue) {\n        logger.info(\n          `[SettingsManager] MentraOS setting '${key}' changed: ${oldValue} -> ${newValue}. Emitting event.`,\n        );\n        this.mentraosEmitter.emit(`augmentos:value:${key}`, newValue, oldValue);\n      }\n    }\n    // Also handle keys that might have been removed from newSettings but existed in oldSettings\n    for (const key of Object.keys(oldSettings)) {\n      if (!(key in newSettings)) {\n        logger.info(\n          `[SettingsManager] MentraOS setting '${key}' removed. Old value: ${oldSettings[key]}. Emitting event with undefined newValue.`,\n        );\n        this.mentraosEmitter.emit(\n          `augmentos:value:${key}`,\n          undefined,\n          oldSettings[key],\n        );\n      }\n    }\n    this.mentraosSettings = { ...newSettings };\n    logger.debug(\n      { mentraosSettings: this.mentraosSettings },\n      `[SettingsManager] Finished updating MentraOS settings. Current state:`,\n    );\n  }\n\n  /**\n   * Subscribe to changes for a specific MentraOS setting (e.g., 'metricSystemEnabled')\n   * @param key The MentraOS setting key to listen for\n   * @param handler Function to call when the value changes (newValue, oldValue)\n   * @returns Function to remove the listener\n   */\n  onMentraosSettingChange<T = any>(\n    key: string,\n    handler: (newValue: T, oldValue: T) => void,\n  ): () => void {\n    const eventName = `augmentos:value:${key}`;\n    logger.info(\n      `[SettingsManager] Registering handler for MentraOS setting '${key}' on event '${eventName}'.`,\n    );\n    this.mentraosEmitter.on(eventName, (...args) => {\n      logger.info(\n        { args },\n        `[SettingsManager] MentraOS setting '${key}' event fired. Args:`,\n      );\n      handler(...(args as [T, T]));\n    });\n\n    if (this.subscribeFn) {\n      const subscriptionKey = `augmentos:${key}`;\n      logger.info(\n        `[SettingsManager] Calling subscribeFn for stream '${subscriptionKey}'.`,\n      );\n      this.subscribeFn([subscriptionKey])\n        .then(() => {\n          logger.info(\n            `[SettingsManager] subscribeFn resolved for stream '${subscriptionKey}'.`,\n          );\n        })\n        .catch((err) => {\n          logger.error(\n            `[SettingsManager] subscribeFn failed for stream '${subscriptionKey}':`,\n            err,\n          );\n        });\n    } else {\n      logger.warn(\n        `[SettingsManager] 'subscribeFn' not provided. Cannot auto-subscribe for MentraOS setting '${key}'. Manual App subscription might be required.`,\n      );\n    }\n\n    return () => {\n      logger.info(\n        `[SettingsManager] Unregistering handler for MentraOS setting '${key}' from event '${eventName}'.`,\n      );\n      this.mentraosEmitter.off(\n        eventName,\n        handler as (newValue: unknown, oldValue: unknown) => void,\n      );\n    };\n  }\n\n  /**\n   * Get the current value of an MentraOS setting\n   */\n  getMentraosSetting<T = any>(key: string, defaultValue?: T): T {\n    console.log(\n      `[SettingsManager] Getting MentraOS setting '${key}' with settings:`,\n      this.mentraosSettings,\n    );\n    if (key in this.mentraosSettings) {\n      return this.mentraosSettings[key] as T;\n    }\n    return defaultValue as T;\n  }\n}\n","/**\n * 🔌 API Client Module\n *\n * Provides HTTP API access to MentraOS Cloud services.\n * Automatically uses the correct server URL derived from the WebSocket URL.\n */\n\n/**\n * Convert a WebSocket URL to a HTTP/HTTPS URL\n *\n * @param wsUrl WebSocket URL to convert\n * @returns HTTP URL equivalent\n */\nexport function wsUrlToHttpUrl(wsUrl?: string): string | undefined {\n  if (!wsUrl) return undefined;\n\n  try {\n    // Parse the WebSocket URL\n    const url = new URL(wsUrl);\n\n    // Change protocol from ws/wss to http/https\n    const protocol = url.protocol === \"wss:\" ? \"https:\" : \"http:\";\n\n    // Recreate the URL with the new protocol\n    return `${protocol}//${url.host}`;\n  } catch (error) {\n    console.error(\"Error converting WebSocket URL to HTTP URL:\", error);\n    return undefined;\n  }\n}\n\n/**\n * API client class for making HTTP requests to MentraOS Cloud\n */\nexport class ApiClient {\n  private baseUrl: string | undefined;\n  private packageName: string;\n  private userId: string | undefined;\n\n  /**\n   * Create a new API client\n   *\n   * @param packageName App package name\n   * @param wsUrl WebSocket URL (optional, can be set later)\n   * @param userId User ID (optional, for authenticated requests)\n   */\n  constructor(packageName: string, wsUrl?: string, userId?: string) {\n    this.packageName = packageName;\n    this.userId = userId;\n\n    if (wsUrl) {\n      this.baseUrl = wsUrlToHttpUrl(wsUrl);\n    }\n  }\n\n  /**\n   * Set the WebSocket URL to derive the HTTP base URL\n   *\n   * @param wsUrl WebSocket URL\n   */\n  setWebSocketUrl(wsUrl: string): void {\n    this.baseUrl = wsUrlToHttpUrl(wsUrl);\n  }\n\n  /**\n   * Set the user ID for authenticated requests\n   *\n   * @param userId User ID\n   */\n  setUserId(userId: string): void {\n    this.userId = userId;\n  }\n\n  /**\n   * Fetch settings from MentraOS Cloud\n   *\n   * @returns Promise resolving to settings array\n   * @throws Error if client is not configured correctly or if request fails\n   */\n  async fetchSettings(): Promise<any[]> {\n    if (!this.baseUrl) {\n      throw new Error(\"API client is not configured with a base URL\");\n    }\n\n    if (!this.userId) {\n      throw new Error(\"User ID is required for fetching settings\");\n    }\n\n    const url = `${this.baseUrl}/appsettings/user/${this.packageName}`;\n\n    try {\n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: {\n          Authorization: `Bearer ${this.userId}`,\n          \"Content-Type\": \"application/json\",\n        },\n      });\n\n      if (!response.ok) {\n        throw new Error(\n          `Failed to fetch settings: ${response.status} ${response.statusText}`,\n        );\n      }\n\n      const data = (await response.json()) as { settings: any[] };\n      return data.settings || [];\n    } catch (error) {\n      console.error(\"Error fetching settings:\", error);\n      throw error;\n    }\n  }\n}\n","import pino from \"pino\"\n\n// Constants and configuration\nconst BETTERSTACK_SOURCE_TOKEN = process.env.BETTERSTACK_SOURCE_TOKEN\nconst BETTERSTACK_ENDPOINT = process.env.BETTERSTACK_ENDPOINT || \"https://s1311181.eu-nbg-2.betterstackdata.com\"\nconst NODE_ENV = process.env.NODE_ENV || \"development\"\nconst PORTER_APP_NAME = process.env.PORTER_APP_NAME || \"cloud-local\"\n\n// Determine log level based on environment\nconst LOG_LEVEL = NODE_ENV === \"production\" ? \"info\" : \"debug\"\n\n// Setup streams array for Pino multistream\nconst streams: pino.StreamEntry[] = []\n\n// Use pretty print in development for better readability\n// if (PRETTY_PRINT && NODE_ENV !== 'production') {\n// Pretty transport for development\nconst prettyTransport = pino.transport({\n  target: \"pino-pretty\",\n  options: {\n    colorize: true,\n    translateTime: \"SYS:standard\",\n    ignore: \"pid,hostname,env,module,server\",\n    messageFormat: \"{msg}\",\n    errorProps: \"*\",\n    customPrettifiers: {\n      // Add custom prettifiers here if needed\n    },\n  },\n})\n\nstreams.push({\n  stream: prettyTransport,\n  level: LOG_LEVEL,\n})\n// } else {\n//   // Plain console in production (JSON format)\n//   streams.push({\n//     stream: process.stdout,\n//     level: LOG_LEVEL,\n//   });\n// }\n\n// Add BetterStack transport if token is provided\nif (BETTERSTACK_SOURCE_TOKEN) {\n  try {\n    const betterStackTransport = pino.transport({\n      target: \"@logtail/pino\",\n      options: {\n        sourceToken: BETTERSTACK_SOURCE_TOKEN,\n        options: {endpoint: BETTERSTACK_ENDPOINT},\n      },\n    })\n\n    streams.push({\n      stream: betterStackTransport,\n      level: LOG_LEVEL,\n    })\n  } catch (error) {\n    console.warn(\"BetterStack logging unavailable:\", error instanceof Error ? error.message : error)\n  }\n}\n\n// Create multistream\nconst multistream = pino.multistream(streams)\n\n/**\n * Configuration for the root logger\n */\nconst baseLoggerOptions: pino.LoggerOptions = {\n  level: LOG_LEVEL,\n  base: {\n    env: NODE_ENV,\n    server: PORTER_APP_NAME,\n  },\n  timestamp: pino.stdTimeFunctions.isoTime,\n}\n\n// Create the root logger with multiple streams\nexport const logger = pino(baseLoggerOptions, multistream)\n\n// Flush logger on process exit\n// process.on('beforeExit', async () => {\n//   logger.flush(); // Flush the root logger\n//   console.log('Logger flushed before exit');\n// });\n\n// Default export is the logger\nexport default logger\n","import {locationWarnLog} from \"../../../utils/permissions-utils\"\n\nimport {AppSession} from \"..\"\nimport {AppToCloudMessageType, LocationUpdate, LocationStreamRequest} from \"../../../types\"\n\nexport class LocationManager {\n  private session: AppSession\n  private lastLocationCleanupHandler: () => void = () => {}\n\n  constructor(session: AppSession) {\n    this.session = session\n  }\n\n  // subscribes to the continuous location stream with a specified accuracy tier\n  public subscribeToStream(\n    options: {\n      accuracy:\n        | \"standard\"\n        | \"high\"\n        | \"realtime\"\n        | \"tenMeters\"\n        | \"hundredMeters\"\n        | \"kilometer\"\n        | \"threeKilometers\"\n        | \"reduced\"\n    },\n    handler: (data: LocationUpdate) => void,\n  ): () => void {\n    //Checking for permission location from dev console:\n    locationWarnLog(this.session.getHttpsServerUrl() || \"\", this.session.getPackageName(), this.subscribeToStream.name)\n\n    const subscription: LocationStreamRequest = {\n      stream: \"location_stream\",\n      rate: options.accuracy,\n    }\n    this.session.subscribe(subscription)\n    this.lastLocationCleanupHandler = this.session.events.onLocation(handler)\n    return this.lastLocationCleanupHandler\n  }\n\n  // unsubscribes from the continuous location stream\n  public unsubscribeFromStream(): void {\n    if (this.lastLocationCleanupHandler) {\n      this.lastLocationCleanupHandler()\n      this.lastLocationCleanupHandler = () => {}\n    } else {\n      this.session.unsubscribe(\"location_stream\")\n    }\n  }\n\n  // performs a one-time, intelligent poll for a location fix\n  public async getLatestLocation(options: {accuracy: string}): Promise<LocationUpdate> {\n    return new Promise((resolve, reject) => {\n      const requestId = `poll_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`\n\n      // listens for a location update with a matching correlationId\n      const unsubscribe = this.session.events.on(\"location_update\", (data: LocationUpdate) => {\n        if (data.correlationId === requestId) {\n          unsubscribe() // clean up the listener\n          resolve(data)\n        }\n      })\n\n      // sends the poll request message to the cloud\n      this.session.sendMessage({\n        type: AppToCloudMessageType.LOCATION_POLL_REQUEST,\n        correlationId: requestId,\n        packageName: this.session.getPackageName(),\n        sessionId: this.session.getSessionId(),\n        accuracy: options.accuracy,\n      })\n\n      // sets a timeout to prevent the promise from hanging indefinitely\n      setTimeout(() => {\n        unsubscribe()\n        reject(\"Location poll request timed out\")\n      }, 15000) // 15 second timeout\n    })\n  }\n}\n","/**\n * 📷 Camera Module\n *\n * Unified camera functionality for App Sessions.\n * Handles both photo requests and RTMP streaming from connected glasses.\n */\n\nimport {\n  PhotoRequest,\n  PhotoData,\n  AppToCloudMessageType,\n  RtmpStreamRequest,\n  RtmpStreamStopRequest,\n  RtmpStreamStatus,\n  isRtmpStreamStatus,\n  ManagedStreamStatus,\n  StreamStatusCheckResponse,\n} from \"../../../types\";\nimport { VideoConfig, AudioConfig, StreamConfig, StreamStatusHandler } from \"../../../types/rtmp-stream\";\nimport { StreamType } from \"../../../types/streams\";\nimport { Logger } from \"pino\";\nimport { CameraManagedExtension, ManagedStreamOptions, ManagedStreamResult } from \"./camera-managed-extension\";\nimport { cameraWarnLog } from \"../../../utils/permissions-utils\";\n\n/**\n * Options for photo requests\n */\nexport interface PhotoRequestOptions {\n  /** Whether to save the photo to the device gallery */\n  saveToGallery?: boolean;\n  /** Custom webhook URL to override the TPA's default webhookUrl */\n  customWebhookUrl?: string;\n  /** Authentication token for custom webhook authentication */\n  authToken?: string;\n  /**\n   * Desired photo size. All sizes are optimized for fast transfer.\n   * - small: 640x480 (VGA) - ultra-fast transfers\n   * - medium: 1280x720 (720p) - good balance (default)\n   * - large: 1920x1080 (1080p) - high quality\n   * - full: native sensor resolution - maximum detail (slower transfer)\n   */\n  size?: \"small\" | \"medium\" | \"large\" | \"full\";\n  /** Image compression level for upload optimization. Defaults to \"none\". */\n  compress?: \"none\" | \"medium\" | \"heavy\";\n}\n\n/**\n * Configuration options for an RTMP stream\n */\nexport interface RtmpStreamOptions {\n  /** The RTMP URL to stream to (e.g., rtmp://server.example.com/live/stream-key) */\n  rtmpUrl: string;\n  /** Optional video configuration settings */\n  video?: VideoConfig;\n  /** Optional audio configuration settings */\n  audio?: AudioConfig;\n  /** Optional stream configuration settings */\n  stream?: StreamConfig;\n}\n\n/**\n * 📷 Camera Module Implementation\n *\n * Unified camera management for App Sessions.\n * Provides methods for:\n * - 📸 Requesting photos from glasses\n * - 📹 Starting/stopping RTMP streams\n * - 🔍 Monitoring photo and stream status\n * - 🧹 Cleanup and cancellation\n *\n * @example\n * ```typescript\n * // Request a photo\n * const photoData = await session.camera.requestPhoto({ saveToGallery: true });\n *\n * // Start streaming\n * await session.camera.startStream({ rtmpUrl: 'rtmp://example.com/live/key' });\n *\n * // Monitor stream status\n * session.camera.onStreamStatus((status) => {\n *   console.log('Stream status:', status.status);\n * });\n *\n * // Stop streaming\n * await session.camera.stopStream();\n * ```\n */\nexport class CameraModule {\n  private session: any; // Reference to AppSession\n  private packageName: string;\n  private sessionId: string;\n  private logger: Logger;\n\n  // Photo functionality\n  // NOTE: Pending photo requests are now stored at AppServer level, not here.\n  // This allows O(1) lookup when HTTP responses arrive and survives session reconnections.\n  // See: cloud/issues/019-sdk-photo-request-architecture\n\n  // Streaming functionality\n  private isStreaming: boolean = false;\n  private currentStreamUrl?: string;\n  private currentStreamState?: RtmpStreamStatus;\n\n  // Managed streaming extension\n  private managedExtension: CameraManagedExtension;\n\n  /**\n   * Create a new CameraModule\n   *\n   * @param session - Reference to the parent AppSession\n   * @param packageName - The App package name\n   * @param sessionId - The current session ID\n   * @param logger - Logger instance for debugging\n   */\n  constructor(session: any, packageName: string, sessionId: string, logger?: Logger) {\n    this.session = session;\n    this.packageName = packageName;\n    this.sessionId = sessionId;\n    this.logger = logger || (console as any);\n\n    // Initialize managed extension\n    this.managedExtension = new CameraManagedExtension(session, packageName, sessionId, this.logger);\n  }\n\n  // =====================================\n  // 📸 Photo Functionality\n  // =====================================\n\n  /**\n   * 📸 Request a photo from the connected glasses\n   *\n   * @param options - Optional configuration for the photo request\n   * @returns Promise that resolves with the actual photo data\n   *\n   * @example\n   * ```typescript\n   * // Request a photo\n   * const photo = await session.camera.requestPhoto();\n   *\n   * // Request a photo with custom webhook URL and authentication\n   * const photo = await session.camera.requestPhoto({\n   *   customWebhookUrl: 'https://my-custom-endpoint.com/photo-upload',\n   *   authToken: 'your-auth-token-here'\n   * });\n   * ```\n   */\n  async requestPhoto(options?: PhotoRequestOptions): Promise<PhotoData> {\n    return new Promise((resolve, reject) => {\n      const baseUrl = this.session?.getHttpsServerUrl?.() || \"\";\n      cameraWarnLog(baseUrl, this.packageName, \"requestPhoto\");\n      try {\n        // Generate unique request ID\n        const requestId = `photo_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n\n        // Register the photo request at AppServer level (single source of truth)\n        // This allows O(1) lookup when HTTP response arrives and survives session reconnections\n        // See: cloud/issues/019-sdk-photo-request-architecture\n        this.session.appServer.registerPhotoRequest(requestId, {\n          userId: this.session.userId,\n          sessionId: this.sessionId,\n          session: this.session,\n          resolve,\n          reject: (error: Error) => reject(error.message),\n          timestamp: Date.now(),\n        });\n\n        // Create photo request message\n        const message: PhotoRequest = {\n          type: AppToCloudMessageType.PHOTO_REQUEST,\n          packageName: this.packageName,\n          sessionId: this.sessionId,\n          requestId,\n          timestamp: new Date(),\n          saveToGallery: options?.saveToGallery || false,\n          customWebhookUrl: options?.customWebhookUrl,\n          authToken: options?.authToken,\n          size: options?.size || \"medium\",\n          compress: options?.compress || \"none\",\n        };\n\n        // Send request to cloud\n        this.session.sendMessage(message);\n\n        this.logger.info(\n          {\n            requestId,\n            saveToGallery: options?.saveToGallery,\n            hasCustomWebhook: !!options?.customWebhookUrl,\n            hasAuthToken: !!options?.authToken,\n          },\n          `📸 Photo request sent`,\n        );\n\n        // If using custom webhook URL, resolve immediately since photo will be uploaded directly to custom endpoint\n        if (options?.customWebhookUrl) {\n          this.logger.info(\n            { requestId, customWebhookUrl: options.customWebhookUrl },\n            `📸 Using custom webhook URL - resolving promise immediately since photo will be uploaded directly to custom endpoint`,\n          );\n\n          // Complete the request at AppServer level and resolve with mock data\n          const pending = this.session.appServer.completePhotoRequest(requestId);\n          if (pending) {\n            const mockPhotoData: PhotoData = {\n              buffer: Buffer.from([]), // Empty buffer since we don't have the actual photo\n              mimeType: \"image/jpeg\",\n              filename: \"photo.jpg\",\n              requestId,\n              size: 0,\n              timestamp: new Date(),\n            };\n            pending.resolve(mockPhotoData);\n          }\n          return;\n        }\n\n        // Timeout is now handled at AppServer level in registerPhotoRequest()\n      } catch (error: unknown) {\n        const errorMessage = error instanceof Error ? error.message : String(error);\n        reject(`Failed to request photo: ${errorMessage}`);\n      }\n    });\n  }\n\n  // NOTE: handlePhotoReceived() and handlePhotoError() have been removed.\n  // Photo responses are now handled directly by AppServer's /photo-upload endpoint,\n  // which resolves/rejects the promise stored in AppServer.pendingPhotoRequests.\n  // See: cloud/issues/019-sdk-photo-request-architecture\n\n  /**\n   * 🔍 Check if there's a pending photo request for the given request ID\n   * @deprecated Photo requests are now managed at AppServer level. This method delegates to AppServer.\n   *\n   * @param requestId - The request ID to check\n   * @returns true if there's a pending request\n   */\n  hasPhotoPendingRequest(requestId: string): boolean {\n    return this.session.appServer.getPhotoRequest(requestId) !== undefined;\n  }\n\n  /**\n   * ❌ Cancel a pending photo request\n   * @deprecated Photo requests are now managed at AppServer level. This method delegates to AppServer.\n   *\n   * @param requestId - The request ID to cancel\n   * @returns true if the request was cancelled, false if it wasn't found\n   */\n  cancelPhotoRequest(requestId: string): boolean {\n    const pending = this.session.appServer.completePhotoRequest(requestId);\n    if (pending) {\n      pending.reject(new Error(\"Photo request cancelled\"));\n      this.logger.info({ requestId }, `📸 Photo request cancelled`);\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * 🧹 Cancel all pending photo requests for this session\n   * @deprecated Photo requests are now managed at AppServer level. Use AppServer.cleanupPhotoRequestsForSession() instead.\n   *\n   * @returns Number of requests that were cancelled (always 0, cleanup happens at AppServer level)\n   */\n  cancelAllPhotoRequests(): number {\n    // Photo request cleanup is now handled by AppServer.cleanupPhotoRequestsForSession()\n    // which is called when the session permanently disconnects\n    this.logger.debug(`📸 cancelAllPhotoRequests called - cleanup now happens at AppServer level`);\n    return 0;\n  }\n\n  // =====================================\n  // 📹 Streaming Functionality\n  // =====================================\n\n  /**\n   * 📹 Start an RTMP stream to the specified URL\n   *\n   * @param options - Configuration options for the stream\n   * @returns Promise that resolves when the stream request is sent (not when streaming begins)\n   *\n   * @example\n   * ```typescript\n   * await session.camera.startStream({\n   *   rtmpUrl: 'rtmp://live.example.com/stream/key',\n   *   video: { resolution: '1920x1080', bitrate: 5000 },\n   *   audio: { bitrate: 128 }\n   * });\n   * ```\n   */\n  async startStream(options: RtmpStreamOptions): Promise<void> {\n    this.logger.info({ rtmpUrl: options.rtmpUrl }, `📹 RTMP stream request starting`);\n\n    cameraWarnLog(this.session.getHttpsServerUrl?.(), this.packageName, \"startStream\");\n\n    if (!options.rtmpUrl) {\n      throw new Error(\"rtmpUrl is required\");\n    }\n\n    if (this.isStreaming) {\n      this.logger.error(\n        {\n          currentStreamUrl: this.currentStreamUrl,\n          requestedUrl: options.rtmpUrl,\n        },\n        `📹 Already streaming error`,\n      );\n      throw new Error(\"Already streaming. Stop the current stream before starting a new one.\");\n    }\n\n    // Create stream request message\n    const message: RtmpStreamRequest = {\n      type: AppToCloudMessageType.RTMP_STREAM_REQUEST,\n      packageName: this.packageName,\n      sessionId: this.sessionId,\n      rtmpUrl: options.rtmpUrl,\n      video: options.video,\n      audio: options.audio,\n      stream: options.stream,\n      timestamp: new Date(),\n    };\n\n    // Save stream URL for reference\n    this.currentStreamUrl = options.rtmpUrl;\n\n    // Send the request\n    try {\n      this.session.sendMessage(message);\n      this.isStreaming = true;\n\n      this.logger.info({ rtmpUrl: options.rtmpUrl }, `📹 RTMP stream request sent successfully`);\n      return Promise.resolve();\n    } catch (error) {\n      this.logger.error({ error, rtmpUrl: options.rtmpUrl }, `📹 Failed to send RTMP stream request`);\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      return Promise.reject(`Failed to request RTMP stream: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 🛑 Stop the current RTMP stream\n   *\n   * @returns Promise that resolves when the stop request is sent\n   *\n   * @example\n   * ```typescript\n   * await session.camera.stopStream();\n   * ```\n   */\n  async stopStream(): Promise<void> {\n    this.logger.info(\n      {\n        isCurrentlyStreaming: this.isStreaming,\n        currentStreamUrl: this.currentStreamUrl,\n      },\n      `📹 RTMP stream stop request`,\n    );\n\n    if (!this.isStreaming) {\n      this.logger.info(`📹 Not streaming - no-op`);\n      // Not an error - just a no-op if not streaming\n      return Promise.resolve();\n    }\n\n    // Create stop request message\n    const message: RtmpStreamStopRequest = {\n      type: AppToCloudMessageType.RTMP_STREAM_STOP,\n      packageName: this.packageName,\n      sessionId: this.sessionId,\n      streamId: this.currentStreamState?.streamId, // Include streamId if available\n      timestamp: new Date(),\n    };\n\n    // Send the request\n    try {\n      this.session.sendMessage(message);\n      return Promise.resolve();\n    } catch (error) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      return Promise.reject(`Failed to stop RTMP stream: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * 🔍 Check if currently streaming\n   *\n   * @returns True if a stream is active or initializing\n   */\n  isCurrentlyStreaming(): boolean {\n    return this.isStreaming;\n  }\n\n  /**\n   * 📍 Get the URL of the current stream (if any)\n   *\n   * @returns The RTMP URL of the current stream, or undefined if not streaming\n   */\n  getCurrentStreamUrl(): string | undefined {\n    return this.currentStreamUrl;\n  }\n\n  /**\n   * 📊 Get the current stream status\n   *\n   * @returns The current stream status, or undefined if not available\n   */\n  getStreamStatus(): RtmpStreamStatus | undefined {\n    return this.currentStreamState;\n  }\n\n  /**\n   * 📺 Subscribe to RTMP stream status updates\n   * This uses the standard stream subscription mechanism\n   */\n  subscribeToStreamStatusUpdates(): void {\n    if (this.session) {\n      this.session.subscribe(StreamType.RTMP_STREAM_STATUS);\n    } else {\n      this.logger.error(\"Cannot subscribe to status updates: session reference not available\");\n    }\n  }\n\n  /**\n   * 📺 Unsubscribe from RTMP stream status updates\n   */\n  unsubscribeFromStreamStatusUpdates(): void {\n    if (this.session) {\n      this.session.unsubscribe(StreamType.RTMP_STREAM_STATUS);\n    }\n  }\n\n  /**\n   * 👂 Listen for stream status updates using the standard event system\n   * @param handler - Function to call when stream status changes\n   * @returns Cleanup function to remove the handler\n   *\n   * @example\n   * ```typescript\n   * const cleanup = session.camera.onStreamStatus((status) => {\n   *   console.log('Stream status:', status.status);\n   *   if (status.status === 'error') {\n   *     console.error('Stream error:', status.errorDetails);\n   *   }\n   * });\n   *\n   * // Later, cleanup the listener\n   * cleanup();\n   * ```\n   */\n  onStreamStatus(handler: StreamStatusHandler): () => void {\n    if (!this.session) {\n      this.logger.error(\"Cannot listen for status updates: session reference not available\");\n      return () => {};\n    }\n\n    this.subscribeToStreamStatusUpdates();\n    return this.session.on(StreamType.RTMP_STREAM_STATUS, handler);\n  }\n\n  /**\n   * 🔄 Update internal stream state based on a status message\n   * For internal use by AppSession\n   * @param message - The status message from the cloud\n   * @internal This method is used internally by AppSession\n   */\n  updateStreamState(message: any): void {\n    this.logger.debug(\n      {\n        messageType: message?.type,\n        messageStatus: message?.status,\n        currentIsStreaming: this.isStreaming,\n      },\n      `📹 Stream state update`,\n    );\n\n    // Verify this is a valid stream response\n    if (!isRtmpStreamStatus(message)) {\n      this.logger.warn({ message }, `📹 Received invalid stream status message`);\n      return;\n    }\n\n    // Convert to StreamStatus format\n    const status: RtmpStreamStatus = {\n      type: message.type,\n      streamId: message.streamId,\n      status: message.status,\n      errorDetails: message.errorDetails,\n      appId: message.appId,\n      stats: message.stats,\n      timestamp: message.timestamp || new Date(),\n    };\n\n    this.logger.info(\n      {\n        streamId: status.streamId,\n        oldStatus: this.currentStreamState?.status,\n        newStatus: status.status,\n        wasStreaming: this.isStreaming,\n      },\n      `📹 Stream status processed`,\n    );\n\n    // Update local state based on status\n    if (status.status === \"stopped\" || status.status === \"error\" || status.status === \"timeout\") {\n      this.logger.info(\n        {\n          status: status.status,\n          wasStreaming: this.isStreaming,\n        },\n        `📹 Stream stopped - updating local state`,\n      );\n      this.isStreaming = false;\n      this.currentStreamUrl = undefined;\n    }\n\n    // Save the latest status\n    this.currentStreamState = status;\n  }\n\n  // =====================================\n  // 📹 Managed Streaming Functionality\n  // =====================================\n\n  /**\n   * 📹 Start a managed stream\n   *\n   * The cloud handles the RTMP endpoint and returns HLS/DASH URLs for viewing.\n   * Multiple apps can consume the same managed stream simultaneously.\n   *\n   * @param options - Configuration options for the managed stream\n   * @returns Promise that resolves with viewing URLs when the stream is ready\n   *\n   * @example\n   * ```typescript\n   * const urls = await session.camera.startManagedStream({\n   *   quality: '720p',\n   *   enableWebRTC: true\n   * });\n   * console.log('HLS URL:', urls.hlsUrl);\n   * ```\n   */\n  async startManagedStream(options?: ManagedStreamOptions): Promise<ManagedStreamResult> {\n    return this.managedExtension.startManagedStream(options);\n  }\n\n  /**\n   * 🛑 Stop the current managed stream\n   *\n   * This will stop streaming for this app only. If other apps are consuming\n   * the same managed stream, it will continue for them.\n   *\n   * @returns Promise that resolves when the stop request is sent\n   */\n  async stopManagedStream(): Promise<void> {\n    return this.managedExtension.stopManagedStream();\n  }\n\n  /**\n   * 🔔 Register a handler for managed stream status updates\n   *\n   * @param handler - Function to call when stream status changes\n   * @returns Cleanup function to unregister the handler\n   */\n  onManagedStreamStatus(handler: (status: ManagedStreamStatus) => void): () => void {\n    return this.managedExtension.onManagedStreamStatus(handler);\n  }\n\n  /**\n   * 📊 Check if currently managed streaming\n   *\n   * @returns true if a managed stream is active\n   */\n  isManagedStreamActive(): boolean {\n    return this.managedExtension.isManagedStreamActive();\n  }\n\n  /**\n   * 🔗 Get current managed stream URLs\n   *\n   * @returns Current stream URLs or undefined if not streaming\n   */\n  getManagedStreamUrls(): ManagedStreamResult | undefined {\n    return this.managedExtension.getManagedStreamUrls();\n  }\n\n  /**\n   * 🔍 Check for any existing streams (managed or unmanaged) for the current user\n   *\n   * This method checks if there's already an active stream for the current user,\n   * which is useful to avoid conflicts and to reconnect to existing streams.\n   *\n   * @returns Promise that resolves with stream information if a stream exists\n   *\n   * @example\n   * ```typescript\n   * const streamInfo = await session.camera.checkExistingStream();\n   * if (streamInfo.hasActiveStream) {\n   *   console.log('Stream type:', streamInfo.streamInfo?.type);\n   *   if (streamInfo.streamInfo?.type === 'managed') {\n   *     console.log('HLS URL:', streamInfo.streamInfo.hlsUrl);\n   *   } else {\n   *     console.log('RTMP URL:', streamInfo.streamInfo.rtmpUrl);\n   *   }\n   * }\n   * ```\n   */\n  async checkExistingStream(): Promise<{\n    hasActiveStream: boolean;\n    streamInfo?: {\n      type: \"managed\" | \"unmanaged\";\n      streamId: string;\n      status: string;\n      createdAt: Date;\n      // For managed streams\n      hlsUrl?: string;\n      dashUrl?: string;\n      webrtcUrl?: string;\n      previewUrl?: string;\n      thumbnailUrl?: string;\n      activeViewers?: number;\n      // For unmanaged streams\n      rtmpUrl?: string;\n      requestingAppId?: string;\n    };\n  }> {\n    return this.managedExtension.checkExistingStream();\n  }\n\n  /**\n   * Handle incoming stream status check response\n   * @internal\n   */\n  handleStreamCheckResponse(response: StreamStatusCheckResponse): void {\n    this.managedExtension.handleStreamCheckResponse(response);\n  }\n\n  /**\n   * Handle incoming managed stream status messages\n   * @internal\n   */\n  handleManagedStreamStatus(message: ManagedStreamStatus): void {\n    this.managedExtension.handleManagedStreamStatus(message);\n  }\n\n  // =====================================\n  // 🔧 General Utilities\n  // =====================================\n\n  /**\n   * 🔧 Update the session ID (used when reconnecting)\n   *\n   * @param newSessionId - The new session ID\n   * @internal This method is used internally by AppSession\n   */\n  updateSessionId(newSessionId: string): void {\n    this.sessionId = newSessionId;\n  }\n\n  /**\n   * 🧹 Cancel all pending requests and clean up resources\n   *\n   * @returns Object with counts of cancelled requests\n   */\n  cancelAllRequests(): { photoRequests: number } {\n    const photoRequests = this.cancelAllPhotoRequests();\n\n    // Stop streaming if active\n    if (this.isStreaming) {\n      this.stopStream().catch((error) => {\n        this.logger.error({ error }, \"Error stopping stream during cleanup\");\n      });\n    }\n\n    // Clean up managed extension\n    this.managedExtension.cleanup();\n\n    return { photoRequests };\n  }\n}\n\n// Re-export types for convenience\nexport { VideoConfig, AudioConfig, StreamConfig, StreamStatusHandler };\n","/**\n * 📷 Camera Module Managed Streaming Extension\n *\n * Extends the camera module with managed streaming capabilities.\n * Apps can request managed streams and receive HLS/DASH URLs without managing RTMP endpoints.\n */\n\nimport {\n  ManagedStreamRequest,\n  ManagedStreamStopRequest,\n  ManagedStreamStatus,\n  StreamStatusCheckRequest,\n  StreamStatusCheckResponse,\n  AppToCloudMessageType,\n  StreamType,\n  RestreamDestination,\n} from \"../../../types\"\nimport {VideoConfig, AudioConfig, StreamConfig} from \"../../../types/rtmp-stream\"\nimport {Logger} from \"pino\"\n\n/**\n * Configuration options for a managed stream\n */\nexport interface ManagedStreamOptions {\n  /** Stream quality preset */\n  quality?: \"720p\" | \"1080p\"\n  /** Enable WebRTC for ultra-low latency viewing */\n  enableWebRTC?: boolean\n  /** Optional video configuration settings */\n  video?: VideoConfig\n  /** Optional audio configuration settings */\n  audio?: AudioConfig\n  /** Optional stream configuration settings */\n  stream?: StreamConfig\n  /** Optional RTMP destinations to re-stream to (YouTube, Twitch, etc) */\n  restreamDestinations?: RestreamDestination[]\n}\n\n/**\n * Result returned when starting a managed stream\n */\nexport interface ManagedStreamResult {\n  /** HLS URL for viewing the stream */\n  hlsUrl: string\n  /** DASH URL for viewing the stream */\n  dashUrl: string\n  /** WebRTC URL if enabled */\n  webrtcUrl?: string\n  /** Cloudflare Stream player/preview URL for embedding */\n  previewUrl?: string\n  /** Thumbnail image URL */\n  thumbnailUrl?: string\n  /** Internal stream ID */\n  streamId: string\n}\n\n/**\n * 📹 Managed Streaming Extension for Camera Module\n *\n * Provides managed streaming capabilities where the cloud handles\n * RTMP endpoints and returns HLS/DASH URLs for viewing.\n *\n * @example\n * ```typescript\n * // Start a managed stream\n * const urls = await session.camera.startManagedStream({\n *   quality: '720p',\n *   enableWebRTC: true\n * });\n * console.log('HLS URL:', urls.hlsUrl);\n * console.log('DASH URL:', urls.dashUrl);\n * console.log('Player URL:', urls.previewUrl);\n * console.log('Thumbnail:', urls.thumbnailUrl);\n *\n * // Monitor managed stream status\n * session.camera.onManagedStreamStatus((status) => {\n *   console.log('Managed stream status:', status.status);\n * });\n *\n * // Stop managed stream\n * await session.camera.stopManagedStream();\n * ```\n */\nexport class CameraManagedExtension {\n  private session: any\n  private packageName: string\n  private sessionId: string\n  private logger: Logger\n\n  // Managed streaming state\n  private isManagedStreaming: boolean = false\n  private currentManagedStreamId?: string\n  private currentManagedStreamUrls?: ManagedStreamResult\n  private managedStreamStatus?: ManagedStreamStatus\n\n  // For tracking pending stream check requests\n  private pendingStreamChecks?: Map<\n    string,\n    {\n      resolve: (value: any) => void\n      timeoutId: NodeJS.Timeout\n    }\n  >\n\n  // Promise tracking for managed stream initialization\n  private pendingManagedStreamRequest?: {\n    resolve: (value: ManagedStreamResult) => void\n    reject: (reason?: any) => void\n  }\n\n  constructor(session: any, packageName: string, sessionId: string, logger: Logger) {\n    this.session = session\n    this.packageName = packageName\n    this.sessionId = sessionId\n    this.logger = logger.child({module: \"CameraManagedExtension\"})\n  }\n\n  /**\n   * 📹 Start a managed stream\n   *\n   * The cloud will handle the RTMP endpoint and return HLS/DASH URLs for viewing.\n   * Multiple apps can consume the same managed stream simultaneously.\n   *\n   * @param options - Configuration options for the managed stream\n   * @returns Promise that resolves with viewing URLs when the stream is ready\n   *\n   * @example\n   * ```typescript\n   * const urls = await session.camera.startManagedStream({\n   *   quality: '1080p',\n   *   enableWebRTC: true,\n   *   video: { fps: 30 },\n   *   audio: { sampleRate: 48000 }\n   * });\n   *\n   * // Access all available URLs\n   * console.log('HLS URL:', urls.hlsUrl);\n   * console.log('DASH URL:', urls.dashUrl);\n   * console.log('Player URL:', urls.previewUrl);  // Embeddable player\n   * console.log('Thumbnail:', urls.thumbnailUrl);\n   * ```\n   */\n  async startManagedStream(options: ManagedStreamOptions = {}): Promise<ManagedStreamResult> {\n    this.logger.info({options}, \"📹 Managed stream request starting\")\n\n    if (this.isManagedStreaming) {\n      this.logger.error(\n        {\n          currentStreamId: this.currentManagedStreamId,\n        },\n        \"📹 Already managed streaming error\",\n      )\n      throw new Error(\"Already streaming. Stop the current managed stream before starting a new one.\")\n    }\n\n    // Create the request message\n    const request: ManagedStreamRequest = {\n      type: AppToCloudMessageType.MANAGED_STREAM_REQUEST,\n      packageName: this.packageName,\n      quality: options.quality,\n      enableWebRTC: options.enableWebRTC,\n      video: options.video,\n      audio: options.audio,\n      stream: options.stream,\n      restreamDestinations: options.restreamDestinations,\n    }\n\n    // Send the request\n    this.session.sendMessage(request)\n    this.isManagedStreaming = true\n\n    // Create promise to wait for URLs\n    return new Promise((resolve, reject) => {\n      this.pendingManagedStreamRequest = {resolve, reject}\n\n      // Set a timeout\n      setTimeout(() => {\n        if (this.pendingManagedStreamRequest) {\n          this.pendingManagedStreamRequest = undefined\n          this.isManagedStreaming = false\n          reject(new Error(\"Managed stream request timeout\"))\n        }\n      }, 30000) // 30 second timeout\n    })\n  }\n\n  /**\n   * 🛑 Stop the current managed stream\n   *\n   * This will stop streaming for this app only. If other apps are consuming\n   * the same managed stream, it will continue for them.\n   *\n   * @returns Promise that resolves when the stop request is sent\n   */\n  async stopManagedStream(): Promise<void> {\n    // Always send the stop request - the cloud will handle whether there's actually\n    // a stream to stop. This ensures the stop works even after reload/reconnection\n    this.logger.info(\n      {\n        streamId: this.currentManagedStreamId,\n        hasInternalState: this.isManagedStreaming,\n      },\n      \"📹 Sending managed stream stop request\",\n    )\n\n    const request: ManagedStreamStopRequest = {\n      type: AppToCloudMessageType.MANAGED_STREAM_STOP,\n      packageName: this.packageName,\n    }\n\n    this.session.sendMessage(request)\n\n    // Don't clean up state immediately - wait for the 'stopped' status from cloud\n    // This ensures we can retry stop if needed and maintains accurate state\n  }\n\n  /**\n   * 🔍 Check for any existing streams (managed or unmanaged) for the current user\n   *\n   * @returns Promise that resolves with stream information if a stream exists\n   *\n   * @example\n   * ```typescript\n   * const streamInfo = await session.camera.checkExistingStream();\n   * if (streamInfo.hasActiveStream) {\n   *   console.log('Stream type:', streamInfo.streamInfo?.type);\n   *   if (streamInfo.streamInfo?.type === 'managed') {\n   *     console.log('HLS URL:', streamInfo.streamInfo.hlsUrl);\n   *   }\n   * }\n   * ```\n   */\n  async checkExistingStream(): Promise<{\n    hasActiveStream: boolean\n    streamInfo?: {\n      type: \"managed\" | \"unmanaged\"\n      streamId: string\n      status: string\n      createdAt: Date\n      // For managed streams\n      hlsUrl?: string\n      dashUrl?: string\n      webrtcUrl?: string\n      previewUrl?: string\n      thumbnailUrl?: string\n      activeViewers?: number\n      // For unmanaged streams\n      rtmpUrl?: string\n      requestingAppId?: string\n    }\n  }> {\n    return new Promise((resolve) => {\n      // Store the resolver for the response\n      const requestId = `stream_check_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`\n\n      // Store a pending request that will be resolved when we get the response\n      if (!this.pendingStreamChecks) {\n        this.pendingStreamChecks = new Map()\n      }\n\n      const timeoutId = setTimeout(() => {\n        this.pendingStreamChecks?.delete(requestId)\n        resolve({hasActiveStream: false})\n      }, 5000) // 5 second timeout\n\n      this.pendingStreamChecks.set(requestId, {\n        resolve,\n        timeoutId,\n      })\n\n      // Send the check request with the requestId\n      const request: StreamStatusCheckRequest = {\n        type: AppToCloudMessageType.STREAM_STATUS_CHECK,\n        packageName: this.packageName,\n        sessionId: this.sessionId,\n      }\n\n      this.session.sendMessage(request)\n    })\n  }\n\n  /**\n   * 📊 Check if currently managed streaming\n   *\n   * @returns true if a managed stream is active\n   */\n  isManagedStreamActive(): boolean {\n    return this.isManagedStreaming\n  }\n\n  /**\n   * 🔗 Get current managed stream URLs\n   *\n   * @returns Current stream URLs or undefined if not streaming\n   */\n  getManagedStreamUrls(): ManagedStreamResult | undefined {\n    return this.currentManagedStreamUrls\n  }\n\n  /**\n   * 📊 Get current managed stream status\n   *\n   * @returns Current stream status or undefined\n   */\n  getManagedStreamStatus(): ManagedStreamStatus | undefined {\n    return this.managedStreamStatus\n  }\n\n  /**\n   * 🔔 Register a handler for managed stream status updates\n   *\n   * @param handler - Function to call when stream status changes\n   * @returns Cleanup function to unregister the handler\n   *\n   * @example\n   * ```typescript\n   * const cleanup = session.camera.onManagedStreamStatus((status) => {\n   *   console.log('Status:', status.status);\n   *   if (status.status === 'active') {\n   *     console.log('Stream is live!');\n   *   }\n   * });\n   *\n   * // Later, unregister the handler\n   * cleanup();\n   * ```\n   */\n  onManagedStreamStatus(handler: (status: ManagedStreamStatus) => void): () => void {\n    if (!this.session) {\n      this.logger.error(\"Cannot listen for managed status updates: session reference not available\")\n      return () => {}\n    }\n\n    this.session.subscribe(StreamType.MANAGED_STREAM_STATUS)\n\n    // Register the handler using the session's event system\n    return this.session.on(StreamType.MANAGED_STREAM_STATUS, handler)\n  }\n\n  /**\n   * Handle incoming stream status check response\n   * Called by the parent AppSession when a response is received\n   */\n  handleStreamCheckResponse(response: StreamStatusCheckResponse): void {\n    // Find and resolve any pending stream check\n    if (this.pendingStreamChecks && this.pendingStreamChecks.size > 0) {\n      const firstEntry = this.pendingStreamChecks.entries().next()\n      if (!firstEntry.done && firstEntry.value) {\n        const [requestId, pending] = firstEntry.value\n        if (pending) {\n          clearTimeout(pending.timeoutId)\n          this.pendingStreamChecks.delete(requestId)\n          pending.resolve(response)\n        }\n      }\n    }\n  }\n\n  /**\n   * Handle incoming managed stream status messages\n   * Called by the parent AppSession when messages are received\n   */\n  handleManagedStreamStatus(status: ManagedStreamStatus): void {\n    this.logger.info(\n      {\n        status: status.status,\n        streamId: status.streamId,\n      },\n      \"📹 Received managed stream status\",\n    )\n\n    this.managedStreamStatus = status\n\n    // Handle initializing status - stream is starting\n    if (status.status === \"initializing\" && status.streamId) {\n      this.isManagedStreaming = true\n      this.currentManagedStreamId = status.streamId\n    }\n\n    // Handle initial stream ready status\n    if (status.status === \"active\") {\n      // Always update state when stream is active\n      this.isManagedStreaming = true\n      this.currentManagedStreamId = status.streamId\n\n      if (status.hlsUrl && status.dashUrl) {\n        const result: ManagedStreamResult = {\n          hlsUrl: status.hlsUrl,\n          dashUrl: status.dashUrl,\n          webrtcUrl: status.webrtcUrl,\n          previewUrl: status.previewUrl,\n          thumbnailUrl: status.thumbnailUrl,\n          streamId: status.streamId || \"\",\n        }\n\n        this.currentManagedStreamUrls = result\n\n        // Resolve pending promise if exists\n        if (this.pendingManagedStreamRequest) {\n          this.pendingManagedStreamRequest.resolve(result)\n          this.pendingManagedStreamRequest = undefined\n        }\n      }\n    }\n\n    // Handle error status\n    if ((status.status === \"error\" || status.status === \"stopped\") && this.pendingManagedStreamRequest) {\n      this.pendingManagedStreamRequest.reject(new Error(status.message || \"Managed stream failed\"))\n      this.pendingManagedStreamRequest = undefined\n      this.isManagedStreaming = false\n      this.currentManagedStreamId = undefined\n      this.currentManagedStreamUrls = undefined\n    }\n\n    // Clean up on stopped status\n    if (status.status === \"stopped\") {\n      this.isManagedStreaming = false\n      this.currentManagedStreamId = undefined\n      this.currentManagedStreamUrls = undefined\n    }\n\n    // Clean up local state on error regardless of pending request state\n    if (status.status === \"error\") {\n      this.isManagedStreaming = false\n      this.currentManagedStreamId = undefined\n      this.currentManagedStreamUrls = undefined\n    }\n\n    // Notify handlers (would use event emitter in real implementation)\n    // this.emit('managedStreamStatus', status);\n  }\n\n  /**\n   * 🧹 Clean up all managed streaming state\n   */\n  cleanup(): void {\n    if (this.pendingManagedStreamRequest) {\n      this.pendingManagedStreamRequest.reject(new Error(\"Camera module cleanup\"))\n      this.pendingManagedStreamRequest = undefined\n    }\n\n    this.isManagedStreaming = false\n    this.currentManagedStreamId = undefined\n    this.currentManagedStreamUrls = undefined\n    this.managedStreamStatus = undefined\n\n    this.logger.info(\"📹 Managed streaming extension cleaned up\")\n  }\n}\n","/**\n * 💡 LED Control Module\n *\n * Provides RGB LED control functionality for App Sessions.\n * Controls the RGB LEDs on smart glasses with custom colors and timing patterns.\n */\n\nimport {RgbLedControlRequest, AppToCloudMessageType, LedColor} from \"../../../types\"\nimport {Logger} from \"pino\"\n\n/**\n * Options for LED control\n */\nexport interface LedControlOptions {\n  /** LED color */\n  color?: LedColor\n  /** LED on duration in milliseconds */\n  ontime?: number\n  /** LED off duration in milliseconds */\n  offtime?: number\n  /** Number of on/off cycles */\n  count?: number\n}\n\n/**\n * 💡 LED Control Module Implementation\n *\n * Provides methods for controlling LEDs on smart glasses.\n * Supports both low-level on/off control and higher-level pattern methods.\n *\n * @example\n * ```typescript\n * // General LED control\n * await session.led.turnOn({ color: 'red', ontime: 1000, count: 1 });\n *\n * // Blink green LED\n * await session.led.blink('green', 500, 500, 5);\n *\n * // Turn off all LEDs\n * await session.led.turnOff();\n * ```\n */\nexport class LedModule {\n  private session: any\n  private packageName: string\n  private sessionId: string\n  private logger: Logger\n\n  /**\n   * Create a new LedModule\n   *\n   * @param session - Reference to the parent AppSession\n   * @param packageName - The App package name\n   * @param sessionId - The current session ID\n   * @param logger - Logger instance for debugging\n   */\n  constructor(session: any, packageName: string, sessionId: string, logger?: Logger) {\n    this.session = session\n    this.packageName = packageName\n    this.sessionId = sessionId\n    this.logger = logger || (console as any)\n  }\n\n  // =====================================\n  // 💡 Low-level LED Control\n  // =====================================\n\n  /**\n   * 💡 Turn on an LED with specified timing parameters\n   *\n   * @param options - LED control options (color, timing, count)\n   * @returns Promise that resolves immediately after sending the command\n   *\n   * @example\n   * ```typescript\n   * // Solid red for 2 seconds\n   * await session.led.turnOn({ color: 'red', ontime: 2000, count: 1 });\n   *\n   * // Blink white 3 times\n   * await session.led.turnOn({ color: 'white', ontime: 500, offtime: 500, count: 3 });\n   * ```\n   */\n  async turnOn(options: LedControlOptions): Promise<void> {\n    try {\n      // Generate unique request ID for tracking\n      const requestId = `led_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`\n\n      // Create LED control request message\n      const message: RgbLedControlRequest = {\n        type: AppToCloudMessageType.RGB_LED_CONTROL,\n        packageName: this.packageName,\n        sessionId: this.sessionId,\n        requestId,\n        timestamp: new Date(),\n        action: \"on\",\n        color: options.color || \"red\",\n        ontime: options.ontime || 1000,\n        offtime: options.offtime || 0,\n        count: options.count || 1,\n      }\n\n      // Send request to cloud (fire-and-forget)\n      this.session.sendMessage(message)\n\n      this.logger.info(\n        {\n          requestId,\n          color: options.color,\n          ontime: options.ontime,\n          offtime: options.offtime,\n          count: options.count,\n        },\n        `💡 LED control request sent`,\n      )\n\n      // Resolve immediately - no waiting for response\n      return Promise.resolve()\n    } catch (error) {\n      this.logger.error({error, options}, \"❌ Error in LED turnOn request\")\n      throw error\n    }\n  }\n\n  /**\n   * 💡 Turn off all LEDs\n   *\n   * @returns Promise that resolves immediately after sending the command\n   *\n   * @example\n   * ```typescript\n   * await session.led.turnOff();\n   * ```\n   */\n  async turnOff(): Promise<void> {\n    try {\n      // Generate unique request ID for tracking\n      const requestId = `led_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`\n\n      // Create LED control request message\n      const message: RgbLedControlRequest = {\n        type: AppToCloudMessageType.RGB_LED_CONTROL,\n        packageName: this.packageName,\n        sessionId: this.sessionId,\n        requestId,\n        timestamp: new Date(),\n        action: \"off\",\n      }\n\n      // Send request to cloud (fire-and-forget)\n      this.session.sendMessage(message)\n\n      this.logger.info({requestId}, `💡 LED turn off request sent`)\n\n      // Resolve immediately - no waiting for response\n      return Promise.resolve()\n    } catch (error) {\n      this.logger.error({error}, \"❌ Error in LED turnOff request\")\n      throw error\n    }\n  }\n\n  /**\n   * 💡 Get available LED capabilities\n   *\n   * @returns Array of available LED configurations\n   *\n   * @example\n   * ```typescript\n   * const capabilities = session.led.getCapabilities();\n   * console.log('Available LEDs:', capabilities);\n   * ```\n   */\n  getCapabilities(): Array<{\n    id: string\n    purpose: string\n    isFullColor: boolean\n    color?: string\n    position?: string\n  }> {\n    // This would need to be implemented with access to session capabilities\n    // For now, return empty array - this would be populated from session.capabilities\n    return []\n  }\n\n  // =====================================\n  // 💡 Pattern Methods (SDK-generated)\n  // =====================================\n\n  /**\n   * 💡 Blink an LED with specified timing\n   *\n   * @param color - LED color to use\n   * @param ontime - How long LED stays on (ms)\n   * @param offtime - How long LED stays off (ms)\n   * @param count - Number of blink cycles\n   * @returns Promise that resolves immediately after sending the command\n   *\n   * @example\n   * ```typescript\n   * // Blink red LED 5 times (500ms on, 500ms off)\n   * await session.led.blink('red', 500, 500, 5);\n   * ```\n   */\n  async blink(color: LedColor, ontime: number, offtime: number, count: number): Promise<void> {\n    return this.turnOn({\n      color,\n      ontime,\n      offtime,\n      count,\n    })\n  }\n\n  /**\n   * 💡 Solid LED mode - LED stays on continuously for specified duration\n   *\n   * Creates a solid LED effect with no off time, perfect for continuous illumination.\n   *\n   * @param color - LED color to use\n   * @param duration - How long LED stays on (ms)\n   * @returns Promise that resolves when the command is sent\n   *\n   * @example\n   * ```typescript\n   * // Solid red LED for 5 seconds\n   * await session.led.solid('red', 5000);\n   *\n   * // Solid white LED for 30 seconds\n   * await session.led.solid('white', 30000);\n   * ```\n   */\n  async solid(color: LedColor, duration: number): Promise<void> {\n    return this.turnOn({\n      color,\n      ontime: duration,\n      offtime: 0, // No off time for solid mode\n      count: 1, // Single cycle\n    })\n  }\n\n  // =====================================\n  // 💡 Cleanup\n  // =====================================\n\n  /**\n   * Clean up LED module\n   *\n   * @internal\n   */\n  cleanup(): void {\n    this.logger.info(\"🧹 LED module cleaned up\")\n  }\n}\n","/**\n * 🔊 Audio Module\n *\n * Audio functionality for App Sessions.\n * Handles audio playback on connected glasses.\n */\n\nimport {AudioPlayRequest, AudioPlayResponse, AudioStopRequest, AppToCloudMessageType} from \"../../../types\"\nimport {Logger} from \"pino\"\n\n/**\n * Options for audio playback\n */\nexport interface AudioPlayOptions {\n  /** URL to audio file for download and play */\n  audioUrl: string\n  /** Volume level 0.0-1.0, defaults to 1.0 */\n  volume?: number\n  /** Whether to stop other audio playback, defaults to true */\n  stopOtherAudio?: boolean\n  /**\n   * Track ID for audio playback (defaults to 0)\n   * - 0: speaker (default audio playback)\n   * - 1: app_audio (app-specific audio)\n   * - 2: tts (text-to-speech audio)\n   * Use different track IDs to play multiple audio streams simultaneously (mixing)\n   */\n  trackId?: number\n}\n\n/**\n * Options for text-to-speech\n */\nexport interface SpeakOptions {\n  /** Voice ID to use (optional, defaults to server's ELEVENLABS_DEFAULT_VOICE_ID) */\n  voice_id?: string\n  /** Model ID to use (optional, defaults to eleven_flash_v2_5) */\n  model_id?: string\n  /** Voice settings object (optional) */\n  voice_settings?: {\n    stability?: number\n    similarity_boost?: number\n    style?: number\n    use_speaker_boost?: boolean\n    speed?: number\n  }\n  /** Volume level 0.0-1.0, defaults to 1.0 */\n  volume?: number\n  /** Whether to stop other audio playback, defaults to true */\n  stopOtherAudio?: boolean\n  /**\n   * Track ID for audio playback (defaults to 2 for TTS)\n   * - 0: speaker (default audio playback)\n   * - 1: app_audio (app-specific audio)\n   * - 2: tts (text-to-speech audio)\n   * Use different track IDs to play multiple audio streams simultaneously (mixing)\n   */\n  trackId?: number\n}\n\n/**\n * Result of audio playback attempt\n */\nexport interface AudioPlayResult {\n  /** Whether the audio playback was successful */\n  success: boolean\n  /** Error message if playback failed */\n  error?: string\n  /** Duration of the audio file in seconds (if available) */\n  duration?: number\n}\n\n/**\n * 🔊 Audio Module Implementation\n *\n * Audio management for App Sessions.\n * Provides methods for:\n * - 🎵 Playing audio on glasses\n * - ⏹️ Stopping audio playback\n * - 🔍 Monitoring audio request status\n * - 🧹 Cleanup and cancellation\n *\n * @example\n * ```typescript\n * // Play audio\n * const result = await session.audio.playAudio({\n *   audioUrl: 'https://example.com/sound.mp3',\n *   volume: 0.8\n * });\n *\n * // Stop all audio\n * session.audio.stopAudio();\n * ```\n */\nexport class AudioManager {\n  private session: any // Reference to AppSession\n  private packageName: string\n  private sessionId: string\n  private logger: Logger\n\n  /** Map to store pending audio play request promises */\n  private pendingAudioRequests = new Map<\n    string,\n    {\n      resolve: (value: AudioPlayResult) => void\n      reject: (reason?: string) => void\n    }\n  >()\n\n  /**\n   * Create a new AudioManager\n   *\n   * @param packageName - The App package name\n   * @param sessionId - The current session ID\n   * @param send - Function to send messages to the cloud\n   * @param session - Reference to the parent AppSession (optional)\n   * @param logger - Logger instance for debugging\n   */\n  constructor(session: any, packageName: string, sessionId: string, logger?: Logger) {\n    this.session = session\n    this.packageName = packageName\n    this.sessionId = sessionId\n    this.logger = logger || (console as any)\n  }\n\n  // =====================================\n  // 🎵 Audio Playback Functionality\n  // =====================================\n\n  /**\n   * 🔊 Play audio on the connected glasses\n   * @param options - Audio playback configuration\n   * @returns Promise that resolves with playback result\n   *\n   * @example\n   * ```typescript\n   * // Play audio from URL\n   * const result = await session.audio.playAudio({\n   *   audioUrl: 'https://example.com/sound.mp3',\n   *   volume: 0.8\n   * });\n   * ```\n   */\n  async playAudio(options: AudioPlayOptions): Promise<AudioPlayResult> {\n    return new Promise((resolve, reject) => {\n      try {\n        // Validate input\n        if (!options.audioUrl) {\n          reject(\"audioUrl must be provided\")\n          return\n        }\n\n        // Generate unique request ID\n        const requestId = `audio_req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`\n\n        const stopOtherAudio = options.stopOtherAudio ?? true\n\n        // CRITICAL: When stopOtherAudio=false (concurrent/mixing mode),\n        // resolve immediately after sending the request (fire-and-forget)\n        // This allows multiple audio streams to play simultaneously\n        if (!stopOtherAudio) {\n          // Create audio play request message\n          const message: AudioPlayRequest = {\n            type: AppToCloudMessageType.AUDIO_PLAY_REQUEST,\n            packageName: this.packageName,\n            sessionId: this.sessionId,\n            requestId,\n            timestamp: new Date(),\n            audioUrl: options.audioUrl,\n            volume: options.volume ?? 1.0,\n            stopOtherAudio: false,\n            trackId: options.trackId ?? 0, // Default to track 0 (speaker)\n          }\n\n          // Send request to cloud\n          this.session.sendMessage(message)\n\n          // Resolve immediately for concurrent playback (fire-and-forget)\n          // The audio will play in the background without blocking\n          this.logger.debug({requestId}, `🔊 Audio playback started in non-blocking mode (concurrent)`)\n          resolve({\n            success: true,\n            duration: undefined, // Duration unknown in fire-and-forget mode\n          })\n          return\n        }\n\n        // For stopOtherAudio=true (blocking/interrupt mode),\n        // wait for the COMPLETED/FAILED event before resolving\n        this.pendingAudioRequests.set(requestId, {resolve, reject})\n\n        // Create audio play request message\n        const message: AudioPlayRequest = {\n          type: AppToCloudMessageType.AUDIO_PLAY_REQUEST,\n          packageName: this.packageName,\n          sessionId: this.sessionId,\n          requestId,\n          timestamp: new Date(),\n          audioUrl: options.audioUrl,\n          volume: options.volume ?? 1.0,\n          stopOtherAudio: true,\n          trackId: options.trackId ?? 0, // Default to track 0 (speaker)\n        }\n\n        // Send request to cloud\n        this.session.sendMessage(message)\n\n        // Set timeout to avoid hanging promises (only for blocking mode)\n        const timeoutMs = 60000 // 60 seconds\n        if (this.session && this.session.resources) {\n          // Use session's resource tracker for automatic cleeanup\n          this.session.resources.setTimeout(() => {\n            if (this.pendingAudioRequests.has(requestId)) {\n              this.pendingAudioRequests.get(requestId)!.reject(\"Audio play request timed out\")\n              this.pendingAudioRequests.delete(requestId)\n              this.logger.warn({requestId}, `🔊 Audio play request timed out`)\n            }\n          }, timeoutMs)\n        } else {\n          // Fallback to regular setTimeout if session not available\n          setTimeout(() => {\n            if (this.pendingAudioRequests.has(requestId)) {\n              this.pendingAudioRequests.get(requestId)!.reject(\"Audio play request timed out\")\n              this.pendingAudioRequests.delete(requestId)\n              this.logger.warn({requestId}, `🔊 Audio play request timed out`)\n            }\n          }, timeoutMs)\n        }\n      } catch (error: unknown) {\n        const errorMessage = error instanceof Error ? error.message : String(error)\n        reject(`Failed to play audio: ${errorMessage}`)\n      }\n    })\n  }\n\n  /**\n   * 🔇 Stop audio playback on the connected glasses\n   * @param trackId - Optional track ID to stop (0=speaker, 1=app_audio, 2=tts). If omitted, stops all tracks.\n   *\n   * @example\n   * ```typescript\n   * // Stop all currently playing audio\n   * session.audio.stopAudio();\n   *\n   * // Stop only the speaker track (track_id 0)\n   * session.audio.stopAudio(0);\n   *\n   * // Stop only TTS track (track_id 2)\n   * session.audio.stopAudio(2);\n   * ```\n   */\n  stopAudio(trackId?: number): void {\n    try {\n      // Create audio stop request message\n      const message: AudioStopRequest = {\n        type: AppToCloudMessageType.AUDIO_STOP_REQUEST,\n        packageName: this.packageName,\n        sessionId: this.sessionId,\n        trackId,\n        timestamp: new Date(),\n      }\n\n      // Send request to cloud (one-way, no response expected)\n      this.session.sendMessage(message)\n\n      const trackInfo = trackId !== undefined ? ` (track ${trackId})` : \" (all tracks)\"\n      this.logger.info(`🔇 Audio stop request sent${trackInfo}`)\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error)\n      this.logger.error(`Failed to stop audio: ${errorMessage}`)\n    }\n  }\n\n  /**\n   * 🗣️ Convert text to speech and play it on the connected glasses\n   * @param text - Text to convert to speech (required)\n   * @param options - Text-to-speech configuration (optional)\n   * @returns Promise that resolves with playback result\n   *\n   * @example\n   * ```typescript\n   * // Basic text-to-speech\n   * const result = await session.audio.speak('Hello, world!');\n   *\n   * // With custom voice settings\n   * const result = await session.audio.speak('Hello, world!', {\n   *   voice_id: 'your_voice_id',\n   *   voice_settings: {\n   *     stability: 0.5,\n   *     speed: 1.2\n   *   },\n   *   volume: 0.8\n   * });\n   *\n   * // Play TTS without stopping other audio\n   * const result = await session.audio.speak('Hello, world!', {\n   *   stopOtherAudio: false\n   * });\n   * ```\n   */\n  async speak(text: string, options: SpeakOptions = {}): Promise<AudioPlayResult> {\n    // Validate input\n    if (!text) {\n      throw new Error(\"text must be provided\")\n    }\n\n    // Get the HTTPS server URL from the session\n    const baseUrl = this.session?.getHttpsServerUrl?.()\n    if (!baseUrl) {\n      throw new Error(\"Cannot determine server URL for TTS endpoint\")\n    }\n\n    // Build query parameters for the TTS endpoint\n    const queryParams = new URLSearchParams()\n    queryParams.append(\"text\", text)\n\n    if (options.voice_id) {\n      queryParams.append(\"voice_id\", options.voice_id)\n    }\n\n    if (options.model_id) {\n      queryParams.append(\"model_id\", options.model_id)\n    }\n\n    if (options.voice_settings) {\n      queryParams.append(\"voice_settings\", JSON.stringify(options.voice_settings))\n    }\n\n    // Construct the TTS URL\n    const ttsUrl = `${baseUrl}/api/tts?${queryParams.toString()}`\n\n    this.logger.info({text, ttsUrl}, `🗣️ Generating speech from text`)\n\n    // IMPORTANT: Don't call stopAudio() here - it closes tracks completely!\n    // The backend will handle stopping any ongoing playback when it receives\n    // the new audio play request (via stopOtherAudio flag)\n\n    // Use the existing playAudio method to play the TTS audio\n    // The stopOtherAudio flag will cancel ongoing playback without closing tracks\n    return this.playAudio({\n      audioUrl: ttsUrl,\n      volume: options.volume,\n      stopOtherAudio: options.stopOtherAudio ?? true, // This flag tells backend to stop current playback\n      trackId: options.trackId ?? 2, // Default to track 2 (tts)\n    })\n  }\n\n  // =====================================\n  // 📥 Response Handling\n  // =====================================\n\n  /**\n   * 📥 Handle audio play response from cloud\n   *\n   * This method is called internally when an audio play response is received.\n   * It resolves the corresponding pending promise with the response data.\n   *\n   * @param response - The audio play response received\n   * @internal This method is used internally by AppSession\n   */\n  handleAudioPlayResponse(response: AudioPlayResponse): void {\n    const pendingRequest = this.pendingAudioRequests.get(response.requestId)\n\n    if (pendingRequest) {\n      // Resolve the promise with the response data\n      pendingRequest.resolve({\n        success: response.success,\n        error: response.error,\n        duration: response.duration,\n      })\n\n      // Clean up\n      this.pendingAudioRequests.delete(response.requestId)\n\n      this.logger.info(\n        {\n          requestId: response.requestId,\n          success: response.success,\n          duration: response.duration,\n        },\n        `🔊 Audio play response received`,\n      )\n    } else {\n      this.logger.warn({requestId: response.requestId}, `🔊 Received audio play response for unknown request ID`)\n    }\n  }\n\n  // =====================================\n  // 🔍 Status and Management\n  // =====================================\n\n  /**\n   * 🔍 Check if there are pending audio requests\n   * @param requestId - Optional specific request ID to check\n   * @returns True if there are pending requests (or specific request exists)\n   */\n  hasPendingRequest(requestId?: string): boolean {\n    if (requestId) {\n      return this.pendingAudioRequests.has(requestId)\n    }\n    return this.pendingAudioRequests.size > 0\n  }\n\n  /**\n   * 📊 Get the number of pending audio requests\n   * @returns Number of pending requests\n   */\n  getPendingRequestCount(): number {\n    return this.pendingAudioRequests.size\n  }\n\n  /**\n   * 📋 Get all pending request IDs\n   * @returns Array of pending request IDs\n   */\n  getPendingRequestIds(): string[] {\n    return Array.from(this.pendingAudioRequests.keys())\n  }\n\n  /**\n   * ❌ Cancel a specific audio request\n   * @param requestId - The request ID to cancel\n   * @returns True if the request was found and cancelled\n   */\n  cancelAudioRequest(requestId: string): boolean {\n    const pendingRequest = this.pendingAudioRequests.get(requestId)\n    if (pendingRequest) {\n      pendingRequest.reject(\"Audio request cancelled\")\n      this.pendingAudioRequests.delete(requestId)\n      this.logger.info({requestId}, `🔊 Audio request cancelled`)\n      return true\n    }\n    return false\n  }\n\n  /**\n   * 🧹 Cancel all pending audio requests\n   * @returns Number of requests that were cancelled\n   */\n  cancelAllAudioRequests(): number {\n    const count = this.pendingAudioRequests.size\n    this.pendingAudioRequests.forEach((request, requestId) => {\n      request.reject(\"Audio request cancelled due to cleanup\")\n      this.logger.debug({requestId}, `🔊 Audio request cancelled during cleanup`)\n    })\n    this.pendingAudioRequests.clear()\n\n    if (count > 0) {\n      this.logger.info({cancelledCount: count}, `🧹 Cancelled all pending audio requests`)\n    }\n\n    return count\n  }\n\n  // =====================================\n  // 🔧 Internal Management\n  // =====================================\n\n  /**\n   * 🔄 Update the session ID when reconnecting\n   * @param newSessionId - The new session ID\n   * @internal Used by AppSession during reconnection\n   */\n  updateSessionId(newSessionId: string): void {\n    this.sessionId = newSessionId\n    this.logger.debug({newSessionId}, `🔄 Audio module session ID updated`)\n  }\n\n  /**\n   * 🧹 Cancel all pending requests (cleanup)\n   * @returns Object with count of cancelled requests\n   * @internal Used by AppSession during cleanup\n   */\n  cancelAllRequests(): {audioRequests: number} {\n    const audioRequests = this.cancelAllAudioRequests()\n    return {audioRequests}\n  }\n}\n","/**\n * Resource Tracker\n * \n * A utility class for tracking and automatically cleaning up resources\n * like timers, event listeners, and other disposable objects.\n * \n * This helps prevent memory leaks by ensuring that all resources are\n * properly disposed when they're no longer needed.\n */\n\n/**\n * Type for a cleanup function that doesn't take any arguments\n */\nexport type CleanupFunction = () => void;\n\n/**\n * Type for any object with a dispose or close method\n */\nexport interface Disposable {\n  dispose?: () => void;\n  close?: () => void;\n}\n\n/**\n * Manages resources to prevent memory leaks\n */\nexport class ResourceTracker {\n  // Collection of cleanup functions to call when dispose() is called\n  private cleanupFunctions: CleanupFunction[] = [];\n  \n  // Flag to track if this resource tracker has been disposed\n  private isDisposed = false;\n  \n  /**\n   * Add a cleanup function to be executed when dispose() is called\n   * \n   * @param cleanup - The cleanup function to register\n   * @returns A function that will remove this cleanup function\n   */\n  track(cleanup: CleanupFunction): CleanupFunction {\n    if (this.isDisposed) {\n      throw new Error('Cannot track resources on a disposed ResourceTracker');\n    }\n    \n    this.cleanupFunctions.push(cleanup);\n    \n    // Return a function that will remove this cleanup function\n    return () => {\n      const index = this.cleanupFunctions.indexOf(cleanup);\n      if (index !== -1) {\n        this.cleanupFunctions.splice(index, 1);\n      }\n    };\n  }\n  \n  /**\n   * Track a disposable object (anything with a dispose or close method)\n   * \n   * @param disposable - The object to track\n   * @returns A function that will remove this disposable\n   */\n  trackDisposable(disposable: Disposable): CleanupFunction {\n    return this.track(() => {\n      if (typeof disposable.dispose === 'function') {\n        disposable.dispose();\n      } else if (typeof disposable.close === 'function') {\n        disposable.close();\n      }\n    });\n  }\n  \n  /**\n   * Track a timer and ensure it gets cleared\n   * \n   * @param timerId - The timer ID to track\n   * @param isInterval - Whether this is an interval (true) or timeout (false)\n   * @returns A function that will remove this timer\n   */\n  trackTimer(timerId: NodeJS.Timeout, isInterval = false): CleanupFunction {\n    return this.track(() => {\n      if (isInterval) {\n        clearInterval(timerId);\n      } else {\n        clearTimeout(timerId);\n      }\n    });\n  }\n  \n  /**\n   * Track a timeout and ensure it gets cleared\n   * \n   * @param timerId - The timeout ID to track\n   * @returns A function that will remove this timeout\n   */\n  trackTimeout(timerId: NodeJS.Timeout): CleanupFunction {\n    return this.trackTimer(timerId, false);\n  }\n  \n  /**\n   * Track an interval and ensure it gets cleared\n   * \n   * @param timerId - The interval ID to track\n   * @returns A function that will remove this interval\n   */\n  trackInterval(timerId: NodeJS.Timeout): CleanupFunction {\n    return this.trackTimer(timerId, true);\n  }\n  \n  /**\n   * Create a tracked timeout\n   * \n   * @param callback - Function to call when the timeout expires\n   * @param ms - Milliseconds to wait\n   * @returns The timeout ID\n   */\n  setTimeout(callback: (...args: any[]) => void, ms: number): NodeJS.Timeout {\n    const timerId = setTimeout(callback, ms);\n    this.trackTimeout(timerId);\n    return timerId;\n  }\n  \n  /**\n   * Create a tracked interval\n   * \n   * @param callback - Function to call at each interval\n   * @param ms - Milliseconds between intervals\n   * @returns The interval ID\n   */\n  setInterval(callback: (...args: any[]) => void, ms: number): NodeJS.Timeout {\n    const timerId = setInterval(callback, ms);\n    this.trackInterval(timerId);\n    return timerId;\n  }\n  \n  /**\n   * Dispose of all tracked resources\n   */\n  dispose(): void {\n    if (this.isDisposed) {\n      return;\n    }\n    \n    // Run all cleanup functions\n    for (const cleanup of this.cleanupFunctions) {\n      try {\n        cleanup();\n      } catch (error) {\n        console.error('Error during resource cleanup:', error);\n      }\n    }\n    \n    // Clear the array\n    this.cleanupFunctions = [];\n    this.isDisposed = true;\n  }\n  \n  /**\n   * Check if this tracker has been disposed\n   */\n  get disposed(): boolean {\n    return this.isDisposed;\n  }\n}\n\n/**\n * Create a new ResourceTracker instance\n * \n * @returns A new ResourceTracker\n */\nexport function createResourceTracker(): ResourceTracker {\n  return new ResourceTracker();\n}","/**\n * Simple Storage SDK Module for MentraOS Apps\n * Provides localStorage-like API with cloud synchronization\n *\n * Mental Model: App server RAM = source of truth, MongoDB = crash recovery backup\n * - User interactions read/write to RAM instantly\n * - Changes are debounced and batched to MongoDB (3s idle / 10s max)\n * - On disconnect, pending writes are flushed automatically\n */\n\nimport {AppSession} from \"..\"\n\n/**\n * Response types for Simple Storage API\n */\ninterface StorageResponse {\n  success: boolean\n  data?: Record<string, string>\n}\n\ninterface StorageOperationResponse {\n  success: boolean\n}\n\n/**\n * Key-value storage with local caching and debounced cloud sync\n * Data is isolated by userId and packageName\n */\nexport class SimpleStorage {\n  private storage: Record<string, string> | null = null\n  private appSession: AppSession\n  private userId: string\n  private packageName: string\n  private baseUrl: string\n\n  // Debounce batching state\n  private pendingWrites = new Map<string, string>()\n  private debounceTimer?: NodeJS.Timeout\n  private maxWaitTimer?: NodeJS.Timeout\n  private firstWriteTime?: number\n\n  // Constants\n  private static readonly MAX_VALUE_SIZE = 100_000 // 100KB\n  private static readonly DEBOUNCE_MS = 3_000 // 3 seconds idle\n  private static readonly MAX_WAIT_MS = 10_000 // 10 seconds max\n\n  constructor(appSession: AppSession) {\n    this.appSession = appSession\n    this.userId = appSession.userId\n    this.packageName = appSession.getPackageName()\n    this.baseUrl = this.getBaseUrl()\n  }\n\n  // Convert WebSocket URL to HTTP for API calls\n  private getBaseUrl(): string {\n    const serverUrl = this.appSession.getServerUrl()\n    if (!serverUrl) return \"http://localhost:8002\"\n    return serverUrl.replace(/\\/app-ws$/, \"\").replace(/^ws/, \"http\")\n  }\n\n  // Generate auth headers for API requests\n  private getAuthHeaders() {\n    const apiKey = (this.appSession as any).config?.apiKey || \"unknown-api-key\"\n    return {\n      \"Authorization\": `Bearer ${this.packageName}:${apiKey}`,\n      \"Content-Type\": \"application/json\",\n    }\n  }\n\n  // Fetch all data from cloud and cache locally\n  private async fetchStorageFromCloud(): Promise<void> {\n    try {\n      const response = await fetch(`${this.baseUrl}/api/sdk/simple-storage/${encodeURIComponent(this.userId)}`, {\n        headers: this.getAuthHeaders(),\n      })\n\n      if (response.ok) {\n        const result = (await response.json()) as StorageResponse\n        if (result.success && result.data) {\n          this.storage = result.data\n        } else {\n          this.storage = {}\n        }\n      } else {\n        console.error(\"Failed to fetch storage from cloud:\", await response.text())\n        this.storage = {}\n      }\n    } catch (error) {\n      console.error(\"Error fetching storage from cloud:\", error)\n      this.storage = {}\n    }\n  }\n\n  // Get item from cache or cloud\n  public async get(key: string): Promise<string | undefined> {\n    try {\n      if (this.storage !== null && this.storage !== undefined) {\n        return this.storage[key]\n      }\n\n      await this.fetchStorageFromCloud()\n      return this.storage?.[key]\n    } catch (error) {\n      console.error(\"Error getting item:\", error)\n      return undefined\n    }\n  }\n\n  // Set item with size validation and debounced batching\n  // RAM updated immediately, MongoDB synced after 3s idle or 10s max\n  public async set(key: string, value: string): Promise<void> {\n    try {\n      // Validate value size\n      if (value.length > SimpleStorage.MAX_VALUE_SIZE) {\n        throw new Error(\n          `SimpleStorage value exceeds 100KB limit (${value.length} bytes). ` +\n            `For large files, use your own S3 bucket storage.`,\n        )\n      }\n\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n\n      // Optimistic update - RAM is source of truth (instant)\n      if (this.storage) {\n        this.storage[key] = value\n      }\n\n      // Add to pending batch for MongoDB persistence\n      this.pendingWrites.set(key, value)\n\n      // Schedule debounced flush\n      this.scheduleFlush()\n    } catch (error) {\n      console.error(\"Error setting item:\", error)\n      throw error\n    }\n  }\n\n  /**\n   * Schedule flush with debounce (3s idle) and max wait (10s)\n   */\n  private scheduleFlush(): void {\n    // Track first write time for max wait\n    if (!this.firstWriteTime) {\n      this.firstWriteTime = Date.now()\n    }\n\n    // Clear existing debounce timer\n    if (this.debounceTimer) {\n      clearTimeout(this.debounceTimer)\n    }\n\n    // Calculate time until max wait\n    const elapsedMs = Date.now() - this.firstWriteTime\n    const remainingMaxWaitMs = SimpleStorage.MAX_WAIT_MS - elapsedMs\n\n    // If we've hit max wait, flush immediately\n    if (remainingMaxWaitMs <= 0) {\n      this.flush().catch((err) => {\n        console.error(\"Error flushing SimpleStorage:\", err)\n      })\n      return\n    }\n\n    // Set debounce timer (3s idle)\n    this.debounceTimer = setTimeout(\n      () => {\n        this.flush().catch((err) => {\n          console.error(\"Error flushing SimpleStorage:\", err)\n        })\n      },\n      Math.min(SimpleStorage.DEBOUNCE_MS, remainingMaxWaitMs),\n    )\n\n    // Set max wait timer if not already set\n    if (!this.maxWaitTimer && remainingMaxWaitMs > 0) {\n      this.maxWaitTimer = setTimeout(() => {\n        this.flush().catch((err) => {\n          console.error(\"Error flushing SimpleStorage (max wait):\", err)\n        })\n      }, remainingMaxWaitMs)\n    }\n  }\n\n  /**\n   * Flush pending writes immediately\n   * Called by: debounce timeout, max wait timeout, disconnect, or explicit flush()\n   */\n  public async flush(): Promise<void> {\n    if (this.pendingWrites.size === 0) return\n\n    // Clear all timers\n    if (this.debounceTimer) {\n      clearTimeout(this.debounceTimer)\n      this.debounceTimer = undefined\n    }\n    if (this.maxWaitTimer) {\n      clearTimeout(this.maxWaitTimer)\n      this.maxWaitTimer = undefined\n    }\n    this.firstWriteTime = undefined\n\n    const batch = Object.fromEntries(this.pendingWrites)\n    this.pendingWrites.clear()\n\n    try {\n      // Persist to MongoDB (backup for crash recovery)\n      const response = await fetch(`${this.baseUrl}/api/sdk/simple-storage/${encodeURIComponent(this.userId)}`, {\n        method: \"PUT\",\n        headers: this.getAuthHeaders(),\n        body: JSON.stringify({data: batch}),\n      })\n\n      if (!response.ok) {\n        const error = await response.text()\n        console.error(\"Failed to persist SimpleStorage to MongoDB:\", error)\n\n        // Re-throw with helpful message\n        if (response.status === 413) {\n          throw new Error(\"SimpleStorage total size exceeds 1MB limit. Delete unused keys.\")\n        }\n        if (response.status === 429) {\n          throw new Error(\"SimpleStorage rate limit exceeded.\")\n        }\n        throw new Error(`SimpleStorage flush failed: ${error}`)\n      }\n    } catch (error) {\n      console.error(\"Error flushing SimpleStorage:\", error)\n      throw error\n    }\n  }\n\n  // Delete item from cache and debounced sync to cloud\n  public async delete(key: string): Promise<boolean> {\n    try {\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n\n      // Remove from cache (RAM = source of truth)\n      if (this.storage) {\n        delete this.storage[key]\n      }\n\n      // Remove from pending writes if exists\n      this.pendingWrites.delete(key)\n\n      // For deletes, we flush immediately to ensure consistency\n      // (could batch this too, but deletes are rare)\n      const response = await fetch(\n        `${this.baseUrl}/api/sdk/simple-storage/${encodeURIComponent(this.userId)}/${encodeURIComponent(key)}`,\n        {\n          method: \"DELETE\",\n          headers: this.getAuthHeaders(),\n        },\n      )\n\n      if (response.ok) {\n        const result = (await response.json()) as StorageOperationResponse\n        return result.success\n      } else {\n        console.error(\"Failed to delete item from cloud:\", await response.text())\n        return false\n      }\n    } catch (error) {\n      console.error(\"Error deleting item:\", error)\n      return false\n    }\n  }\n\n  // Clear all data from cache and cloud\n  public async clear(): Promise<boolean> {\n    try {\n      this.storage = {}\n\n      const response = await fetch(`${this.baseUrl}/api/sdk/simple-storage/${encodeURIComponent(this.userId)}`, {\n        method: \"DELETE\",\n        headers: this.getAuthHeaders(),\n      })\n\n      if (response.ok) {\n        const result = (await response.json()) as StorageOperationResponse\n        return result.success\n      } else {\n        console.error(\"Failed to clear storage from cloud:\", await response.text())\n        return false\n      }\n    } catch (error) {\n      console.error(\"Error clearing storage:\", error)\n      return false\n    }\n  }\n\n  // Get all storage keys\n  public async keys(): Promise<string[]> {\n    try {\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n      return Object.keys(this.storage || {})\n    } catch (error) {\n      console.error(\"Error getting keys:\", error)\n      return []\n    }\n  }\n\n  // Get number of stored items\n  public async size(): Promise<number> {\n    try {\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n      return Object.keys(this.storage || {}).length\n    } catch (error) {\n      console.error(\"Error getting storage size:\", error)\n      return 0\n    }\n  }\n\n  // Check if key exists\n  public async hasKey(key: string): Promise<boolean> {\n    try {\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n      return key in (this.storage || {})\n    } catch (error) {\n      console.error(\"Error checking key:\", error)\n      return false\n    }\n  }\n\n  // Get copy of all stored data\n  public async getAllData(): Promise<Record<string, string>> {\n    try {\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n      return {...(this.storage || {})}\n    } catch (error) {\n      console.error(\"Error getting all data:\", error)\n      return {}\n    }\n  }\n\n  // Set multiple items at once with validation\n  public async setMultiple(data: Record<string, string>): Promise<void> {\n    try {\n      // Validate all values first\n      for (const [key, value] of Object.entries(data)) {\n        if (value.length > SimpleStorage.MAX_VALUE_SIZE) {\n          throw new Error(`SimpleStorage value for key \"${key}\" exceeds 100KB limit (${value.length} bytes)`)\n        }\n      }\n\n      if (this.storage === null || this.storage === undefined) {\n        await this.fetchStorageFromCloud()\n      }\n\n      // Update cache (RAM = source of truth)\n      if (this.storage) {\n        Object.assign(this.storage, data)\n      }\n\n      // Add all to pending batch\n      for (const [key, value] of Object.entries(data)) {\n        this.pendingWrites.set(key, value)\n      }\n\n      // Schedule debounced flush\n      this.scheduleFlush()\n    } catch (error) {\n      console.error(\"Error setting multiple items:\", error)\n      throw error\n    }\n  }\n}\n","/**\n * Observable<T> - A reactive value wrapper that notifies listeners of changes\n *\n * Provides synchronous value access and reactive subscriptions via onChange().\n * Supports implicit coercion for use in conditionals and comparisons.\n *\n * @example\n * ```typescript\n * const wifiStatus = new Observable(false);\n *\n * // Synchronous read\n * console.log(wifiStatus.value); // false\n *\n * // Implicit coercion\n * if (wifiStatus) { ... } // Works via valueOf()\n *\n * // Reactive subscription\n * const cleanup = wifiStatus.onChange((connected) => {\n *   console.log(\"WiFi:\", connected);\n * });\n *\n * // Update (triggers callbacks)\n * wifiStatus.setValue(true);\n *\n * // Cleanup\n * cleanup();\n * ```\n */\nexport class Observable<T> {\n  private _value: T;\n  private _listeners: Set<(value: T) => void> = new Set();\n  private _initialized: boolean = false; // Track if value has been set from WebSocket\n\n  constructor(initialValue: T) {\n    this._value = initialValue;\n  }\n\n  /**\n   * Get the current value synchronously\n   */\n  get value(): T {\n    return this._value;\n  }\n\n  /**\n   * Implicit coercion to primitive value (for conditionals/comparisons)\n   */\n  valueOf(): T {\n    return this._value;\n  }\n\n  /**\n   * String representation\n   */\n  toString(): string {\n    return String(this._value);\n  }\n\n  /**\n   * Symbol.toPrimitive for implicit type coercion\n   * Allows usage in conditionals: if (observable) { ... }\n   */\n  [Symbol.toPrimitive](hint: string): T | string {\n    if (hint === 'string') {\n      return String(this._value);\n    }\n    return this._value;\n  }\n\n  /**\n   * Subscribe to value changes\n   *\n   * The callback is called immediately with the current value ONLY if\n   * the Observable has been initialized (setValue() called at least once).\n   * This prevents callbacks from firing with default/uninitialized values.\n   *\n   * @param callback - Function to call when value changes\n   * @returns Cleanup function to unsubscribe\n   *\n   * @example\n   * ```typescript\n   * const cleanup = observable.onChange((value) => {\n   *   console.log(\"New value:\", value);\n   * });\n   *\n   * // Later: unsubscribe\n   * cleanup();\n   * ```\n   */\n  onChange(callback: (value: T) => void): () => void {\n    this._listeners.add(callback);\n    // Call immediately with current value ONLY if initialized\n    if (this._initialized) {\n      callback(this._value);\n    }\n    // Return cleanup function\n    return () => this._listeners.delete(callback);\n  }\n\n  /**\n   * Update the value and notify listeners\n   *\n   * Triggers callbacks if:\n   * 1. This is the first setValue() call (initialization from WebSocket), OR\n   * 2. The new value is different from current value\n   *\n   * Uses strict equality (===) for comparison.\n   *\n   * @param value - New value to set\n   *\n   * @internal This method is called by DeviceState when receiving WebSocket updates\n   */\n  setValue(value: T): void {\n    const isFirstInit = !this._initialized;\n\n    // Mark as initialized (first setValue call from WebSocket)\n    if (isFirstInit) {\n      this._initialized = true;\n    }\n\n    // Notify listeners if this is initialization OR value changed\n    if (isFirstInit || this._value !== value) {\n      this._value = value;\n      // Notify all listeners\n      this._listeners.forEach((cb) => {\n        try {\n          cb(value);\n        } catch (error) {\n          console.error('Error in Observable onChange callback:', error);\n        }\n      });\n    }\n  }\n\n  /**\n   * Get the number of active listeners\n   * @internal Used for debugging/testing\n   */\n  get listenerCount(): number {\n    return this._listeners.size;\n  }\n}\n","/**\n * DeviceState - Reactive device state management via WebSocket\n *\n * Provides real-time Observable properties for device state (WiFi, battery, hotspot, etc.)\n * Uses flat structure matching GlassesInfo field names exactly.\n *\n * @example\n * ```typescript\n * // Synchronous read\n * if (session.device.state.wifiConnected) {\n *   console.log(\"Connected to:\", session.device.state.wifiSsid.value);\n * }\n *\n * // Reactive subscription\n * session.device.state.wifiConnected.onChange((connected) => {\n *   console.log(\"WiFi status:\", connected);\n * });\n *\n * session.device.state.batteryLevel.onChange((level) => {\n *   console.log(\"Battery:\", level, \"%\");\n * });\n * ```\n */\n\nimport {Observable} from '../../utils/Observable';\nimport type {GlassesInfo} from '@mentra/types';\nimport type {AppSession} from './index';\n\nexport class DeviceState {\n  // ============================================================================\n  // WiFi Status Observables\n  // ============================================================================\n\n  /** WiFi connection status */\n  public readonly wifiConnected: Observable<boolean>;\n\n  /** WiFi network SSID (null if not connected) */\n  public readonly wifiSsid: Observable<string | null>;\n\n  /** WiFi local IP address (null if not connected) */\n  public readonly wifiLocalIp: Observable<string | null>;\n\n  // ============================================================================\n  // Battery Status Observables\n  // ============================================================================\n\n  /** Glasses battery level (0-100, null if unknown) */\n  public readonly batteryLevel: Observable<number | null>;\n\n  /** Glasses charging status (null if unknown) */\n  public readonly charging: Observable<boolean | null>;\n\n  /** Case battery level (0-100, null if no case or unknown) */\n  public readonly caseBatteryLevel: Observable<number | null>;\n\n  /** Case charging status (null if no case or unknown) */\n  public readonly caseCharging: Observable<boolean | null>;\n\n  /** Case open/closed status (true = open, null if no case or unknown) */\n  public readonly caseOpen: Observable<boolean | null>;\n\n  /** Case removed status (true = glasses not in case, null if no case or unknown) */\n  public readonly caseRemoved: Observable<boolean | null>;\n\n  // ============================================================================\n  // Hotspot Status Observables\n  // ============================================================================\n\n  /** Hotspot enabled status (null if not supported or unknown) */\n  public readonly hotspotEnabled: Observable<boolean | null>;\n\n  /** Hotspot SSID (null if disabled or unknown) */\n  public readonly hotspotSsid: Observable<string | null>;\n\n  // ============================================================================\n  // Connection & Device Info Observables\n  // ============================================================================\n\n  /** Glasses connected to phone/cloud (true = connected) */\n  public readonly connected: Observable<boolean>;\n\n  /** Glasses model name (e.g., \"even_g1\", \"xreal_air_2_pro\") */\n  public readonly modelName: Observable<string | null>;\n\n  // ============================================================================\n  // Internal State\n  // ============================================================================\n\n  private appSession: AppSession;\n\n  constructor(appSession: AppSession) {\n    this.appSession = appSession;\n\n    // Initialize all observables with default/safe values\n    // These will be updated when cloud sends DEVICE_STATE_UPDATE\n\n    // WiFi (default: disconnected)\n    this.wifiConnected = new Observable<boolean>(false);\n    this.wifiSsid = new Observable<string | null>(null);\n    this.wifiLocalIp = new Observable<string | null>(null);\n\n    // Battery (default: unknown)\n    this.batteryLevel = new Observable<number | null>(null);\n    this.charging = new Observable<boolean | null>(null);\n    this.caseBatteryLevel = new Observable<number | null>(null);\n    this.caseCharging = new Observable<boolean | null>(null);\n    this.caseOpen = new Observable<boolean | null>(null);\n    this.caseRemoved = new Observable<boolean | null>(null);\n\n    // Hotspot (default: disabled/unknown)\n    this.hotspotEnabled = new Observable<boolean | null>(null);\n    this.hotspotSsid = new Observable<string | null>(null);\n\n    // Connection (default: disconnected)\n    this.connected = new Observable<boolean>(false);\n    this.modelName = new Observable<string | null>(null);\n  }\n\n  /**\n   * Update device state from WebSocket message\n   *\n   * Called internally by AppSession when DEVICE_STATE_UPDATE message is received.\n   * Only updates Observables for fields present in the state object.\n   * Observables automatically notify listeners only if value changed.\n   *\n   * @param state - Partial device state (only changed fields, or full snapshot)\n   * @internal\n   */\n  updateFromMessage(state: Partial<GlassesInfo>): void {\n    // Connection state\n    if (state.connected !== undefined) {\n      this.connected.setValue(state.connected);\n    }\n    if (state.modelName !== undefined) {\n      this.modelName.setValue(state.modelName);\n    }\n\n    // WiFi state\n    if (state.wifiConnected !== undefined) {\n      this.wifiConnected.setValue(state.wifiConnected);\n    }\n    if (state.wifiSsid !== undefined) {\n      this.wifiSsid.setValue(state.wifiSsid ?? null);\n    }\n    if (state.wifiLocalIp !== undefined) {\n      this.wifiLocalIp.setValue(state.wifiLocalIp ?? null);\n    }\n\n    // Battery state\n    if (state.batteryLevel !== undefined) {\n      this.batteryLevel.setValue(state.batteryLevel ?? null);\n    }\n    if (state.charging !== undefined) {\n      this.charging.setValue(state.charging ?? null);\n    }\n    if (state.caseBatteryLevel !== undefined) {\n      this.caseBatteryLevel.setValue(state.caseBatteryLevel ?? null);\n    }\n    if (state.caseCharging !== undefined) {\n      this.caseCharging.setValue(state.caseCharging ?? null);\n    }\n    if (state.caseOpen !== undefined) {\n      this.caseOpen.setValue(state.caseOpen ?? null);\n    }\n    if (state.caseRemoved !== undefined) {\n      this.caseRemoved.setValue(state.caseRemoved ?? null);\n    }\n\n    // Hotspot state\n    if (state.hotspotEnabled !== undefined) {\n      this.hotspotEnabled.setValue(state.hotspotEnabled ?? null);\n    }\n    if (state.hotspotSsid !== undefined) {\n      this.hotspotSsid.setValue(state.hotspotSsid ?? null);\n    }\n  }\n\n  /**\n   * Get snapshot of current device state\n   *\n   * Returns a plain object with current values of all Observables.\n   * Useful for compatibility with REST-style code or debugging.\n   *\n   * @returns Current device state as GlassesInfo partial object\n   *\n   * @example\n   * ```typescript\n   * const currentState = session.device.state.getSnapshot();\n   * console.log(\"Current state:\", currentState);\n   * ```\n   */\n  getSnapshot(): Partial<GlassesInfo> {\n    return {\n      // Connection\n      connected: this.connected.value,\n      modelName: this.modelName.value ?? undefined,\n\n      // WiFi\n      wifiConnected: this.wifiConnected.value,\n      wifiSsid: this.wifiSsid.value ?? undefined,\n      wifiLocalIp: this.wifiLocalIp.value ?? undefined,\n\n      // Battery\n      batteryLevel: this.batteryLevel.value ?? undefined,\n      charging: this.charging.value ?? undefined,\n      caseBatteryLevel: this.caseBatteryLevel.value ?? undefined,\n      caseCharging: this.caseCharging.value ?? undefined,\n      caseOpen: this.caseOpen.value ?? undefined,\n      caseRemoved: this.caseRemoved.value ?? undefined,\n\n      // Hotspot\n      hotspotEnabled: this.hotspotEnabled.value ?? undefined,\n      hotspotSsid: this.hotspotSsid.value ?? undefined,\n    };\n  }\n}\n","// src/app/webview/index.ts\nimport axios from \"axios\";\nimport { Response, NextFunction } from \"express\";\nimport { AuthenticatedRequest } from \"../../types\";\n// Note: Your Express app needs to use cookie-parser middleware for this to work\n// Example: app.use(require('cookie-parser')());\nimport * as crypto from \"crypto\";\nimport { KEYUTIL, KJUR, RSAKey } from \"jsrsasign\";\nimport { AppSession } from \"../session\";\n\nconst userTokenPublicKey =\n  process.env.MENTRAOS_CLOUD_USER_TOKEN_PUBLIC_KEY ||\n  \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Yt2RtNOdeKQxWMY0c84\\nADpY1Jy58YWZhaEgP2A5tBwFUKgy/TH9gQLWZjQ3dQ/6XXO8qq0kluoYFqM7ZDRF\\nzJ0E4Yi0WQncioLRcCx4q8pDmqY9vPKgv6PruJdFWca0l0s3gZ3BqSeWum/C23xK\\nFPHPwi8gvRdc6ALrkcHeciM+7NykU8c0EY8PSitNL+Tchti95kGu+j6APr5vNewi\\nzRpQGOdqaLWe+ahHmtj6KtUZjm8o6lan4f/o08C6litizguZXuw2Nn/Kd9fFI1xF\\nIVNJYMy9jgGaOi71+LpGw+vIpwAawp/7IvULDppvY3DdX5nt05P1+jvVJXPxMKzD\\nTQIDAQAB\\n-----END PUBLIC KEY-----\";\n\n/**\n * Extracts the temporary token from a URL string.\n * @param url The URL string, typically window.location.href.\n * @returns The token string or null if not found.\n */\nexport function extractTempToken(url: string): string | null {\n  try {\n    const parsedUrl = new URL(url);\n    return parsedUrl.searchParams.get(\"aos_temp_token\");\n  } catch (e) {\n    console.error(\"Error parsing URL for temp token:\", e);\n    return null;\n  }\n}\n\n/**\n * Exchanges a temporary token for a user ID with the MentraOS Cloud.\n * This should be called from the App's backend server.\n * @param cloudApiUrl The base URL of the MentraOS Cloud API.\n * @param tempToken The temporary token obtained from the webview URL.\n * @param apiKey Your App's secret API key.\n * @returns A Promise that resolves with an object containing the userId.\n * @throws Throws an error if the exchange fails (e.g., invalid token, expired, network error).\n */\nexport async function exchangeToken(\n  cloudApiUrl: string,\n  tempToken: string,\n  apiKey: string,\n  packageName: string,\n): Promise<{ userId: string }> {\n  const endpoint = `${cloudApiUrl}/api/auth/exchange-user-token`;\n  console.log(`Exchanging token for user at ${endpoint}`);\n  try {\n    const response = await axios.post(\n      endpoint,\n      { aos_temp_token: tempToken, packageName: packageName },\n      {\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${apiKey}`,\n        },\n        timeout: 10000, // 10 second timeout\n      },\n    );\n\n    if (\n      response.status === 200 &&\n      response.data.success &&\n      response.data.userId\n    ) {\n      return { userId: response.data.userId };\n    } else {\n      // Handle specific error messages from the server if available\n      const errorMessage =\n        response.data?.error || `Failed with status ${response.status}`;\n      throw new Error(errorMessage);\n    }\n  } catch (error) {\n    if (axios.isAxiosError(error)) {\n      const status = error.response?.status;\n      const data = error.response?.data;\n      const message =\n        data?.error || error.message || \"Unknown error during token exchange\";\n      console.error(`Token exchange failed with status ${status}: ${message}`);\n      throw new Error(`Token exchange failed: ${message}`);\n    } else {\n      console.error(\"Unexpected error during token exchange:\", error);\n      throw new Error(\"An unexpected error occurred during token exchange.\");\n    }\n  }\n}\n\n/**\n * Signs a user ID to create a secure session token.\n * @param userId The user ID to sign\n * @param secret The secret key used for signing\n * @returns A signed session token string\n */\nfunction signSession(userId: string, secret: string): string {\n  // Format: userId.timestamp.signature\n  const timestamp = Date.now();\n  const data = `${userId}|${timestamp}`;\n  const signature = crypto\n    .createHmac(\"sha256\", secret)\n    .update(data)\n    .digest(\"hex\");\n\n  return `${data}|${signature}`;\n}\n\n/**\n * Verifies and extracts the user ID from a signed session token.\n * @param token The signed session token\n * @param secret The secret key used for verification\n * @param maxAge The maximum age of the token in milliseconds\n * @returns The extracted user ID if valid, or null if invalid\n */\nfunction verifySession(\n  token: string,\n  secret: string,\n  maxAge?: number,\n): string | null {\n  try {\n    const parts = token.split(\"|\");\n    if (parts.length !== 3) return null;\n\n    const [userId, timestampStr, signature] = parts;\n    const timestamp = parseInt(timestampStr, 10);\n\n    // Check if token has expired\n    if (maxAge && Date.now() - timestamp > maxAge) {\n      console.log(\n        `Session token expired: ${token}.  Parsed date is ${timestamp}, meaning age is ${Date.now() - timestamp}, but maxAge is ${maxAge}`,\n      );\n      return null;\n    }\n\n    // Verify signature\n    const data = `${userId}|${timestamp}`;\n    const expectedSignature = crypto\n      .createHmac(\"sha256\", secret)\n      .update(data)\n      .digest(\"hex\");\n\n    if (signature !== expectedSignature) {\n      console.log(\n        `Session token signature mismatch: ${signature} !== ${expectedSignature}`,\n      );\n      return null;\n    }\n\n    return userId;\n  } catch (error) {\n    console.error(\"Session verification failed:\", error);\n    return null;\n  }\n}\n\n/**\n * Verifies a signed user token and extracts the user ID from it\n * @param signedUserToken The JWT token to verify\n * @returns The user ID (subject) from the token, or null if invalid\n */\nasync function verifySignedUserToken(\n  signedUserToken: string,\n): Promise<string | null> {\n  try {\n    // 1. Parse the PEM public key into a jsrsasign key object\n    const publicKeyObj = KEYUTIL.getKey(userTokenPublicKey) as RSAKey;\n    // 2. Verify JWT signature + claims (issuer, exp, iat) with 2-min tolerance\n    const isValid = KJUR.jws.JWS.verifyJWT(signedUserToken, publicKeyObj, {\n      alg: [\"RS256\"],\n      iss: [\"https://prod.augmentos.cloud\"],\n      verifyAt: KJUR.jws.IntDate.get(\"now\"),\n      gracePeriod: 120,\n    });\n    if (!isValid) return null;\n\n    // 3. Decode payload and return the subject (user ID)\n    const parsed = KJUR.jws.JWS.parse(signedUserToken);\n    return (parsed.payloadObj as { sub: string }).sub || null;\n  } catch (e) {\n    console.error(\"[verifySignedUserToken] Error verifying token:\", e);\n    return null;\n  }\n}\n\n/**\n * Verifies a frontend token by comparing it to a secure hash of the API key\n * @param frontendToken The token to verify (should be a hash of the API key)\n * @param apiKey The API key to hash and compare against\n * @param userId Optional user ID that may be embedded in the token format\n * @returns The user ID if the token is valid, or null if invalid\n */\nfunction verifyFrontendToken(\n  frontendToken: string,\n  apiKey: string,\n): string | null {\n  try {\n    // Check if the token contains a user ID and hash separated by a colon\n    const tokenParts = frontendToken.split(\":\");\n\n    if (tokenParts.length === 2) {\n      // Format: userId:hash\n      const [tokenUserId, tokenHash] = tokenParts;\n\n      // Align the hashing algorithm with server-side `hashWithApiKey`\n      // 1. Hash the API key first (server only stores the hashed version)\n      const hashedApiKey = crypto\n        .createHash(\"sha256\")\n        .update(apiKey)\n        .digest(\"hex\");\n      // 2. Create the expected hash using userId + hashedApiKey (same order & update calls)\n      const expectedHash = crypto\n        .createHash(\"sha256\")\n        .update(tokenUserId)\n        .update(hashedApiKey)\n        .digest(\"hex\");\n\n      if (tokenHash === expectedHash) {\n        return tokenUserId;\n      }\n    } else {\n      throw new Error(\"Invalid frontend token format\");\n    }\n\n    return null;\n  } catch (error) {\n    console.error(\"Frontend token verification failed:\", error);\n    return null;\n  }\n}\n\nfunction validateCloudApiUrlChecksum(\n  checksum: string,\n  cloudApiUrl: string,\n  apiKey: string,\n): boolean {\n  const hashedApiKey = crypto.createHash(\"sha256\").update(apiKey).digest(\"hex\");\n  const expectedChecksum = crypto\n    .createHash(\"sha256\")\n    .update(cloudApiUrl)\n    .update(hashedApiKey)\n    .digest(\"hex\");\n\n  return expectedChecksum === checksum;\n}\n/**\n * Express middleware for automatically handling the token exchange.\n * Assumes API key and Cloud URL are available (e.g., via environment variables).\n * Adds `req.authUserId` if successful.\n *\n * @param options Configuration options.\n * @param options.cloudApiUrl The base URL of the MentraOS Cloud API.\n * @param options.apiKey Your App's secret API key.\n * @param options.tokenQueryParam The name of the query parameter containing the token (default: 'aos_temp_token').\n * @param options.cookieName The name of the cookie to store the session token (default: 'aos_session').\n * @param options.cookieSecret Secret key used to sign the session cookie. MUST be provided and kept secure.\n * @param options.cookieOptions Options for the session cookie (default: { httpOnly: true, secure: process.env.NODE_ENV === 'production' }).\n */\nexport function createAuthMiddleware(options: {\n  apiKey: string;\n  packageName: string;\n  cookieName?: string;\n  cookieSecret: string;\n  getAppSessionForUser?: (userId: string) => AppSession | null;\n  cookieOptions?: {\n    httpOnly?: boolean;\n    secure?: boolean;\n    maxAge?: number;\n    sameSite?: boolean | \"lax\" | \"strict\" | \"none\";\n    path?: string;\n  };\n}) {\n  const {\n    apiKey,\n    packageName,\n    cookieName = \"aos_session\",\n    cookieSecret,\n    getAppSessionForUser,\n    cookieOptions = {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === \"production\",\n      maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days by default\n      sameSite: process.env.NODE_ENV === \"production\" ? \"none\" : \"lax\",\n      path: \"/\",\n    },\n  } = options;\n\n  if (!apiKey) {\n    throw new Error(\"API Key are required for the auth middleware.\");\n  }\n\n  if (\n    !cookieSecret ||\n    typeof cookieSecret !== \"string\" ||\n    cookieSecret.length < 8\n  ) {\n    throw new Error(\n      \"A strong cookieSecret (at least 8 characters) is required for secure session management.\",\n    );\n  }\n\n  return async (\n    req: AuthenticatedRequest,\n    res: Response,\n    next: NextFunction,\n  ) => {\n    // First check for temporary token in the query string\n    const tempToken = req.query[\"aos_temp_token\"] as string;\n    const frontendToken =\n      (req.headers.authorization?.replace(\"Bearer \", \"\") as string) ||\n      (req.query[\"aos_frontend_token\"] as string);\n    const signedUserToken = req.query[\"aos_signed_user_token\"] as string;\n\n    // first check for signed user token\n    if (signedUserToken) {\n      const userId = await verifySignedUserToken(signedUserToken);\n      if (userId) {\n        // Set the user ID on the request\n        req.authUserId = userId;\n        if (getAppSessionForUser) {\n          const appSession = getAppSessionForUser(userId);\n          if (appSession) {\n            req.activeSession = appSession;\n          } else {\n            req.activeSession = null;\n          }\n        }\n\n        // Create a signed session token and store it in a cookie\n        const signedSession = signSession(userId, cookieSecret);\n        res.cookie(cookieName, signedSession, cookieOptions);\n\n        console.log(\n          \"[auth.middleware] User ID verified from signed user token: \",\n          userId,\n        );\n        return next();\n      } else {\n        console.log(\"[auth.middleware] Signed user token invalid\");\n      }\n    }\n    // If temporary token exists, authenticate with it\n    if (tempToken) {\n      try {\n        let cloudApiUrl = `https://api.mentra.glass`;\n        const cloudApiUrlFromQuery = req.query[\"cloudApiUrl\"] as string;\n        if (cloudApiUrlFromQuery) {\n          const cloudApiUrlChecksum = req.query[\n            \"cloudApiUrlChecksum\"\n          ] as string;\n\n          if (\n            validateCloudApiUrlChecksum(\n              cloudApiUrlChecksum,\n              cloudApiUrlFromQuery,\n              apiKey,\n            )\n          ) {\n            console.log(\n              `Cloud API is being routed to alternate url at request of the server: ${cloudApiUrlFromQuery}`,\n            );\n            cloudApiUrl = cloudApiUrlFromQuery;\n          } else {\n            console.error(\n              `Server requested alternate cloud url of ${cloudApiUrlFromQuery} but the checksum is invalid (checksum: ${cloudApiUrlChecksum}).  Using default cloud url of ${cloudApiUrl} instead.`,\n            );\n          }\n        }\n\n        const { userId } = await exchangeToken(\n          cloudApiUrl,\n          tempToken,\n          apiKey,\n          packageName,\n        );\n\n        // Set the user ID on the request\n        req.authUserId = userId;\n        if (getAppSessionForUser) {\n          const appSession = getAppSessionForUser(userId);\n          if (appSession) {\n            req.activeSession = appSession;\n          }\n        }\n\n        // Create a signed session token and store it in a cookie\n        const signedSession = signSession(userId, cookieSecret);\n        res.cookie(cookieName, signedSession, cookieOptions);\n\n        console.log(\n          \"[auth.middleware] User ID verified from temporary token: \",\n          userId,\n        );\n\n        return next();\n      } catch (error) {\n        console.error(\"Webview token exchange failed:\", error);\n        // Temporary token is invalid\n      }\n    }\n\n    if (frontendToken) {\n      // Check for user ID in headers if not embedded in token\n      const userId = verifyFrontendToken(frontendToken, apiKey);\n\n      if (userId) {\n        req.authUserId = userId;\n        if (getAppSessionForUser) {\n          const appSession = getAppSessionForUser(userId);\n          if (appSession) {\n            req.activeSession = appSession;\n          }\n        }\n        // Create a signed session token and store it in a cookie\n        const signedSession = signSession(userId, cookieSecret);\n        res.cookie(cookieName, signedSession, cookieOptions);\n        console.log(\n          \"[auth.middleware] User ID verified from frontend user token: \",\n          userId,\n        );\n        return next();\n      } else {\n        console.log(\"[auth.middleware] Frontend token invalid\");\n      }\n    }\n\n    // No valid temporary token, check for existing session cookie\n    const sessionCookie = req.cookies?.[cookieName];\n\n    if (sessionCookie) {\n      try {\n        // Verify the signed session cookie and extract the user ID\n        const userId = verifySession(\n          sessionCookie,\n          cookieSecret,\n          cookieOptions.maxAge,\n        );\n        if (userId) {\n          req.authUserId = userId;\n          if (getAppSessionForUser) {\n            const appSession = getAppSessionForUser(userId);\n            if (appSession) {\n              req.activeSession = appSession;\n            }\n          }\n          return next();\n        }\n\n        // Invalid or expired session, clear the cookie\n        res.clearCookie(cookieName, { path: cookieOptions.path });\n      } catch (error) {\n        console.error(\"Invalid session cookie:\", error);\n        // Clear the invalid cookie\n        res.clearCookie(cookieName, { path: cookieOptions.path });\n      }\n    }\n\n    // No valid authentication method found, proceed without setting req.authUserId\n    next();\n  };\n}\n","/**\n * updates.ts\n *\n * This file defines constant messages that should be displayed\n * in the terminal to notify developers about new SDK releases.\n *\n * Each function generates a stylized ASCII message (banner-style)\n * that highlights the latest SDK version and provides the npm install command.\n * https://patorjk.com/software/taag/\n *\n * These messages are intended to be logged to the console or shown in\n * terminal output so developers are aware of updates in a clear\n * and visually distinct way.\n *\n *\n */\n\nimport chalk from \"chalk\";\nimport boxen from \"boxen\";\nimport { mentraLogo_1, newUpdateText } from \"./logos\";\n\nconst createUpdateNotification = (versionNumb: string): string => {\n  const line = chalk.bold.gray(\"-------------------------------\");\n  const logo = chalk.cyan(mentraLogo_1);\n  const title = chalk.bold.cyan(newUpdateText);\n  const versionMessage = `Version ${chalk.bold.cyan(`SDK VERSION V${versionNumb} is out! 🎉`)}`;\n  const currentNote = chalk.yellow(\"You are running an older version\");\n  const instructions = chalk.yellow(\"Update to the latest version with:\");\n  const command = chalk.green.bold(\"bun install @mentra/sdk@latest\");\n\n  const content = [\n    logo,\n    title,\n    line,\n    versionMessage,\n    currentNote,\n    line,\n    instructions,\n    command,\n  ].join(\"\\n\");\n\n  return boxen(content, {\n    padding: 1,\n    margin: 1,\n    borderStyle: \"round\",\n    borderColor: \"cyan\",\n    textAlignment: \"left\",\n  });\n};\n\nexport const newSDKUpdate = (versionNumb: string): string => {\n  return createUpdateNotification(versionNumb);\n};\n","/**\n * 🔐 App Token Module\n *\n * Provides utilities for working with App tokens.\n */\nexport * from './utils';"]}