diff --git a/Gemfile.lock b/Gemfile.lock index c67465d..ac2788c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - fragment-dev (1.4.5) + fragment-dev (1.4.6) graphql (>= 2.2.5, < 3.0) graphql-client (~> 0.23.0) sorbet-runtime (~> 0.5) diff --git a/lib/fragment.schema.json b/lib/fragment.schema.json index 96449d9..7c798a2 100644 --- a/lib/fragment.schema.json +++ b/lib/fragment.schema.json @@ -13800,6 +13800,16 @@ "ofType": null }, "defaultValue": null + }, + { + "name": "repeated", + "description": "Repeated expansion configuration. When set, this condition is expanded at runtime for each element\nin the array parameter named by the key.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SchemaRepeatedConfigInput", + "ofType": null + }, + "defaultValue": null } ], "interfaces": null, @@ -14114,6 +14124,16 @@ }, "defaultValue": null }, + { + "name": "repeated", + "description": "Repeated expansion configuration. When set, this line is expanded at runtime for each element\nin the array parameter named by the key.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SchemaRepeatedConfigInput", + "ofType": null + }, + "defaultValue": null + }, { "name": "tx", "description": "The external transaction to reconcile.\nThis field is required if the Ledger Account being posted to is a Linked Ledger Account. Otherwise, this field is disallowed.\nIt supports parameters in its attributes via handlebars syntax.\n\nSee the docs on [reconciling payments](https://fragment.dev/docs/reconcile-payments).", @@ -14164,6 +14184,31 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "SchemaRepeatedConfigInput", + "description": "Configuration for repeated expansion of a line or condition. The key names a client-supplied\narray parameter whose elements each generate one copy of the line or condition at runtime.", + "fields": null, + "inputFields": [ + { + "name": "key", + "description": "The key of the array parameter whose elements expand this line or condition.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "SafeString", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "SchemaTxMatchInput", diff --git a/lib/fragment_client.rb b/lib/fragment_client.rb index 50ea0e6..60b4b00 100644 --- a/lib/fragment_client.rb +++ b/lib/fragment_client.rb @@ -166,8 +166,12 @@ def create_token uri = URI.parse(@oauth_url.to_s) post = Net::HTTP::Post.new(uri.request_uri) post.basic_auth(@client_id, @client_secret) - post.body = format('grant_type=client_credentials&scope=%s&client_id=%s', scope: @oauth_scope, - id: @client_id) + post.content_type = "application/x-www-form-urlencoded" + post.body = URI.encode_www_form( + grant_type: 'client_credentials', + scope: @oauth_scope, + client_id: @client_id + ) begin http = Net::HTTP.new(uri.host, uri.port) diff --git a/lib/fragment_client/version.rb b/lib/fragment_client/version.rb index a3a2a1a..64dc7e4 100644 --- a/lib/fragment_client/version.rb +++ b/lib/fragment_client/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FragmentSDK - VERSION = '1.4.5' + VERSION = '1.4.6' end diff --git a/test/unit_test.rb b/test/unit_test.rb index 7609a1c..f79c23d 100644 --- a/test/unit_test.rb +++ b/test/unit_test.rb @@ -299,6 +299,13 @@ def test_token_request_conforms_to_oauth2_and_http_specs refute_nil body_params['scope'], 'scope parameter should be present when configured (RFC 6749 §4.4.2)' + # RFC 6749 §4.4.2 requires the token request entity-body to use the + # application/x-www-form-urlencoded format per Appendix B, and the + # section's example request explicitly sets this Content-Type. + content_type = captured_auth_request.headers['Content-Type'] + assert_match(/\Aapplication\/x-www-form-urlencoded\b/, content_type, + 'Token request Content-Type should be application/x-www-form-urlencoded (RFC 6749 §4.4.2)') + # RFC 6749 §2.3.1: "Clients in possession of a client password MAY use # the HTTP Basic authentication scheme [...] The client identifier is [...] # used as the username; the client password [...] used as the password." @@ -340,4 +347,26 @@ def test_token_request_uses_origin_form_with_custom_oauth_url $VERBOSE = verbose end end + + def test_token_request_uses_appendix_b_form_encoding_utf8 + captured_auth_request = nil + stub_request(:post, 'https://auth.fragment.dev/oauth2/token') + .to_return do |request| + captured_auth_request = request + { status: 200, body: { access_token: 'test_token', expires_in: 3600 }.to_json } + end + + client_id = 'client id+special&utf8-umlaut-umlauts-äöü' + oauth_scope = 'https://api.fragment.dev/read write?x=1&y=2' + + FragmentClient.new(client_id, 'secret', oauth_scope: oauth_scope) + + # RFC 6749 §4.4.2 requires x-www-form-urlencoded per Appendix B (UTF-8). + body_params = URI.decode_www_form(captured_auth_request.body).to_h + assert_equal 'client_credentials', body_params['grant_type'] + assert_equal oauth_scope, body_params['scope'], + 'scope should round-trip via x-www-form-urlencoded UTF-8 encoding' + assert_equal client_id, body_params['client_id'], + 'client_id should round-trip via x-www-form-urlencoded UTF-8 encoding' + end end