diff --git a/features/credentials.feature b/features/credentials.feature index 0603aee..15febbc 100644 --- a/features/credentials.feature +++ b/features/credentials.feature @@ -33,7 +33,7 @@ Feature: Manage AI provider credentials When I run `wp ai credentials list --format=json` Then STDOUT should be JSON containing: """ - [{"provider":"openai","api_key":"sk-*********6789"}] + [{"provider":"openai","api_key":"••••••••••••6789"}] """ @require-wp-7.0 @@ -51,7 +51,7 @@ Feature: Manage AI provider credentials """ And STDOUT should contain: """ - "api_key":"sk-**********-123" + "api_key":"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022-123" """ @require-wp-7.0 @@ -80,7 +80,7 @@ Feature: Manage AI provider credentials When I try `wp ai credentials get nonexistent` Then STDERR should contain: """ - Error: Credentials for provider "nonexistent" not found. + Error: Provider "nonexistent" is not a supported AI connector. """ And the return code should be 1 @@ -100,8 +100,8 @@ Feature: Manage AI provider credentials And I run `wp ai credentials list` Then STDOUT should be a table containing rows: | provider | api_key | - | openai | sk-*****i123 | - | anthropic | sk-*******-456 | + | openai | ••••••••i123 | + | anthropic | ••••••••••-456 | @require-wp-7.0 Scenario: Update existing credentials @@ -120,5 +120,5 @@ Feature: Manage AI provider credentials When I run `wp ai credentials get openai --format=json` Then STDOUT should contain: """ - "api_key":"new****-456" + "api_key":"\u2022\u2022\u2022\u2022\u2022\u2022\u2022-456" """ diff --git a/src/Credentials_Command.php b/src/Credentials_Command.php index 48e687f..b444f9d 100644 --- a/src/Credentials_Command.php +++ b/src/Credentials_Command.php @@ -24,11 +24,6 @@ */ class Credentials_Command extends WP_CLI_Command { - /** - * The option name where credentials are stored. - */ - const OPTION_NAME = 'wp_ai_client_provider_credentials'; - /** * Lists all stored AI provider credentials. * @@ -52,7 +47,7 @@ class Credentials_Command extends WP_CLI_Command { * +----------+----------+ * | provider | api_key | * +----------+----------+ - * | openai | sk-***** | + * | openai | ••••••• | * +----------+----------+ * * @subcommand list @@ -74,7 +69,7 @@ public function list_( $args, $assoc_args ) { foreach ( $credentials as $provider => $api_key ) { $items[] = array( 'provider' => $provider, - 'api_key' => $this->mask_api_key( $api_key ?? '' ), + 'api_key' => $api_key, ); } @@ -103,7 +98,7 @@ public function list_( $args, $assoc_args ) { * * # Get OpenAI credentials * $ wp ai credentials get openai - * {"provider":"openai","api_key":"sk-*****"} + * {"provider":"openai","api_key":"••••••••••••6789"} * * @when after_wp_load * @@ -114,15 +109,17 @@ public function list_( $args, $assoc_args ) { public function get( $args, $assoc_args ) { list( $provider ) = $args; - $credentials = $this->get_all_credentials(); + $option_name = $this->get_connector_setting_name( $provider ); + $raw_key = get_option( $option_name, '' ); + $api_key = is_string( $raw_key ) ? $raw_key : ''; - if ( ! isset( $credentials[ $provider ] ) ) { + if ( '' === $api_key ) { WP_CLI::error( sprintf( 'Credentials for provider "%s" not found.', $provider ) ); } $data = array( 'provider' => $provider, - 'api_key' => $this->mask_api_key( $credentials[ $provider ] ?? '' ), + 'api_key' => $api_key, ); $format = $assoc_args['format'] ?? 'json'; @@ -164,11 +161,11 @@ public function set( $args, $assoc_args ) { list( $provider ) = $args; $api_key = $assoc_args['api-key']; - $credentials = $this->get_all_credentials(); - - $credentials[ $provider ] = $api_key; + $option_name = $this->get_connector_setting_name( $provider ); - $this->save_all_credentials( $credentials ); + // Remove any sanitize callback to bypass provider-side validation (e.g., live API checks). + remove_all_filters( "sanitize_option_{$option_name}" ); + update_option( $option_name, $api_key, false ); WP_CLI::success( sprintf( 'Credentials for provider "%s" have been saved.', $provider ) ); } @@ -196,69 +193,87 @@ public function set( $args, $assoc_args ) { public function delete( $args, $assoc_args ) { list( $provider ) = $args; - $credentials = $this->get_all_credentials(); + $option_name = $this->get_connector_setting_name( $provider ); + $raw_key = get_option( $option_name, '' ); + $api_key = is_string( $raw_key ) ? $raw_key : ''; - if ( ! isset( $credentials[ $provider ] ) ) { + if ( '' === $api_key ) { WP_CLI::error( sprintf( 'Credentials for provider "%s" not found.', $provider ) ); } - unset( $credentials[ $provider ] ); - $this->save_all_credentials( $credentials ); + delete_option( $option_name ); WP_CLI::success( sprintf( 'Credentials for provider "%s" have been deleted.', $provider ) ); } /** - * Gets all credentials from the database. + * Gets the option name for a provider's API key from the connector registry. * - * @return array + * @param string $provider The connector/provider ID. + * @return string The option name. */ - private function get_all_credentials() { - $credentials = get_option( self::OPTION_NAME, array() ); + private function get_connector_setting_name( string $provider ): string { + if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { + WP_CLI::error( 'Requires WordPress 7.0 or greater.' ); + } - if ( ! is_array( $credentials ) ) { - return array(); + $settings = _wp_connectors_get_connector_settings(); + + if ( ! isset( $settings[ $provider ] ) ) { + WP_CLI::error( sprintf( 'Provider "%s" is not a supported AI connector.', $provider ) ); } - /** - * @var array $credentials - */ + $setting_name = $this->get_api_key_setting_name( $settings[ $provider ]['authentication'] ?? [] ); - return $credentials; + if ( null === $setting_name ) { + WP_CLI::error( sprintf( 'Provider "%s" does not support API key authentication.', $provider ) ); + } + + return $setting_name; } /** - * Saves all credentials to the database. + * Returns the option/setting name if the given authentication config is an API key type, or null otherwise. * - * @param array $credentials The credentials to save. - * @return bool + * @param mixed $auth Authentication config from the connector registry. + * @return string|null */ - private function save_all_credentials( $credentials ) { - if ( empty( $credentials ) ) { - return delete_option( self::OPTION_NAME ); + private function get_api_key_setting_name( $auth ): ?string { + if ( ! is_array( $auth ) || ! isset( $auth['method'] ) || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { + return null; } - return update_option( self::OPTION_NAME, $credentials, false ); + return (string) $auth['setting_name']; } /** - * Masks an API key for display purposes. + * Gets all credentials from the database. * - * @param string $api_key The API key to mask. - * @return string + * @return array */ - private function mask_api_key( $api_key ) { - if ( empty( $api_key ) ) { - return ''; + private function get_all_credentials() { + if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { + return array(); } - $length = strlen( $api_key ); + $credentials = array(); - if ( $length <= 8 ) { - return str_repeat( '*', $length ); + foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { + $setting_name = $this->get_api_key_setting_name( $connector_data['authentication'] ?? [] ); + + if ( null === $setting_name ) { + continue; + } + + $raw = get_option( $setting_name, '' ); + $value = is_string( $raw ) ? $raw : ''; + if ( '' !== $value ) { + $credentials[ $connector_id ] = $value; + } } - // Show first 3 and last 4 characters - return substr( $api_key, 0, 3 ) . str_repeat( '*', min( 10, $length - 7 ) ) . substr( $api_key, -4 ); + ksort( $credentials ); + + return $credentials; } }