HTTP status (contract v2 §Errors). */ const CODES = array( 'invalid_payload' => 400, 'invalid_action' => 400, 'invalid_key' => 401, 'task_not_found' => 404, 'proof_unavailable' => 409, 'invalid_state' => 409, 'proof_purged' => 410, 'document_too_large' => 413, 'license_unavailable' => 503, // Operational safeguard outside the business dictionary (vault not configured). 'vault_unavailable' => 503, // Unexpected server-side failure whose cause is NOT the vault/KEK (honest diagnostic). 'internal_error' => 500, ); /** Opaque reference / token pattern (URL-safe base64 without padding). */ const REF_PATTERN = '[A-Za-z0-9_-]{16,128}'; /** * Registers the REST routes. * * @return void */ public static function register_routes() { register_rest_route( EQUASIGN_REST_NS, '/tasks', array( 'methods' => 'POST', 'callback' => array( 'EquaSign_Rest_Creer', 'process' ), 'permission_callback' => '__return_true', ) ); register_rest_route( EQUASIGN_REST_NS, '/tasks/(?P' . self::REF_PATTERN . ')', array( 'methods' => 'GET', 'callback' => array( 'EquaSign_Rest_Consulter', 'process' ), 'permission_callback' => '__return_true', 'args' => array( 'ref' => array( 'required' => true ) ), ) ); register_rest_route( EQUASIGN_REST_NS, '/tasks/(?P' . self::REF_PATTERN . ')/proof', array( 'methods' => 'GET', 'callback' => array( 'EquaSign_Rest_Preuve', 'process' ), 'permission_callback' => '__return_true', 'args' => array( 'ref' => array( 'required' => true ) ), ) ); register_rest_route( EQUASIGN_REST_NS, '/tasks/(?P' . self::REF_PATTERN . ')/lifecycle', array( 'methods' => 'POST', 'callback' => array( 'EquaSign_Rest_Cycle', 'process' ), 'permission_callback' => '__return_true', 'args' => array( 'ref' => array( 'required' => true ) ), ) ); } /** * Verifies the API key presented in the X-EquaSign-Key header against the * multi-key store. The request is authorized when the provided token matches * any NON-revoked key (constant-time comparison, see EquaSign_Repo). * * @param WP_REST_Request $request Request. * @return WP_REST_Response|null Error response (401) or null if valid. */ public static function authenticate( WP_REST_Request $request ) { $provided = (string) $request->get_header( 'X-EquaSign-Key' ); if ( '' === $provided || ! EquaSign_Repo::api_key_match( $provided ) ) { return self::error( 'invalid_key', __( 'Invalid or missing API key.', 'equasign' ) ); } // FREE — structural closure (arbitration #4): the REST API exists (the admin drives // it) but is NOT a public feature. A valid instance key is not enough; the request // must also carry a logged-in manage_equasign session, which an external caller // cannot forge (the instance key is not a secret). The admin client forwards the // operator's session on its loopback call, so the plugin's own admin keeps working. if ( is_user_logged_in() && current_user_can( 'manage_equasign' ) ) { return null; } return self::error( 'external_api_admin_only', __( 'This feature is not available from an external call.', 'equasign' ) ); } /** * Safeguard: is the vault (KEK) available? Otherwise we can neither * encrypt nor decrypt: we refuse cleanly. * * @return WP_REST_Response|null */ public static function verify_vault() { if ( ! EquaSign_Crypto::kek_present() ) { return self::error( 'vault_unavailable', __( 'Server vault key (EQUASIGN_KEK) is not configured.', 'equasign' ) ); } // NOTE: an unreadable *sealing key* (KEK changed) is deliberately NOT checked // here. verify_vault() also guards read-only operations (e.g. GET proof), which // need only the KEK-encrypted storage and the seal PUBLIC key embedded in the // dossier — never the seal secret — so a corrupted seal secret must not block // retrieval. Sealing itself is blocked at the front seal() action, which checks // EquaSign_Seal::available() so no unsigned dossier is ever produced. return null; } /** * Builds an error response matching the uniform envelope. * * @param string $code Stable dictionary code. * @param string $message Informative message. * @param array|null $details Optional details. * @return WP_REST_Response */ public static function error( $code, $message, $details = null ) { $http = isset( self::CODES[ $code ] ) ? self::CODES[ $code ] : 400; $body = array( 'error' => array( 'code' => $code, 'message' => $message, ), ); if ( null !== $details ) { $body['error']['details'] = $details; } return new WP_REST_Response( $body, $http ); } /** * Success response. * * @param array $body Body. * @param int $http HTTP status. * @return WP_REST_Response */ public static function success( array $body, $http = 200 ) { return new WP_REST_Response( $body, $http ); } }