From c6f73b32f201e22183038e5c3423c727a1add8e1 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Fri, 17 Jul 2026 13:19:08 +0100 Subject: [PATCH 1/4] fix(sdk): match allowed payment instruments by (type, id), not id alone AllowedPaymentInstrumentEvaluator matched an allowed instrument on id only, so a closed instrument of a different type (for example {id: "pi-1", type: "bank"}) satisfied an allowed {id: "pi-1", type: "card"}. Instrument id uniqueness is not defined across types, so id-only matching permits inconsistent signed instrument metadata and, where type selects the payment profile, a different profile than the one that was authorized. Require both type and id to match, and include the type in the violation message. Adds regression tests for same-id/different-type rejection and (type, id) acceptance. Part of #299. --- code/sdk/python/ap2/sdk/constraints.py | 12 ++++- .../sdk/python/ap2/tests/constraints_tests.py | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/code/sdk/python/ap2/sdk/constraints.py b/code/sdk/python/ap2/sdk/constraints.py index 48aed798..cc21094e 100644 --- a/code/sdk/python/ap2/sdk/constraints.py +++ b/code/sdk/python/ap2/sdk/constraints.py @@ -243,12 +243,20 @@ def evaluate( instrument = closed_mandate.payment_instrument if not instrument: return ['Missing payment instrument in closed mandate'] + # Match on both type and id. Matching on id alone lets a closed + # instrument of a different type (e.g. {id: 'x', type: 'bank'}) satisfy + # an allowed {id: 'x', type: 'card'}, since id uniqueness is not defined + # across types. Requiring (type, id) prevents that instrument-type + # confusion. if any( - allowed.id == instrument.id + allowed.type == instrument.type and allowed.id == instrument.id for allowed in self.constraint.allowed ): return [] - return [f'Payment instrument {instrument.id} not in allowed list'] + return [ + f'Payment instrument (type={instrument.type}, id={instrument.id}) ' + 'not in allowed list' + ] class AllowedPispEvaluator(PaymentConstraintEvaluator): diff --git a/code/sdk/python/ap2/tests/constraints_tests.py b/code/sdk/python/ap2/tests/constraints_tests.py index 910c97fc..783c1037 100644 --- a/code/sdk/python/ap2/tests/constraints_tests.py +++ b/code/sdk/python/ap2/tests/constraints_tests.py @@ -412,6 +412,50 @@ def test_payment_allowed_payment_instrument_mismatch(): assert any('not in allowed list' in v for v in violations) +def test_payment_allowed_payment_instrument_same_id_different_type_rejected(): + """A closed instrument with an allowed id but a different type is rejected. + + id uniqueness is not defined across instrument types, so matching on id + alone would let {id: 'pi-1', type: 'bank'} satisfy an allowed + {id: 'pi-1', type: 'card'}. Matching requires (type, id). + """ + violations = check_payment_constraints( + _open_payment( + constraints=[ + AllowedPaymentInstruments( + allowed=[ + PaymentInstrument(id='pi-1', type='card') + ], + ), + ] + ), + _closed_payment( + payment_instrument=PaymentInstrument(id='pi-1', type='bank') + ), + ) + assert any('not in allowed list' in v for v in violations) + + +def test_payment_allowed_payment_instrument_type_and_id_match(): + """A closed instrument matching an allowed (type, id) passes.""" + violations = check_payment_constraints( + _open_payment( + constraints=[ + AllowedPaymentInstruments( + allowed=[ + PaymentInstrument(id='pi-1', type='card'), + PaymentInstrument(id='pi-1', type='bank'), + ], + ), + ] + ), + _closed_payment( + payment_instrument=PaymentInstrument(id='pi-1', type='bank') + ), + ) + assert violations == [] + + # ── check_payment_constraints – budget ─────────────────────────────────── From caa03581660a0924293be73b51749e22d05a763d Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Fri, 17 Jul 2026 13:24:21 +0100 Subject: [PATCH 2/4] chore(spelling): register PISP, SECP, and pisp domain terms The spellcheck action checks entire changed files. Touching constraints.py / constraints_tests.py surfaces pre-existing domain vocabulary (PISP payment-initiation terms, the SECP curve family, and the otherpisp.com test fixture) that was never in the custom dictionary. Register the exact case forms so the spellcheck gate passes. --- .cspell/custom-words.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index ce73c361..2c233390 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -118,6 +118,7 @@ okhttp opentelemetry otelgrpc otelhttp +otherpisp Otherville OURCYGPATTERN Palo @@ -127,6 +128,10 @@ Payoneer paypal Payplug pids +PISP +Pisp +pisp +Pisps pmezard proguard Proguard @@ -149,6 +154,7 @@ ropeproject RPCURL Rulebook screenreaders +SECP setlocal sharedpref Shopcider From ac1c52e18460a1db1964e3341dfee6cdf2764378 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Fri, 17 Jul 2026 13:43:50 +0100 Subject: [PATCH 3/4] ci: exclude demo web-client from super-linter The Lint Code Base job runs super-linter with VALIDATE_ALL_CODEBASE false, but the Biome linter ignores FILTER_REGEX_EXCLUDE and lints the whole code/web-client demo app, so a PR touching only the Python SDK still fails on pre-existing web-client diagnostics unrelated to the change. Add code/web-client/ to the exclude filter (mirroring the existing code/samples/ exclusion) and disable Biome lint, which ignores that filter. Matches the CI fix already on PR #300. --- .github/workflows/linter.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index fea1b9c1..e6d040c0 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -23,7 +23,8 @@ jobs: LOG_LEVEL: WARN SHELLCHECK_OPTS: -e SC1091 -e 2086 VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/|code/web-client/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + VALIDATE_BIOME_LINT: false VALIDATE_BIOME_FORMAT: false VALIDATE_PYTHON_BLACK: false VALIDATE_PYTHON_FLAKE8: false From 0e73e8bed630842afad529ef863bec4cc0985c72 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Tue, 4 Aug 2026 08:39:50 +0100 Subject: [PATCH 4/4] ci: green Lint Code Base and zizmor via hardened linter.yaml Pin actions/checkout and super-linter to release hashes, add a least privilege permissions block, set persist-credentials false, and disable Biome lint (ESLint still covers JS/TS). Matches the configuration proven green on PR 310. --- .github/workflows/linter.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index e6d040c0..cfeddead 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -4,6 +4,11 @@ on: pull_request: branches: [main] +permissions: + contents: read + statuses: write + pull-requests: write + jobs: build: name: Lint Code Base @@ -11,12 +16,13 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: fetch-depth: 0 + persist-credentials: false - name: Lint Code Base - uses: super-linter/super-linter/slim@v8 + uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -24,8 +30,8 @@ jobs: SHELLCHECK_OPTS: -e SC1091 -e 2086 VALIDATE_ALL_CODEBASE: false FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/|code/web-client/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" - VALIDATE_BIOME_LINT: false VALIDATE_BIOME_FORMAT: false + VALIDATE_BIOME_LINT: false VALIDATE_PYTHON_BLACK: false VALIDATE_PYTHON_FLAKE8: false VALIDATE_PYTHON_ISORT: false