From 43a24de0ccd453baf1e5d1fc92b9f58e5381031f Mon Sep 17 00:00:00 2001 From: jacksonhuether Date: Wed, 25 Feb 2026 17:57:23 -0500 Subject: [PATCH 1/4] Fix missing Content-Type header on OAuth token request The create_token method sends a form-encoded POST body but does not set the Content-Type header to application/x-www-form-urlencoded. Some OAuth servers (including AWS Cognito) return 405 Method Not Allowed when the header is absent because they cannot identify the request as a valid form POST. Co-Authored-By: Claude Opus 4.6 --- lib/fragment_client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fragment_client.rb b/lib/fragment_client.rb index 50ea0e6..db318ed 100644 --- a/lib/fragment_client.rb +++ b/lib/fragment_client.rb @@ -166,6 +166,7 @@ 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.content_type = "application/x-www-form-urlencoded" post.body = format('grant_type=client_credentials&scope=%s&client_id=%s', scope: @oauth_scope, id: @client_id) From 202bc2d5acc821f159c34ac1175b4a326c91aa9f Mon Sep 17 00:00:00 2001 From: Steven Klaiber-Noble Date: Mon, 2 Mar 2026 19:07:05 -0800 Subject: [PATCH 2/4] Bump version to 1.4.6 and sync schema Updates release version metadata and regenerates fragment.schema.json using the current schema sync process. Made-with: Cursor --- Gemfile.lock | 2 +- lib/fragment.schema.json | 45 ++++++++++++++++++++++++++++++++++ lib/fragment_client/version.rb | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) 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/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 From 869aa4d0d6530fefa433c9c38091a98e3a803a36 Mon Sep 17 00:00:00 2001 From: Steven Klaiber-Noble Date: Mon, 2 Mar 2026 19:11:30 -0800 Subject: [PATCH 3/4] Assert OAuth token request Content-Type per RFC 6749 Extends the OAuth request spec test to verify the token request uses application/x-www-form-urlencoded content type, aligned with RFC 6749 section 4.4.2 and its example request. Made-with: Cursor --- test/unit_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unit_test.rb b/test/unit_test.rb index 7609a1c..8315fc2 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." From c480bda6bf7b3c00fe2c6df77a81a4863e7c331c Mon Sep 17 00:00:00 2001 From: Steven Klaiber-Noble Date: Mon, 2 Mar 2026 19:13:58 -0800 Subject: [PATCH 4/4] Use Appendix B form encoding for OAuth token body Build the token request body with URI.encode_www_form to satisfy RFC 6749 section 4.4.2 / Appendix B UTF-8 form encoding requirements. Adds a test that verifies client_id and scope round-trip with special characters. Made-with: Cursor --- lib/fragment_client.rb | 7 +++++-- test/unit_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/fragment_client.rb b/lib/fragment_client.rb index db318ed..60b4b00 100644 --- a/lib/fragment_client.rb +++ b/lib/fragment_client.rb @@ -167,8 +167,11 @@ def create_token post = Net::HTTP::Post.new(uri.request_uri) post.basic_auth(@client_id, @client_secret) post.content_type = "application/x-www-form-urlencoded" - post.body = format('grant_type=client_credentials&scope=%s&client_id=%s', scope: @oauth_scope, - id: @client_id) + 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/test/unit_test.rb b/test/unit_test.rb index 8315fc2..f79c23d 100644 --- a/test/unit_test.rb +++ b/test/unit_test.rb @@ -347,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