diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 33b30b18..88e7ce4a 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -6,7 +6,6 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterShellArgs import io.flutter.plugin.common.MethodChannel -import io.flutter.plugins.GeneratedPluginRegistrant import gobackend.Gobackend import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -158,7 +157,6 @@ class MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - GeneratedPluginRegistrant.registerWith(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> scope.launch { diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index 16c16b78..bbc5fac3 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -309,7 +309,6 @@ func (m *ExtensionManager) UnloadExtension(extensionID string) error { return nil } -// Returns error if extension not found (gomobile compatible) func (m *ExtensionManager) GetExtension(extensionID string) (*LoadedExtension, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -632,8 +631,6 @@ type ExtensionUpgradeInfo struct { IsInstalled bool `json:"is_installed"` } -// checkExtensionUpgradeInternal checks if a package file is an upgrade for an existing extension -// Internal function that returns struct func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*ExtensionUpgradeInfo, error) { // Validate file extension if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") { @@ -803,8 +800,6 @@ func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) { return string(jsonBytes), nil } -// ==================== Extension Lifecycle ==================== - func (m *ExtensionManager) InitializeExtension(extensionID string, settings map[string]interface{}) error { m.mu.Lock() defer m.mu.Unlock() @@ -933,7 +928,6 @@ func (m *ExtensionManager) UnloadAllExtensions() { GoLog("[Extension] All extensions unloaded\n") } -// The function is called as extension.() and can return a result func (m *ExtensionManager) InvokeAction(extensionID string, actionName string) (map[string]interface{}, error) { m.mu.Lock() defer m.mu.Unlock() diff --git a/go_backend/extension_manifest.go b/go_backend/extension_manifest.go index 34b89fe6..37af1b83 100644 --- a/go_backend/extension_manifest.go +++ b/go_backend/extension_manifest.go @@ -25,9 +25,9 @@ const ( ) type ExtensionPermissions struct { - Network []string `json:"network"` // List of allowed domains - Storage bool `json:"storage"` // Whether extension can use storage API - File bool `json:"file"` // Whether extension can use file API + Network []string `json:"network"` + Storage bool `json:"storage"` + File bool `json:"file"` } type ExtensionSetting struct { @@ -38,15 +38,15 @@ type ExtensionSetting struct { Required bool `json:"required,omitempty"` Secret bool `json:"secret,omitempty"` Default interface{} `json:"default,omitempty"` - Options []string `json:"options,omitempty"` // For select type - Action string `json:"action,omitempty"` // For button type: JS function name to call (e.g., "startLogin") + Options []string `json:"options,omitempty"` + Action string `json:"action,omitempty"` } type QualityOption struct { - ID string `json:"id"` // Unique identifier (e.g., "mp3_320", "opus_128") - Label string `json:"label"` // Display name (e.g., "MP3 320kbps") - Description string `json:"description"` // Optional description (e.g., "Best quality MP3") - Settings []QualitySpecificSetting `json:"settings,omitempty"` // Quality-specific settings + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Settings []QualitySpecificSetting `json:"settings,omitempty"` } type QualitySpecificSetting struct { @@ -57,48 +57,48 @@ type QualitySpecificSetting struct { Required bool `json:"required,omitempty"` Secret bool `json:"secret,omitempty"` Default interface{} `json:"default,omitempty"` - Options []string `json:"options,omitempty"` // For select type + Options []string `json:"options,omitempty"` } type SearchFilter struct { - ID string `json:"id"` // Filter identifier (e.g., "track", "album", "artist", "playlist") - Label string `json:"label,omitempty"` // Display label (e.g., "Songs", "Albums", "Artists", "Playlists") - Icon string `json:"icon,omitempty"` // Optional icon name + ID string `json:"id"` + Label string `json:"label,omitempty"` + Icon string `json:"icon,omitempty"` } type SearchBehaviorConfig struct { - Enabled bool `json:"enabled"` // Whether extension provides custom search - Placeholder string `json:"placeholder,omitempty"` // Placeholder text for search box - Primary bool `json:"primary,omitempty"` // If true, show as primary search tab - Icon string `json:"icon,omitempty"` // Icon for search tab - ThumbnailRatio string `json:"thumbnailRatio,omitempty"` // Thumbnail aspect ratio: "square" (1:1), "wide" (16:9), "portrait" (2:3) - ThumbnailWidth int `json:"thumbnailWidth,omitempty"` // Custom thumbnail width in pixels - ThumbnailHeight int `json:"thumbnailHeight,omitempty"` // Custom thumbnail height in pixels - Filters []SearchFilter `json:"filters,omitempty"` // Available search filters (e.g., track, album, artist, playlist) + Enabled bool `json:"enabled"` + Placeholder string `json:"placeholder,omitempty"` + Primary bool `json:"primary,omitempty"` + Icon string `json:"icon,omitempty"` + ThumbnailRatio string `json:"thumbnailRatio,omitempty"` + ThumbnailWidth int `json:"thumbnailWidth,omitempty"` + ThumbnailHeight int `json:"thumbnailHeight,omitempty"` + Filters []SearchFilter `json:"filters,omitempty"` } type URLHandlerConfig struct { - Enabled bool `json:"enabled"` // Whether extension handles URLs - Patterns []string `json:"patterns,omitempty"` // URL patterns to match (e.g., "music.youtube.com", "soundcloud.com") + Enabled bool `json:"enabled"` + Patterns []string `json:"patterns,omitempty"` } type TrackMatchingConfig struct { - CustomMatching bool `json:"customMatching"` // Whether extension handles matching - Strategy string `json:"strategy,omitempty"` // "isrc", "name", "duration", "custom" - DurationTolerance int `json:"durationTolerance,omitempty"` // Tolerance in seconds for duration matching + CustomMatching bool `json:"customMatching"` + Strategy string `json:"strategy,omitempty"` + DurationTolerance int `json:"durationTolerance,omitempty"` } type PostProcessingHook struct { - ID string `json:"id"` // Unique identifier - Name string `json:"name"` // Display name - Description string `json:"description,omitempty"` // Description - DefaultEnabled bool `json:"defaultEnabled,omitempty"` // Whether enabled by default - SupportedFormats []string `json:"supportedFormats,omitempty"` // Supported file formats (e.g., ["flac", "mp3"]) + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + DefaultEnabled bool `json:"defaultEnabled,omitempty"` + SupportedFormats []string `json:"supportedFormats,omitempty"` } type PostProcessingConfig struct { - Enabled bool `json:"enabled"` // Whether extension provides post-processing - Hooks []PostProcessingHook `json:"hooks,omitempty"` // Available hooks + Enabled bool `json:"enabled"` + Hooks []PostProcessingHook `json:"hooks,omitempty"` } type ExtensionManifest struct { @@ -108,19 +108,19 @@ type ExtensionManifest struct { Author string `json:"author"` Description string `json:"description"` Homepage string `json:"homepage,omitempty"` - Icon string `json:"icon,omitempty"` // Icon filename (e.g., "icon.png") + Icon string `json:"icon,omitempty"` Types []ExtensionType `json:"type"` Permissions ExtensionPermissions `json:"permissions"` Settings []ExtensionSetting `json:"settings,omitempty"` - QualityOptions []QualityOption `json:"qualityOptions,omitempty"` // Custom quality options for download providers + QualityOptions []QualityOption `json:"qualityOptions,omitempty"` MinAppVersion string `json:"minAppVersion,omitempty"` - SkipMetadataEnrichment bool `json:"skipMetadataEnrichment,omitempty"` // If true, don't enrich metadata from Deezer/Spotify - SkipBuiltInFallback bool `json:"skipBuiltInFallback,omitempty"` // If true, don't fallback to built-in providers (tidal/qobuz/amazon) - SearchBehavior *SearchBehaviorConfig `json:"searchBehavior,omitempty"` // Custom search behavior - URLHandler *URLHandlerConfig `json:"urlHandler,omitempty"` // Custom URL handling - TrackMatching *TrackMatchingConfig `json:"trackMatching,omitempty"` // Custom track matching - PostProcessing *PostProcessingConfig `json:"postProcessing,omitempty"` // Post-processing hooks - Capabilities map[string]interface{} `json:"capabilities,omitempty"` // Extension capabilities (homeFeed, browseCategories, etc.) + SkipMetadataEnrichment bool `json:"skipMetadataEnrichment,omitempty"` + SkipBuiltInFallback bool `json:"skipBuiltInFallback,omitempty"` + SearchBehavior *SearchBehaviorConfig `json:"searchBehavior,omitempty"` + URLHandler *URLHandlerConfig `json:"urlHandler,omitempty"` + TrackMatching *TrackMatchingConfig `json:"trackMatching,omitempty"` + PostProcessing *PostProcessingConfig `json:"postProcessing,omitempty"` + Capabilities map[string]interface{} `json:"capabilities,omitempty"` } type ManifestValidationError struct { diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index 559f10d6..61674062 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -23,9 +23,8 @@ type ExtensionAuthState struct { RefreshToken string ExpiresAt time.Time IsAuthenticated bool - // PKCE support - PKCEVerifier string - PKCEChallenge string + PKCEVerifier string + PKCEChallenge string } type PendingAuthRequest struct { @@ -210,7 +209,7 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { httpObj.Set("put", r.httpPut) httpObj.Set("delete", r.httpDelete) httpObj.Set("patch", r.httpPatch) - httpObj.Set("request", r.httpRequest) // Generic HTTP request (GET, POST, PUT, DELETE, etc.) + httpObj.Set("request", r.httpRequest) httpObj.Set("clearCookies", r.httpClearCookies) vm.Set("http", httpObj) @@ -220,7 +219,6 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { storageObj.Set("remove", r.storageRemove) vm.Set("storage", storageObj) - // Secure Credentials API (encrypted storage for sensitive data) credentialsObj := vm.NewObject() credentialsObj.Set("store", r.credentialsStore) credentialsObj.Set("get", r.credentialsGet) @@ -235,7 +233,6 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { authObj.Set("clearAuth", r.authClear) authObj.Set("isAuthenticated", r.authIsAuthenticated) authObj.Set("getTokens", r.authGetTokens) - // PKCE support authObj.Set("generatePKCE", r.authGeneratePKCE) authObj.Set("getPKCE", r.authGetPKCE) authObj.Set("startOAuthWithPKCE", r.authStartOAuthWithPKCE) @@ -277,14 +274,12 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { utilsObj.Set("hmacSHA1", r.hmacSHA1) utilsObj.Set("parseJSON", r.parseJSON) utilsObj.Set("stringifyJSON", r.stringifyJSON) - // Crypto utilities for developers utilsObj.Set("encrypt", r.cryptoEncrypt) utilsObj.Set("decrypt", r.cryptoDecrypt) utilsObj.Set("generateKey", r.cryptoGenerateKey) utilsObj.Set("randomUserAgent", r.randomUserAgent) vm.Set("utils", utilsObj) - // Log object (already set in extension_manager.go, but we can enhance it) logObj := vm.NewObject() logObj.Set("debug", r.logDebug) logObj.Set("info", r.logInfo) @@ -296,10 +291,6 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { gobackendObj.Set("sanitizeFilename", r.sanitizeFilenameWrapper) vm.Set("gobackend", gobackendObj) - // ==================== Browser-like Polyfills ==================== - // These make porting browser/Node.js libraries easier - - // Global fetch() - Promise-style HTTP API (browser-compatible) vm.Set("fetch", r.fetchPolyfill) vm.Set("atob", r.atobPolyfill) diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go index 42460937..da53b5dc 100644 --- a/go_backend/extension_runtime_http.go +++ b/go_backend/extension_runtime_http.go @@ -73,16 +73,14 @@ func (r *ExtensionRuntime) httpGet(call goja.FunctionCall) goja.Value { }) } - // Set headers - user headers first for k, v := range headers { req.Header.Set(k, v) } - // Only set default User-Agent if not provided by extension + if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", "Spotiflac-Extension/1.0") } - // Execute request resp, err := r.httpClient.Do(req) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -98,19 +96,18 @@ func (r *ExtensionRuntime) httpGet(call goja.FunctionCall) goja.Value { }) } - // Extract response headers - return all values as arrays for multi-value headers (cookies, etc.) respHeaders := make(map[string]interface{}) for k, v := range resp.Header { if len(v) == 1 { respHeaders[k] = v[0] } else { - respHeaders[k] = v // Return as array if multiple values + respHeaders[k] = v } } return r.vm.ToValue(map[string]interface{}{ "statusCode": resp.StatusCode, - "status": resp.StatusCode, // Alias for convenience + "status": resp.StatusCode, "ok": resp.StatusCode >= 200 && resp.StatusCode < 300, "body": string(body), "headers": respHeaders, @@ -133,7 +130,6 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { }) } - // Get body if provided - support both string and object var bodyStr string if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) { bodyArg := call.Arguments[1].Export() @@ -141,7 +137,6 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { case string: bodyStr = v case map[string]interface{}, []interface{}: - // Auto-stringify objects and arrays to JSON jsonBytes, err := json.Marshal(v) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -150,12 +145,10 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { } bodyStr = string(jsonBytes) default: - // Fallback to string conversion bodyStr = call.Arguments[1].String() } } - // Get headers if provided headers := make(map[string]string) if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) { headersObj := call.Arguments[2].Export() @@ -173,11 +166,10 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { }) } - // Set headers - user headers first for k, v := range headers { req.Header.Set(k, v) } - // Only set defaults if not provided by extension + if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", "Spotiflac-Extension/1.0") } @@ -185,7 +177,6 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { req.Header.Set("Content-Type", "application/json") } - // Execute request resp, err := r.httpClient.Do(req) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -201,19 +192,18 @@ func (r *ExtensionRuntime) httpPost(call goja.FunctionCall) goja.Value { }) } - // Extract response headers - return all values as arrays for multi-value headers respHeaders := make(map[string]interface{}) for k, v := range resp.Header { if len(v) == 1 { respHeaders[k] = v[0] } else { - respHeaders[k] = v // Return as array if multiple values + respHeaders[k] = v } } return r.vm.ToValue(map[string]interface{}{ "statusCode": resp.StatusCode, - "status": resp.StatusCode, // Alias for convenience + "status": resp.StatusCode, "ok": resp.StatusCode >= 200 && resp.StatusCode < 300, "body": string(body), "headers": respHeaders, @@ -236,27 +226,22 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { }) } - // Default options method := "GET" var bodyStr string headers := make(map[string]string) - // Parse options if provided if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) { optionsObj := call.Arguments[1].Export() if opts, ok := optionsObj.(map[string]interface{}); ok { - // Get method if m, ok := opts["method"].(string); ok { method = strings.ToUpper(m) } - // Get body - support both string and object if bodyArg, ok := opts["body"]; ok && bodyArg != nil { switch v := bodyArg.(type) { case string: bodyStr = v case map[string]interface{}, []interface{}: - // Auto-stringify objects and arrays to JSON jsonBytes, err := json.Marshal(v) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -269,7 +254,6 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { } } - // Get headers if h, ok := opts["headers"].(map[string]interface{}); ok { for k, v := range h { headers[k] = fmt.Sprintf("%v", v) @@ -278,7 +262,6 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { } } - // Create request var reqBody io.Reader if bodyStr != "" { reqBody = strings.NewReader(bodyStr) @@ -291,11 +274,10 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { }) } - // Set headers - user headers first for k, v := range headers { req.Header.Set(k, v) } - // Only set defaults if not provided by extension + if req.Header.Get("User-Agent") == "" { req.Header.Set("User-Agent", "Spotiflac-Extension/1.0") } @@ -303,7 +285,6 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { req.Header.Set("Content-Type", "application/json") } - // Execute request resp, err := r.httpClient.Do(req) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -319,20 +300,18 @@ func (r *ExtensionRuntime) httpRequest(call goja.FunctionCall) goja.Value { }) } - // Extract response headers - return all values as arrays for multi-value headers respHeaders := make(map[string]interface{}) for k, v := range resp.Header { if len(v) == 1 { respHeaders[k] = v[0] } else { - respHeaders[k] = v // Return as array if multiple values + respHeaders[k] = v } } - // Return response with helper properties return r.vm.ToValue(map[string]interface{}{ "statusCode": resp.StatusCode, - "status": resp.StatusCode, // Alias for convenience + "status": resp.StatusCode, "ok": resp.StatusCode >= 200 && resp.StatusCode < 300, "body": string(body), "headers": respHeaders, @@ -370,9 +349,7 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC var bodyStr string headers := make(map[string]string) - // For DELETE, second arg is headers; for PUT/PATCH, second arg is body if method == "DELETE" { - // http.delete(url, headers) if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) { headersObj := call.Arguments[1].Export() if h, ok := headersObj.(map[string]interface{}); ok { @@ -382,7 +359,6 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC } } } else { - // http.put(url, body, headers) / http.patch(url, body, headers) if len(call.Arguments) > 1 && !goja.IsUndefined(call.Arguments[1]) && !goja.IsNull(call.Arguments[1]) { bodyArg := call.Arguments[1].Export() switch v := bodyArg.(type) { @@ -411,7 +387,6 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC } } - // Create request var reqBody io.Reader if bodyStr != "" { reqBody = strings.NewReader(bodyStr) @@ -424,7 +399,6 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC }) } - // Set headers - user headers first for k, v := range headers { req.Header.Set(k, v) } @@ -435,7 +409,6 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC req.Header.Set("Content-Type", "application/json") } - // Execute request resp, err := r.httpClient.Do(req) if err != nil { return r.vm.ToValue(map[string]interface{}{ @@ -451,7 +424,6 @@ func (r *ExtensionRuntime) httpMethodShortcut(method string, call goja.FunctionC }) } - // Extract response headers respHeaders := make(map[string]interface{}) for k, v := range resp.Header { if len(v) == 1 { diff --git a/go_backend/extension_runtime_storage.go b/go_backend/extension_runtime_storage.go index 73108e20..5e9770ff 100644 --- a/go_backend/extension_runtime_storage.go +++ b/go_backend/extension_runtime_storage.go @@ -17,12 +17,10 @@ import ( // ==================== Storage API ==================== -// getStoragePath returns the path to the extension's storage file func (r *ExtensionRuntime) getStoragePath() string { return filepath.Join(r.dataDir, "storage.json") } -// loadStorage loads the storage data from disk func (r *ExtensionRuntime) loadStorage() (map[string]interface{}, error) { storagePath := r.getStoragePath() data, err := os.ReadFile(storagePath) @@ -41,7 +39,6 @@ func (r *ExtensionRuntime) loadStorage() (map[string]interface{}, error) { return storage, nil } -// saveStorage saves the storage data to disk func (r *ExtensionRuntime) saveStorage(storage map[string]interface{}) error { storagePath := r.getStoragePath() data, err := json.MarshalIndent(storage, "", " ") @@ -52,7 +49,6 @@ func (r *ExtensionRuntime) saveStorage(storage map[string]interface{}) error { return os.WriteFile(storagePath, data, 0644) } -// storageGet retrieves a value from storage func (r *ExtensionRuntime) storageGet(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return goja.Undefined() @@ -68,7 +64,6 @@ func (r *ExtensionRuntime) storageGet(call goja.FunctionCall) goja.Value { value, exists := storage[key] if !exists { - // Return default value if provided if len(call.Arguments) > 1 { return call.Arguments[1] } @@ -78,7 +73,6 @@ func (r *ExtensionRuntime) storageGet(call goja.FunctionCall) goja.Value { return r.vm.ToValue(value) } -// storageSet stores a value in storage func (r *ExtensionRuntime) storageSet(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(false) @@ -103,7 +97,6 @@ func (r *ExtensionRuntime) storageSet(call goja.FunctionCall) goja.Value { return r.vm.ToValue(true) } -// storageRemove removes a value from storage func (r *ExtensionRuntime) storageRemove(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue(false) @@ -127,19 +120,14 @@ func (r *ExtensionRuntime) storageRemove(call goja.FunctionCall) goja.Value { return r.vm.ToValue(true) } -// ==================== Credentials API (Encrypted Storage) ==================== - -// getCredentialsPath returns the path to the extension's encrypted credentials file func (r *ExtensionRuntime) getCredentialsPath() string { return filepath.Join(r.dataDir, ".credentials.enc") } -// getSaltPath returns the path to the extension's encryption salt file func (r *ExtensionRuntime) getSaltPath() string { return filepath.Join(r.dataDir, ".cred_salt") } -// getOrCreateSalt gets existing salt or creates a new random one func (r *ExtensionRuntime) getOrCreateSalt() ([]byte, error) { saltPath := r.getSaltPath() @@ -160,22 +148,17 @@ func (r *ExtensionRuntime) getOrCreateSalt() ([]byte, error) { return salt, nil } -// getEncryptionKey derives an encryption key from extension ID + random salt func (r *ExtensionRuntime) getEncryptionKey() ([]byte, error) { - // Get or create per-installation random salt salt, err := r.getOrCreateSalt() if err != nil { return nil, err } - // Combine extension ID + random salt for key derivation - // This makes each installation unique, preventing mass decryption attacks combined := append([]byte(r.extensionID), salt...) hash := sha256.Sum256(combined) return hash[:], nil } -// loadCredentials loads and decrypts credentials from disk func (r *ExtensionRuntime) loadCredentials() (map[string]interface{}, error) { credPath := r.getCredentialsPath() data, err := os.ReadFile(credPath) @@ -186,7 +169,6 @@ func (r *ExtensionRuntime) loadCredentials() (map[string]interface{}, error) { return nil, err } - // Decrypt the data key, err := r.getEncryptionKey() if err != nil { return nil, fmt.Errorf("failed to get encryption key: %w", err) @@ -204,7 +186,6 @@ func (r *ExtensionRuntime) loadCredentials() (map[string]interface{}, error) { return creds, nil } -// saveCredentials encrypts and saves credentials to disk func (r *ExtensionRuntime) saveCredentials(creds map[string]interface{}) error { data, err := json.Marshal(creds) if err != nil { @@ -221,10 +202,9 @@ func (r *ExtensionRuntime) saveCredentials(creds map[string]interface{}) error { } credPath := r.getCredentialsPath() - return os.WriteFile(credPath, encrypted, 0600) // Restrictive permissions + return os.WriteFile(credPath, encrypted, 0600) } -// credentialsStore stores an encrypted credential func (r *ExtensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(map[string]interface{}{ @@ -260,7 +240,6 @@ func (r *ExtensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value { }) } -// credentialsGet retrieves a decrypted credential func (r *ExtensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return goja.Undefined() @@ -276,7 +255,6 @@ func (r *ExtensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value { value, exists := creds[key] if !exists { - // Return default value if provided if len(call.Arguments) > 1 { return call.Arguments[1] } @@ -286,7 +264,6 @@ func (r *ExtensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value { return r.vm.ToValue(value) } -// credentialsRemove removes a credential func (r *ExtensionRuntime) credentialsRemove(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue(false) @@ -310,7 +287,6 @@ func (r *ExtensionRuntime) credentialsRemove(call goja.FunctionCall) goja.Value return r.vm.ToValue(true) } -// credentialsHas checks if a credential exists func (r *ExtensionRuntime) credentialsHas(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue(false) @@ -327,9 +303,6 @@ func (r *ExtensionRuntime) credentialsHas(call goja.FunctionCall) goja.Value { return r.vm.ToValue(exists) } -// ==================== Crypto Utilities ==================== - -// encryptAES encrypts data using AES-GCM func encryptAES(plaintext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -350,7 +323,6 @@ func encryptAES(plaintext []byte, key []byte) ([]byte, error) { return ciphertext, nil } -// decryptAES decrypts data using AES-GCM func decryptAES(ciphertext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/go_backend/extension_runtime_utils.go b/go_backend/extension_runtime_utils.go index 568b2366..c3a7675e 100644 --- a/go_backend/extension_runtime_utils.go +++ b/go_backend/extension_runtime_utils.go @@ -19,7 +19,6 @@ import ( // ==================== Utility Functions ==================== -// base64Encode encodes a string to base64 func (r *ExtensionRuntime) base64Encode(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -28,7 +27,6 @@ func (r *ExtensionRuntime) base64Encode(call goja.FunctionCall) goja.Value { return r.vm.ToValue(base64.StdEncoding.EncodeToString([]byte(input))) } -// base64Decode decodes a base64 string func (r *ExtensionRuntime) base64Decode(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -41,7 +39,6 @@ func (r *ExtensionRuntime) base64Decode(call goja.FunctionCall) goja.Value { return r.vm.ToValue(string(decoded)) } -// md5Hash computes MD5 hash of a string func (r *ExtensionRuntime) md5Hash(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -51,7 +48,6 @@ func (r *ExtensionRuntime) md5Hash(call goja.FunctionCall) goja.Value { return r.vm.ToValue(hex.EncodeToString(hash[:])) } -// sha256Hash computes SHA256 hash of a string func (r *ExtensionRuntime) sha256Hash(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -61,7 +57,6 @@ func (r *ExtensionRuntime) sha256Hash(call goja.FunctionCall) goja.Value { return r.vm.ToValue(hex.EncodeToString(hash[:])) } -// hmacSHA256 computes HMAC-SHA256 of a message with a key func (r *ExtensionRuntime) hmacSHA256(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue("") @@ -74,7 +69,6 @@ func (r *ExtensionRuntime) hmacSHA256(call goja.FunctionCall) goja.Value { return r.vm.ToValue(hex.EncodeToString(mac.Sum(nil))) } -// hmacSHA256Base64 computes HMAC-SHA256 and returns base64 encoded result func (r *ExtensionRuntime) hmacSHA256Base64(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue("") @@ -87,9 +81,6 @@ func (r *ExtensionRuntime) hmacSHA256Base64(call goja.FunctionCall) goja.Value { return r.vm.ToValue(base64.StdEncoding.EncodeToString(mac.Sum(nil))) } -// hmacSHA1 computes HMAC-SHA1 of a message with a key (for TOTP) -// Arguments: message (string or array of bytes), key (string or array of bytes) -// Returns: array of bytes (for TOTP dynamic truncation) func (r *ExtensionRuntime) hmacSHA1(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue([]byte{}) @@ -142,7 +133,6 @@ func (r *ExtensionRuntime) hmacSHA1(call goja.FunctionCall) goja.Value { return r.vm.ToValue(jsArray) } -// parseJSON parses a JSON string func (r *ExtensionRuntime) parseJSON(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return goja.Undefined() @@ -158,7 +148,6 @@ func (r *ExtensionRuntime) parseJSON(call goja.FunctionCall) goja.Value { return r.vm.ToValue(result) } -// stringifyJSON converts a value to JSON string func (r *ExtensionRuntime) stringifyJSON(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -174,9 +163,6 @@ func (r *ExtensionRuntime) stringifyJSON(call goja.FunctionCall) goja.Value { return r.vm.ToValue(string(data)) } -// ==================== Crypto Utilities for Extensions ==================== - -// cryptoEncrypt encrypts a string using AES-GCM (for extension use) func (r *ExtensionRuntime) cryptoEncrypt(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(map[string]interface{}{ @@ -188,7 +174,6 @@ func (r *ExtensionRuntime) cryptoEncrypt(call goja.FunctionCall) goja.Value { plaintext := call.Arguments[0].String() keyStr := call.Arguments[1].String() - // Derive 32-byte key from provided key string keyHash := sha256.Sum256([]byte(keyStr)) encrypted, err := encryptAES([]byte(plaintext), keyHash[:]) @@ -205,7 +190,6 @@ func (r *ExtensionRuntime) cryptoEncrypt(call goja.FunctionCall) goja.Value { }) } -// cryptoDecrypt decrypts a string using AES-GCM (for extension use) func (r *ExtensionRuntime) cryptoDecrypt(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return r.vm.ToValue(map[string]interface{}{ @@ -225,14 +209,13 @@ func (r *ExtensionRuntime) cryptoDecrypt(call goja.FunctionCall) goja.Value { }) } - // Derive 32-byte key from provided key string keyHash := sha256.Sum256([]byte(keyStr)) decrypted, err := decryptAES(ciphertext, keyHash[:]) if err != nil { return r.vm.ToValue(map[string]interface{}{ "success": false, - "error": err.Error(), + "error": "invalid base64 ciphertext", }) } @@ -242,9 +225,8 @@ func (r *ExtensionRuntime) cryptoDecrypt(call goja.FunctionCall) goja.Value { }) } -// cryptoGenerateKey generates a random encryption key func (r *ExtensionRuntime) cryptoGenerateKey(call goja.FunctionCall) goja.Value { - length := 32 // Default 256-bit key + length := 32 if len(call.Arguments) > 0 && !goja.IsUndefined(call.Arguments[0]) { if l, ok := call.Arguments[0].Export().(float64); ok { length = int(l) @@ -266,13 +248,10 @@ func (r *ExtensionRuntime) cryptoGenerateKey(call goja.FunctionCall) goja.Value }) } -// randomUserAgent returns a random Chrome User-Agent string func (r *ExtensionRuntime) randomUserAgent(call goja.FunctionCall) goja.Value { return r.vm.ToValue(getRandomUserAgent()) } -// ==================== Logging Functions ==================== - func (r *ExtensionRuntime) logDebug(call goja.FunctionCall) goja.Value { msg := r.formatLogArgs(call.Arguments) GoLog("[Extension:%s:DEBUG] %s\n", r.extensionID, msg) @@ -305,8 +284,6 @@ func (r *ExtensionRuntime) formatLogArgs(args []goja.Value) string { return strings.Join(parts, " ") } -// ==================== Go Backend Wrappers ==================== - func (r *ExtensionRuntime) sanitizeFilenameWrapper(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return r.vm.ToValue("") @@ -315,7 +292,6 @@ func (r *ExtensionRuntime) sanitizeFilenameWrapper(call goja.FunctionCall) goja. return r.vm.ToValue(sanitizeFilename(input)) } -// RegisterGoBackendAPIs adds more Go backend functions to the VM func (r *ExtensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { gobackendObj := vm.Get("gobackend") if gobackendObj == nil || goja.IsUndefined(gobackendObj) { @@ -325,7 +301,6 @@ func (r *ExtensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { obj := gobackendObj.(*goja.Object) - // Expose sanitizeFilename obj.Set("sanitizeFilename", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return vm.ToValue("") @@ -333,7 +308,6 @@ func (r *ExtensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { return vm.ToValue(sanitizeFilename(call.Arguments[0].String())) }) - // Expose getAudioQuality obj.Set("getAudioQuality", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { return vm.ToValue(map[string]interface{}{ @@ -356,7 +330,6 @@ func (r *ExtensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { }) }) - // Expose buildFilename obj.Set("buildFilename", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 2 { return vm.ToValue("") @@ -373,7 +346,6 @@ func (r *ExtensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) { return vm.ToValue(buildFilenameFromTemplate(template, metadata)) }) - // Expose getLocalTime - returns device local time info obj.Set("getLocalTime", func(call goja.FunctionCall) goja.Value { now := time.Now() _, offsetSeconds := now.Zone()