Issue #3606530: Branch-scoped interaction links for parallel waits
Adds branch-scoped interaction links so several people can each validate their own parallel branch.
Issue: https://www.drupal.org/project/orchestra/issues/3606530 Builds on the token model from #3605995 / MR !223 (merged) (now in 1.x).
Problem
The external interaction capability was instance-scoped only: one link drove whatever step the instance was parked on. That fits a sequential flow, or a single wait offered to several people (first to act wins). It did not fit an AND-split where several branches each park on their own interactive node and a distinct validation link is emailed per branch: every link collapsed onto the lowest-id branch, so only one recipient could act.
Change
The capability token now optionally names a single parked token, so a link can target one branch.
- InteractionToken issues and opens both shapes: "instance.expiry.signature" (instance-scoped) and "instance.token.expiry.signature" (branch-scoped), the token id bound into the HMAC. open() returns an InteractionGrant (instance, optional token); isValid() still gates on the instance.
- InteractionUrls gains stepUrlForToken() / signalUrlForToken(); the dispatcher and the signal confirm form resolve a branch-scoped grant to that exact token (InteractionResolver::parkedTokenById), and an instance-scoped one to the parked step as before. A branch link to an already-acted token shows the neutral message.
- InteractionResolver::parkedInteractiveTokens() enumerates every branch awaiting a visitor, so an integrator (or a notifier) can mint and send a link per recipient.
- The webform binding is scoped to the specific parked token, so a submission resumes exactly the branch its form was opened for, even when other branches are parked or the instance moves on before submit.
Backward compatible: instance-scoped links and the single-element webform binding are unchanged; this only adds the branch-scoped variant.
Tests
- InteractionTokenTest: issue/open for both shapes, expiry, and tamper rejection (instance, branch, expiry).
- InteractionAccessTest: the dispatcher drives each parallel branch by its own link and shows the neutral message once a branch has acted.
- OrchestraResumeHandlerTest: a branch-scoped binding resumes only its own branch.
Docs: a "Link scope" section in the external-interaction page.
Verified locally: phpcs and phpstan clean; orchestra_interaction + orchestra_interaction_webform kernel suites green (19 tests).