fix(payment): gate PaymentAccountHolder.methodTokens in the field resolver (R2-G10)

Merged
EV
11d ago
+80-8

Item: R2-G10 (handover HANDOVER-2026-10-17-gauzy-px-improvements.md), security.

Problem

PaymentAccountHolder.methodTokens carried only @Permissions(PAYMENT_METHOD_TOKENS_VIEW). The GraphQL module (packages/core/src/lib/graphql/graphql-helper.ts) sets no fieldResolverEnhancers, so Nest runs guards for root fields and never for a @ResolveField. That metadata was never read. Any caller who could read an account (PAYMENT_ACCOUNT_HOLDERS_VIEW) got its saved instruments too.
REST (withInstrumentsGranted in payment-account-holder.controller.ts) leaves the member out unless the caller holds PAYMENT_METHOD_TOKENS_VIEW.

Fix

The check now runs at the field resolver itself. The global config is not changed: turning on fieldResolverEnhancers would put every guard and interceptor on every field of every type, the idempotency and organization interceptors included.

  • methodTokens reads through FieldVisibility.guard(PAYMENT_METHOD_TOKENS_VIEW, …). This is the platform's gate for a withheld GraphQL field, the same one PaymentMethodToken.token uses, plus @VisibleWith.
  • Without the grant, the nullable field resolves to null with a typed 403 PERMISSION_DENIED (details: { resource: 'paymentAccountHolder', field: 'methodTokens', requiredPermission }), and no instrument is read. The account itself still resolves.
  • FieldVisibility is an @Optional() defaulted 4th constructor argument, the same pattern as PaymentMethodTokenResolver.

Tests

payment-instrument.resolver.spec.ts has 3 new cases: the declaration, the denial (no read reaches the kernel) and the grant. 2 are red on develop and green here (20/20). The existing list test now grants PAYMENT_METHOD_TOKENS_VIEW, because it reads the field. It was run locally as a single spec. Full suites run at the end of the lane run.

Note

The other payment @ResolveField with a @Permissions line, Refund.lines, is reached through the refund queries and the refund mutation payloads. Their REST twins return the lines as well, so that field does not expose more than REST does, and it is not changed here.

🤖 Generated with Claude Code