neoagent 2.3.1-beta.6 → 2.3.1-beta.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +45 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +6 -5
- package/docs/hardware.md +1 -1
- package/docs/integrations.md +2 -3
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +111 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +571 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +275 -0
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +366 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
- package/flutter_app/lib/main.dart +103 -0
- package/flutter_app/lib/main_account_settings.dart +1250 -0
- package/flutter_app/lib/main_admin.dart +886 -0
- package/flutter_app/lib/main_app_shell.dart +2134 -0
- package/flutter_app/lib/main_chat.dart +3316 -0
- package/flutter_app/lib/main_controller.dart +6836 -0
- package/flutter_app/lib/main_devices.dart +2396 -0
- package/flutter_app/lib/main_integrations.dart +909 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3539 -0
- package/flutter_app/lib/main_navigation.dart +188 -0
- package/flutter_app/lib/main_operations.dart +4851 -0
- package/flutter_app/lib/main_recordings.dart +920 -0
- package/flutter_app/lib/main_runtime.dart +838 -0
- package/flutter_app/lib/main_settings.dart +2040 -0
- package/flutter_app/lib/main_shared.dart +3147 -0
- package/flutter_app/lib/main_theme.dart +257 -0
- package/flutter_app/lib/main_voice_assistant.dart +957 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1837 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
- package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
- package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1224 -0
- package/flutter_app/pubspec.yaml +57 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/install_helpers.js +31 -0
- package/lib/manager.js +201 -6
- package/package.json +5 -3
- package/server/db/database.js +125 -1
- package/server/http/middleware.js +55 -2
- package/server/http/routes.js +1 -0
- package/server/http/static.js +41 -1
- package/server/index.js +3 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +281 -1
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +86707 -82621
- package/server/routes/auth.js +26 -5
- package/server/routes/integrations.js +108 -1
- package/server/routes/memory.js +11 -2
- package/server/routes/settings.js +84 -2
- package/server/routes/wearable.js +67 -0
- package/server/services/ai/engine.js +3 -2
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/toolSelector.js +54 -1
- package/server/services/ai/tools.js +88 -5
- package/server/services/desktop/screenRecorder.js +65 -9
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/figma/provider.js +1 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/google/provider.js +1 -0
- package/server/services/integrations/manager.js +88 -12
- package/server/services/integrations/microsoft/provider.js +1 -0
- package/server/services/integrations/oauth_provider.js +25 -8
- package/server/services/integrations/provider_config_store.js +85 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/integrations/spotify/provider.js +1 -0
- package/server/services/integrations/trello/provider.js +842 -0
- package/server/services/manager.js +46 -1
- package/server/services/mcp/client.js +120 -23
- package/server/services/memory/manager.js +39 -2
- package/server/services/messaging/access_policy.js +10 -0
- package/server/services/messaging/manager.js +49 -0
- package/server/services/messaging/meshtastic.js +277 -0
- package/server/services/messaging/meshtastic_env.js +40 -0
- package/server/services/messaging/meshtastic_protocol.js +530 -0
- package/server/services/messaging/meshtastic_protocol.test.js +33 -0
- package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/openaiSpeech.js +6 -1
- package/server/services/voice/providers.js +144 -15
- package/server/services/voice/runtimeManager.js +156 -19
- package/server/services/voice/turnRunner.js +29 -9
- package/server/services/wearable/firmware_manifest.js +353 -0
- package/server/services/wearable/gateway.js +350 -0
- package/server/services/wearable/protocol.js +45 -0
- package/server/services/wearable/service.js +240 -0
- package/server/services/widgets/service.js +3 -0
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
- package/server/services/integrations/home_assistant/provider.js +0 -350
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WEARABLE_WS_PATH = '/api/wearable/ws';
|
|
4
|
+
|
|
5
|
+
const CLIENT_MESSAGE_TYPES = new Set([
|
|
6
|
+
'wearable:hello',
|
|
7
|
+
'voice:session_open',
|
|
8
|
+
'voice:input_start',
|
|
9
|
+
'voice:audio_chunk',
|
|
10
|
+
'voice:input_commit',
|
|
11
|
+
'voice:interrupt',
|
|
12
|
+
'voice:session_close',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function parseWearableMessage(data) {
|
|
16
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
17
|
+
const parsed = JSON.parse(text);
|
|
18
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
19
|
+
throw new Error('Wearable message must be a JSON object.');
|
|
20
|
+
}
|
|
21
|
+
const type = String(parsed.type || '').trim();
|
|
22
|
+
if (!type) {
|
|
23
|
+
throw new Error('Wearable message type is required.');
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...parsed,
|
|
27
|
+
type,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isWearableHello(message) {
|
|
32
|
+
return message?.type === 'wearable:hello';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isSupportedClientMessageType(type) {
|
|
36
|
+
return CLIENT_MESSAGE_TYPES.has(String(type || '').trim());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
CLIENT_MESSAGE_TYPES,
|
|
41
|
+
WEARABLE_WS_PATH,
|
|
42
|
+
isSupportedClientMessageType,
|
|
43
|
+
isWearableHello,
|
|
44
|
+
parseWearableMessage,
|
|
45
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getVersionInfo } = require('../../utils/version');
|
|
4
|
+
const { clientIpFromRequest, lookupIpLocation } = require('../account/geoip');
|
|
5
|
+
const { WEARABLE_WS_PATH } = require('./protocol');
|
|
6
|
+
const {
|
|
7
|
+
resolveFirmwareManifest,
|
|
8
|
+
normalizeChannel,
|
|
9
|
+
} = require('./firmware_manifest');
|
|
10
|
+
|
|
11
|
+
function toTrimmedString(value, maxLength = 512) {
|
|
12
|
+
return String(value || '').trim().slice(0, maxLength);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseOptionalJson(value, fallback = null) {
|
|
16
|
+
if (!value) return fallback;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(String(value));
|
|
19
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
20
|
+
? parsed
|
|
21
|
+
: fallback;
|
|
22
|
+
} catch {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function publicBaseUrlForRequest(req) {
|
|
28
|
+
const configured = req.app?.locals?.httpRuntimeConfig?.publicUrl || process.env.PUBLIC_URL || '';
|
|
29
|
+
if (configured) {
|
|
30
|
+
return String(configured).replace(/\/+$/, '');
|
|
31
|
+
}
|
|
32
|
+
const forwardedProto = String(req.get?.('x-forwarded-proto') || '').trim();
|
|
33
|
+
const protocol = forwardedProto || req.protocol || 'http';
|
|
34
|
+
return `${protocol}://${req.get('host')}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function websocketUrlForBase(baseUrl) {
|
|
38
|
+
const parsed = new URL(baseUrl);
|
|
39
|
+
parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
40
|
+
parsed.pathname = WEARABLE_WS_PATH;
|
|
41
|
+
parsed.search = '';
|
|
42
|
+
parsed.hash = '';
|
|
43
|
+
return parsed.toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseTimeZoneOffsetLabel(value) {
|
|
47
|
+
const raw = String(value || '').trim();
|
|
48
|
+
if (!raw) return null;
|
|
49
|
+
if (raw === 'GMT' || raw === 'UTC') return 0;
|
|
50
|
+
const match = raw.match(/^(?:GMT|UTC)([+-])(\d{1,2})(?::?(\d{2}))?$/i);
|
|
51
|
+
if (!match) return null;
|
|
52
|
+
const sign = match[1] === '-' ? -1 : 1;
|
|
53
|
+
const hours = Number(match[2] || 0);
|
|
54
|
+
const minutes = Number(match[3] || 0);
|
|
55
|
+
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
|
|
56
|
+
return sign * ((hours * 60 * 60) + (minutes * 60));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function utcOffsetSecondsForTimeZone(timeZone, now = new Date()) {
|
|
60
|
+
const normalized = String(timeZone || '').trim();
|
|
61
|
+
if (!normalized) return null;
|
|
62
|
+
try {
|
|
63
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
64
|
+
timeZone: normalized,
|
|
65
|
+
timeZoneName: 'shortOffset',
|
|
66
|
+
hour: '2-digit',
|
|
67
|
+
minute: '2-digit',
|
|
68
|
+
hour12: false,
|
|
69
|
+
});
|
|
70
|
+
const zonePart = formatter.formatToParts(now).find((part) => part.type === 'timeZoneName');
|
|
71
|
+
return parseTimeZoneOffsetLabel(zonePart?.value || '');
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class WearableService {
|
|
78
|
+
constructor({ app }) {
|
|
79
|
+
this.app = app;
|
|
80
|
+
this.connectionsByUser = new Map();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async buildBootstrap(req) {
|
|
84
|
+
const baseUrl = publicBaseUrlForRequest(req);
|
|
85
|
+
const version = getVersionInfo();
|
|
86
|
+
return {
|
|
87
|
+
server: {
|
|
88
|
+
baseUrl,
|
|
89
|
+
websocketUrl: websocketUrlForBase(baseUrl),
|
|
90
|
+
serverTime: new Date().toISOString(),
|
|
91
|
+
},
|
|
92
|
+
auth: {
|
|
93
|
+
qrLoginChallengePath: '/api/auth/qr-login/challenge',
|
|
94
|
+
qrLoginStatusPathTemplate: '/api/auth/qr-login/challenge/{challengeId}/status',
|
|
95
|
+
qrLoginClaimPathTemplate: '/api/auth/qr-login/challenge/{challengeId}/claim',
|
|
96
|
+
qrLoginResolvePath: '/api/account/qr-login/resolve',
|
|
97
|
+
qrLoginApprovePath: '/api/account/qr-login/approve',
|
|
98
|
+
},
|
|
99
|
+
widgets: {
|
|
100
|
+
snapshotsPath: '/api/widgets/snapshots',
|
|
101
|
+
},
|
|
102
|
+
voice: {
|
|
103
|
+
websocketPath: WEARABLE_WS_PATH,
|
|
104
|
+
supportsAudioStreaming: true,
|
|
105
|
+
events: [
|
|
106
|
+
'voice:session_ready',
|
|
107
|
+
'voice:assistant_state',
|
|
108
|
+
'voice:transcript_partial',
|
|
109
|
+
'voice:transcript_final',
|
|
110
|
+
'voice:assistant_text',
|
|
111
|
+
'voice:audio_chunk',
|
|
112
|
+
'voice:audio_done',
|
|
113
|
+
'voice:error',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
recordings: {
|
|
117
|
+
basePath: '/api/recordings',
|
|
118
|
+
voiceAssistantRespondPath: '/api/voice-assistant/respond',
|
|
119
|
+
},
|
|
120
|
+
firmware: await this.buildFirmwareManifest(req),
|
|
121
|
+
version,
|
|
122
|
+
user: {
|
|
123
|
+
id: req.session.userId,
|
|
124
|
+
username: req.session.username || null,
|
|
125
|
+
},
|
|
126
|
+
features: {
|
|
127
|
+
qrPairing: true,
|
|
128
|
+
widgets: true,
|
|
129
|
+
voice: true,
|
|
130
|
+
recordings: true,
|
|
131
|
+
otaManifest: true,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async buildFirmwareManifest(req) {
|
|
137
|
+
const baseUrl = publicBaseUrlForRequest(req);
|
|
138
|
+
const channel = normalizeChannel(req?.query?.channel || process.env.NEOAGENT_WEARABLE_FIRMWARE_CHANNEL || 'stable');
|
|
139
|
+
const version = getVersionInfo();
|
|
140
|
+
const manifest = await resolveFirmwareManifest({
|
|
141
|
+
channel,
|
|
142
|
+
repositoryOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY, 256),
|
|
143
|
+
assetNameOverride: toTrimmedString(process.env.NEOAGENT_WEARABLE_FIRMWARE_ASSET_NAME, 128),
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
...manifest,
|
|
147
|
+
websocketUrl: websocketUrlForBase(baseUrl),
|
|
148
|
+
minimumServerVersion: manifest.minimumServerVersion || version.version,
|
|
149
|
+
rollout: parseOptionalJson(process.env.NEOAGENT_WEARABLE_FIRMWARE_ROLLOUT_JSON, null),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
buildTimeConfig(req) {
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const clientIp = req ? clientIpFromRequest(req) : null;
|
|
156
|
+
const detectedTimeZone = String(lookupIpLocation(clientIp).data?.timezone || '').trim();
|
|
157
|
+
const fallbackTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
158
|
+
const timeZone = detectedTimeZone || fallbackTimeZone;
|
|
159
|
+
const offsetSeconds =
|
|
160
|
+
utcOffsetSecondsForTimeZone(timeZone, now)
|
|
161
|
+
?? utcOffsetSecondsForTimeZone(fallbackTimeZone, now)
|
|
162
|
+
?? (-now.getTimezoneOffset() * 60);
|
|
163
|
+
return {
|
|
164
|
+
timezone: timeZone,
|
|
165
|
+
utcOffsetSeconds: offsetSeconds,
|
|
166
|
+
serverTime: now.toISOString(),
|
|
167
|
+
source: detectedTimeZone ? 'client_ip_geoip' : 'server_fallback',
|
|
168
|
+
ipAddress: clientIp || null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
registerConnection({
|
|
173
|
+
userId,
|
|
174
|
+
ws,
|
|
175
|
+
remoteAddress = null,
|
|
176
|
+
userAgent = null,
|
|
177
|
+
hello = {},
|
|
178
|
+
}) {
|
|
179
|
+
const key = Number(userId);
|
|
180
|
+
const userSet = this.connectionsByUser.get(key) || new Map();
|
|
181
|
+
const deviceId = [
|
|
182
|
+
toTrimmedString(hello.device?.deviceId, 160),
|
|
183
|
+
toTrimmedString(hello.device?.macAddress, 64),
|
|
184
|
+
toTrimmedString(ws?._socket?.remoteAddress, 120),
|
|
185
|
+
].find(Boolean) || `wearable-${Date.now()}`;
|
|
186
|
+
const connection = {
|
|
187
|
+
deviceId,
|
|
188
|
+
ws,
|
|
189
|
+
connectedAt: new Date().toISOString(),
|
|
190
|
+
remoteAddress: toTrimmedString(remoteAddress, 120) || null,
|
|
191
|
+
userAgent: toTrimmedString(userAgent, 500) || null,
|
|
192
|
+
hello: {
|
|
193
|
+
platform: toTrimmedString(hello.device?.platform, 40) || 'esp32s3',
|
|
194
|
+
firmwareVersion: toTrimmedString(hello.device?.firmwareVersion, 120) || null,
|
|
195
|
+
deviceLabel: toTrimmedString(hello.device?.deviceLabel, 120) || null,
|
|
196
|
+
},
|
|
197
|
+
lastSeenAt: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
userSet.set(deviceId, connection);
|
|
200
|
+
this.connectionsByUser.set(key, userSet);
|
|
201
|
+
return connection;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
touchConnection(userId, deviceId) {
|
|
205
|
+
const userSet = this.connectionsByUser.get(Number(userId));
|
|
206
|
+
const connection = userSet?.get(String(deviceId || '').trim());
|
|
207
|
+
if (connection) {
|
|
208
|
+
connection.lastSeenAt = new Date().toISOString();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
unregisterConnection(userId, deviceId) {
|
|
213
|
+
const userSet = this.connectionsByUser.get(Number(userId));
|
|
214
|
+
if (!userSet) return;
|
|
215
|
+
userSet.delete(String(deviceId || '').trim());
|
|
216
|
+
if (userSet.size === 0) {
|
|
217
|
+
this.connectionsByUser.delete(Number(userId));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
listConnections(userId) {
|
|
222
|
+
const userSet = this.connectionsByUser.get(Number(userId));
|
|
223
|
+
if (!userSet) return [];
|
|
224
|
+
return Array.from(userSet.values()).map((entry) => ({
|
|
225
|
+
deviceId: entry.deviceId,
|
|
226
|
+
connectedAt: entry.connectedAt,
|
|
227
|
+
lastSeenAt: entry.lastSeenAt,
|
|
228
|
+
remoteAddress: entry.remoteAddress,
|
|
229
|
+
userAgent: entry.userAgent,
|
|
230
|
+
hello: entry.hello,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
WEARABLE_WS_PATH,
|
|
237
|
+
WearableService,
|
|
238
|
+
publicBaseUrlForRequest,
|
|
239
|
+
websocketUrlForBase,
|
|
240
|
+
};
|
|
@@ -540,11 +540,14 @@ class WidgetService {
|
|
|
540
540
|
'',
|
|
541
541
|
'You are updating a structured product widget. Keep the layout fixed. Refresh only the content snapshot.',
|
|
542
542
|
'Use fresh tools for time-sensitive claims. Do not rely on stale memory for live data such as weather, markets, incidents, or schedules.',
|
|
543
|
+
'The refresh run has a hard limit of 30 tool-call iterations. Call save_widget_snapshot as early as you have enough accurate data; do not keep exploring after saving.',
|
|
543
544
|
'After gathering the latest information, call save_widget_snapshot exactly once with a payload matching this schema:',
|
|
544
545
|
'{"title":"","kicker":"","subtitle":"","body":"","metric":"","metricLabel":"","secondaryMetric":"","secondaryLabel":"","tertiaryMetric":"","tertiaryLabel":"","trend":{"label":"","direction":"flat"},"progress":{"value":0,"max":100,"label":""},"rows":[{"label":"","value":""}],"chips":[""],"iconToken":"","accentToken":"","backgroundToken":"","surfaceColor":"","updatedAt":"","deepLink":""}',
|
|
545
546
|
'Rules:',
|
|
546
547
|
'- Do not change the template or layout variant.',
|
|
548
|
+
'- The run is only complete after save_widget_snapshot succeeds. A plain text reply without saving is a failed refresh.',
|
|
547
549
|
'- Once you have enough accurate data, call save_widget_snapshot exactly once and stop. Do not keep exploring after saving.',
|
|
550
|
+
'- Do not repeat the same tool call with identical arguments unless the previous attempt clearly failed or returned empty/incomplete data.',
|
|
548
551
|
'- Keep rows to at most 3 and chips to at most 3.',
|
|
549
552
|
'- Prefer concrete data over generic prose. Use metric + supporting fields whenever live data exists.',
|
|
550
553
|
'- Make the widget immediately useful at a glance. Avoid filler copy, duplicated labels, or repeating the widget name unless it helps identify the subject.',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { getSessionSecret } = require('../services/account/session_secret');
|
|
5
|
+
|
|
6
|
+
const LOCAL_SECRET_PREFIX = 'enc:local:v1:';
|
|
7
|
+
|
|
8
|
+
function getLocalSecretMaterial() {
|
|
9
|
+
const configured = String(process.env.NEOAGENT_DATA_ENCRYPTION_KEY || '').trim();
|
|
10
|
+
if (configured) return configured;
|
|
11
|
+
return getSessionSecret();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deriveKey() {
|
|
15
|
+
return crypto.createHash('sha256').update(getLocalSecretMaterial()).digest();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isLocalEncryptedValue(value) {
|
|
19
|
+
return String(value || '').startsWith(LOCAL_SECRET_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encryptLocalValue(value) {
|
|
23
|
+
const text = String(value || '');
|
|
24
|
+
if (!text) return '';
|
|
25
|
+
if (isLocalEncryptedValue(text)) return text;
|
|
26
|
+
|
|
27
|
+
const iv = crypto.randomBytes(12);
|
|
28
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(), iv);
|
|
29
|
+
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
|
|
30
|
+
const tag = cipher.getAuthTag();
|
|
31
|
+
return `${LOCAL_SECRET_PREFIX}${Buffer.concat([iv, tag, encrypted]).toString('base64')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decryptLocalValue(value) {
|
|
35
|
+
const text = String(value || '');
|
|
36
|
+
if (!text) return '';
|
|
37
|
+
if (!isLocalEncryptedValue(text)) return text;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const payload = Buffer.from(text.slice(LOCAL_SECRET_PREFIX.length), 'base64');
|
|
41
|
+
const iv = payload.subarray(0, 12);
|
|
42
|
+
const tag = payload.subarray(12, 28);
|
|
43
|
+
const ciphertext = payload.subarray(28);
|
|
44
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(), iv);
|
|
45
|
+
decipher.setAuthTag(tag);
|
|
46
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
decryptLocalValue,
|
|
54
|
+
encryptLocalValue,
|
|
55
|
+
isLocalEncryptedValue,
|
|
56
|
+
};
|
package/server/utils/logger.js
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const SENSITIVE_KEY_RE = /(?:^|_|-)(?:token|secret|password|api[_-]?key|authorization|cookie|session(?:id)?|sid)(?:$|_|-)/i;
|
|
4
|
+
|
|
5
|
+
function redactSecrets(input) {
|
|
6
|
+
let text = String(input || '');
|
|
7
|
+
if (!text) return text;
|
|
8
|
+
|
|
9
|
+
text = text
|
|
10
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi, '$1[redacted]')
|
|
11
|
+
.replace(/([?&](?:token|access_token|refresh_token|api[_-]?key|secret|password|authorization|cookie|session|sid)=)([^&#\s]+)/gi, '$1[redacted]')
|
|
12
|
+
.replace(/\b(token|access_token|refresh_token|authorization|api[_-]?key|secret|password|cookie|session(?:id)?|sid)\b\s*[:=]\s*("[^"]*"|'[^']*'|[^,\s;]+)/gi, '$1=[redacted]');
|
|
13
|
+
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeUrl(value) {
|
|
18
|
+
return redactSecrets(String(value || ''));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSensitiveKey(key) {
|
|
22
|
+
return SENSITIVE_KEY_RE.test(String(key || ''));
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
function serializeValue(value, seen = new WeakSet()) {
|
|
4
26
|
if (value instanceof Error) {
|
|
5
27
|
return JSON.stringify({
|
|
6
28
|
name: value.name,
|
|
7
|
-
message: value.message,
|
|
8
|
-
stack: value.stack,
|
|
29
|
+
message: redactSecrets(value.message),
|
|
30
|
+
stack: redactSecrets(value.stack),
|
|
9
31
|
code: value.code,
|
|
10
32
|
cause: value.cause instanceof Error
|
|
11
|
-
? { name: value.cause.name, message: value.cause.message, stack: value.cause.stack }
|
|
33
|
+
? { name: value.cause.name, message: redactSecrets(value.cause.message), stack: redactSecrets(value.cause.stack) }
|
|
12
34
|
: value.cause
|
|
13
35
|
});
|
|
14
36
|
}
|
|
@@ -22,7 +44,7 @@ function serializeValue(value, seen = new WeakSet()) {
|
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
if (!value || typeof value !== 'object') {
|
|
25
|
-
return String(value);
|
|
47
|
+
return redactSecrets(String(value));
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
if (seen.has(value)) {
|
|
@@ -33,21 +55,27 @@ function serializeValue(value, seen = new WeakSet()) {
|
|
|
33
55
|
|
|
34
56
|
try {
|
|
35
57
|
return JSON.stringify(value, (key, nestedValue) => {
|
|
58
|
+
if (isSensitiveKey(key)) {
|
|
59
|
+
return '[redacted]';
|
|
60
|
+
}
|
|
36
61
|
if (nestedValue instanceof Error) {
|
|
37
62
|
return {
|
|
38
63
|
name: nestedValue.name,
|
|
39
|
-
message: nestedValue.message,
|
|
40
|
-
stack: nestedValue.stack,
|
|
64
|
+
message: redactSecrets(nestedValue.message),
|
|
65
|
+
stack: redactSecrets(nestedValue.stack),
|
|
41
66
|
code: nestedValue.code
|
|
42
67
|
};
|
|
43
68
|
}
|
|
44
69
|
if (typeof nestedValue === 'bigint') {
|
|
45
70
|
return nestedValue.toString();
|
|
46
71
|
}
|
|
72
|
+
if (typeof nestedValue === 'string') {
|
|
73
|
+
return redactSecrets(nestedValue);
|
|
74
|
+
}
|
|
47
75
|
return nestedValue;
|
|
48
76
|
});
|
|
49
77
|
} catch (err) {
|
|
50
|
-
return `[Unserializable object: ${err.message}]`;
|
|
78
|
+
return `[Unserializable object: ${redactSecrets(err.message)}]`;
|
|
51
79
|
} finally {
|
|
52
80
|
seen.delete(value);
|
|
53
81
|
}
|
|
@@ -58,14 +86,14 @@ function formatLogArgs(args) {
|
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
function logRequestSummary(level, req, message, extra = null) {
|
|
61
|
-
const prefix = `[HTTP] ${req.method} ${req.originalUrl || req.url}`;
|
|
89
|
+
const prefix = `[HTTP] ${req.method} ${sanitizeUrl(req.originalUrl || req.url)}`;
|
|
62
90
|
const summary = {
|
|
63
91
|
ip: req.ip,
|
|
64
92
|
userId: req.session?.userId || null,
|
|
65
93
|
userAgent: req.get?.('user-agent') || null,
|
|
66
94
|
...extra
|
|
67
95
|
};
|
|
68
|
-
console[level](`${prefix} ${message}
|
|
96
|
+
console[level](redactSecrets(`${prefix} ${message}`), summary);
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
/**
|