From ad78b4307204f22ac2993ef60f0f3741a6136dc3 Mon Sep 17 00:00:00 2001 From: PIERLUIGI VITI Date: Fri, 28 Aug 2026 19:21:35 +0200 Subject: [PATCH 1/2] fix(errors): a bare forbidden is not about payers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hint read "on create the payer must be the signed-in address" for every 403 forbidden the gateway sends. It describes a rule that no longer exists there: grepping the gateway for it returns nothing. The gateway split the party-mismatch 403s into codes of their own — not_the_payee, not_the_payer, wallet_deactivated, not_your_account — because they need separate fixes, and its own catalogue has read generically ever since. This entry kept the pre-split sentence. Three sites answer a bare forbidden, none about payers: the operator grant, a webhook owned by another account, and a transaction signed by a party the operation does not allow. The first sends NO detail, deliberately — so this hint is the only thing its caller reads, and an admin whose grant lapsed was told to check the payer address. --- lib/rail0/error_hints.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rail0/error_hints.rb b/lib/rail0/error_hints.rb index 4f99fd5..36baba7 100644 --- a/lib/rail0/error_hints.rb +++ b/lib/rail0/error_hints.rb @@ -62,7 +62,11 @@ module Rail0 "unknown_token" => "the token isn't configured on this chain", "no_active_contract" => "no active RAIL0 contract on that chain", "missing_param" => "a required parameter is missing from the request", - "forbidden" => "not permitted for this session — on create the payer must be the signed-in address" + # A BARE forbidden is not a party mismatch: the gateway split those into codes of + # their own (not_the_payee, not_the_payer, wallet_deactivated, not_your_account) + # because they need different fixes. This entry kept describing one of them long + # after the split — and the rule it named no longer exists in the gateway at all. + "forbidden" => "not permitted for this session — typically the operator grant, a resource owned by another account, or a transaction signed by the wrong wallet" }.freeze # An actionable hint for a rail0 error code, or nil when the code is unknown. From 58bf7af8a44af98c1e76c9253340199b72bcfc97 Mon Sep 17 00:00:00 2001 From: PIERLUIGI VITI Date: Fri, 28 Aug 2026 19:24:31 +0200 Subject: [PATCH 2/2] test(errors): the spec pinned the sentence that was wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It asserted the forbidden hint includes "payer", under the title "explains what forbidden usually means on create" — so the stale wording had a guard holding it in place, which is why it survived the gateway splitting those 403s into codes of their own. Now it asserts what a bare forbidden actually is, and adds the negative: the hint must NOT mention a payer. That is the sentence that crept in and stayed, so it is the one worth guarding against. Mea culpa on the CI failure: I ran `ruby -c` and rubocop on the file I changed and not the suite, which is exactly where a spec pinning the old copy would speak up. --- spec/errors_spec.rb | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index fd57d71..927114f 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -112,10 +112,19 @@ def stub_error(status, body) end end - # `forbidden` is the one whose hint has to say something the code cannot: the - # payer/caller rule on create is the most common way to hit it. - it "explains what forbidden usually means on create" do - expect(Rail0.describe_error("forbidden")).to include("payer") + # `forbidden` is the one whose hint has to say something the code cannot, because the + # gateway sends it with NO detail for the case that matters most — a missing operator + # grant — so the hint is everything the caller reads. + # + # It used to promise the payer rule on create, and this example pinned that promise. + # Both were wrong: the gateway split the party mismatches into codes of their own + # (not_the_payee, not_the_payer, wallet_deactivated, not_your_account) and no longer + # has the rule at all. The negative assertion is the guard — it is exactly the sentence + # that crept in and stayed. + it "names what a bare forbidden actually is, not the rule that was split out" do + hint = Rail0.describe_error("forbidden") + expect(hint).to include("operator grant") + expect(hint).not_to include("payer") end end end