feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600
feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600jklein24 wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-typescript studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
|
@greptile review |
Greptile SummaryThis PR adds the remaining EU/Striga SCA management surface to the Grid API spec: factor enrollment (TOTP + passkey), SCA login/session, beneficiary whitelisting/de-whitelisting, and liveness-gated 2FA reset. All 13 new endpoints follow the established
Confidence Score: 5/5Purely additive OpenAPI spec change — no runtime code, no migrations; lint gate passes clean. Every new path and schema is self-contained and non-breaking. The one documentation inconsistency (ScaChallenge.id description conflicting with its use as challengeId in the trust flow) is a prose clarity issue, not a correctness issue — the confirm-request schema itself is unambiguous. No missing required fields, no broken $refs, no route conflicts. openapi/components/schemas/sca/BeneficiaryTrustStart.yaml — the reused ScaChallenge.id description warrants a follow-up prose fix.
|
| Filename | Overview |
|---|---|
| openapi/openapi.yaml | Registers 13 new SCA path refs in the master path index. All refs resolve correctly; no ordering or naming conflicts with existing paths. |
| openapi/components/schemas/sca/BeneficiaryTrustStart.yaml | New schema: whitelistedId (required) + optional scaChallenge. Reuses ScaChallenge whose id.description says "informational / need not supply back," contradicting trust-confirm's challengeId requirement. |
| openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust.yaml | Adds startBeneficiaryTrust POST endpoint; no requestBody (path params sufficient); returns BeneficiaryTrustStart (whitelistedId + optional scaChallenge). Previously-flagged doc issues around untrust ceremony addressed in-thread. |
| openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust_confirm.yaml | Adds confirmBeneficiaryTrust POST; uses shared BeneficiaryTrustConfirmRequest (whitelistedId + SCA proof). Returns BeneficiaryTrustConfirm with trusted:true. |
| openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_untrust_confirm.yaml | Adds confirmBeneficiaryUntrust POST; reuses BeneficiaryTrustConfirmRequest and BeneficiaryTrustConfirm schemas; returns trusted:false. Previously-flagged ceremony asymmetry addressed in-thread. |
| openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml | Adds startTwoFactorReset POST (201); returns TwoFactorResetStart with resetId, sumsubAccessToken, verificationLink. Clear poll-then-complete flow documented. |
| openapi/paths/customers/customers_{customerId}sca_factors_reset{resetId}.yaml | Adds getTwoFactorResetStatus GET; returns TwoFactorResetStatus with LIVENESS_PASSED terminal value documented. Previously-flagged opaque status addressed in-thread. |
| openapi/paths/customers/customers_{customerId}sca_factors_reset{resetId}_complete.yaml | Adds completeTwoFactorReset POST; no requestBody (resetId in path is sufficient); returns 204 on success, 400 if liveness not passed yet. |
| openapi/components/schemas/sca/TwoFactorResetStatus.yaml | New schema; status string with LIVENESS_PASSED terminal value described in prose. Previously-flagged missing terminal value addressed. |
| openapi/components/schemas/sca/ScaLoginStart.yaml | New schema for login-start response; factor-discriminated fields (challengeId/expiresAt for SMS_OTP, passkeyOptions for PASSKEY) are all nullable/optional, consistent with codebase pattern. |
| openapi/components/schemas/sca/TotpEnrollmentStart.yaml | New schema; returns secret, secretBase32Encoded, totpUri. Stateless round-trip of secret on confirm is intentional per previous thread discussion. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Grid API
rect rgb(200, 230, 255)
Note over Client, Grid API: TOTP Factor Enrollment
Client->>Grid API: POST /sca/factors/totp → TotpEnrollmentStart (secret, totpUri)
Client->>Grid API: POST /sca/factors/totp/confirm {secret, code} → TotpEnrollmentConfirmResponse (recoveryCodes)
end
rect rgb(200, 255, 220)
Note over Client, Grid API: Passkey Factor Enrollment
Client->>Grid API: POST /sca/factors/passkey → PasskeyEnrollmentStart (options, allowedOrigins)
Client->>Grid API: POST /sca/factors/passkey/confirm {credential, origin} → PasskeyEnrollmentConfirmResponse
Client->>Grid API: DELETE /sca/factors/passkey/{credentialId} → 204
end
rect rgb(255, 240, 200)
Note over Client, Grid API: SCA Login / 180-day Session
Client->>Grid API: POST /sca/login/start {factor} → ScaLoginStart (challengeId?, passkeyOptions?)
Client->>Grid API: POST /sca/login/complete {factor, code|passkeyAssertion} → ScaLoginComplete (status: SUCCESS)
Client->>Grid API: POST /sca/record-event {eventType} → 204
end
rect rgb(255, 220, 220)
Note over Client, Grid API: Beneficiary Trust/Untrust
Client->>Grid API: POST /external-accounts/{id}/trust → BeneficiaryTrustStart (whitelistedId, scaChallenge?)
Client->>Grid API: POST /external-accounts/{id}/trust/confirm {whitelistedId, code} → BeneficiaryTrustConfirm (trusted: true)
Client->>Grid API: POST /external-accounts/{id}/trust (again for untrust whitelistedId)
Client->>Grid API: POST /external-accounts/{id}/untrust/confirm {whitelistedId, code} → BeneficiaryTrustConfirm (trusted: false)
end
rect rgb(230, 210, 255)
Note over Client, Grid API: 2FA Reset (liveness-gated)
Client->>Grid API: POST /sca/factors/reset {factor} → TwoFactorResetStart (resetId, sumsubAccessToken?) [201]
loop Poll until LIVENESS_PASSED
Client->>Grid API: GET /sca/factors/reset/{resetId} → TwoFactorResetStatus (status)
end
Client->>Grid API: POST /sca/factors/reset/{resetId}/complete → 204
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant Grid API
rect rgb(200, 230, 255)
Note over Client, Grid API: TOTP Factor Enrollment
Client->>Grid API: POST /sca/factors/totp → TotpEnrollmentStart (secret, totpUri)
Client->>Grid API: POST /sca/factors/totp/confirm {secret, code} → TotpEnrollmentConfirmResponse (recoveryCodes)
end
rect rgb(200, 255, 220)
Note over Client, Grid API: Passkey Factor Enrollment
Client->>Grid API: POST /sca/factors/passkey → PasskeyEnrollmentStart (options, allowedOrigins)
Client->>Grid API: POST /sca/factors/passkey/confirm {credential, origin} → PasskeyEnrollmentConfirmResponse
Client->>Grid API: DELETE /sca/factors/passkey/{credentialId} → 204
end
rect rgb(255, 240, 200)
Note over Client, Grid API: SCA Login / 180-day Session
Client->>Grid API: POST /sca/login/start {factor} → ScaLoginStart (challengeId?, passkeyOptions?)
Client->>Grid API: POST /sca/login/complete {factor, code|passkeyAssertion} → ScaLoginComplete (status: SUCCESS)
Client->>Grid API: POST /sca/record-event {eventType} → 204
end
rect rgb(255, 220, 220)
Note over Client, Grid API: Beneficiary Trust/Untrust
Client->>Grid API: POST /external-accounts/{id}/trust → BeneficiaryTrustStart (whitelistedId, scaChallenge?)
Client->>Grid API: POST /external-accounts/{id}/trust/confirm {whitelistedId, code} → BeneficiaryTrustConfirm (trusted: true)
Client->>Grid API: POST /external-accounts/{id}/trust (again for untrust whitelistedId)
Client->>Grid API: POST /external-accounts/{id}/untrust/confirm {whitelistedId, code} → BeneficiaryTrustConfirm (trusted: false)
end
rect rgb(230, 210, 255)
Note over Client, Grid API: 2FA Reset (liveness-gated)
Client->>Grid API: POST /sca/factors/reset {factor} → TwoFactorResetStart (resetId, sumsubAccessToken?) [201]
loop Poll until LIVENESS_PASSED
Client->>Grid API: GET /sca/factors/reset/{resetId} → TwoFactorResetStatus (status)
end
Client->>Grid API: POST /sca/factors/reset/{resetId}/complete → 204
end
Reviews (6): Last reviewed commit: "docs(sca): document terminal status valu..." | Re-trigger Greptile
ce99ce4 to
79c4671
Compare
9c598bf to
8b2b873
Compare
|
@greptile review |
1 similar comment
|
@greptile review |
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
1bca871 to
9689a50
Compare
9689a50 to
3267da9
Compare
|
@faraday review this |
|
📌 Bolt Status 2026-07-09 06:52:06 UTC — ⚡ Agent |
3267da9 to
513676e
Compare
…mt schemas Address faraday review on #600: - Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest and ScaLoginCompleteRequest so a proof-less body fails schema validation, not just at runtime (matching lint-ignore precedent for required-on-shared-props). - Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset start path) to keep the surface provider-neutral; the sparkcore response key is renamed in lockstep. - Document whitelistedId provenance (scoped to this account's own trust start, not reusable across accounts or after expiry). - Clarify eventType is a provider-defined closed vocabulary, not free-form input. Co-Authored-By: Claude <noreply@anthropic.com>
Stacks on the per-transaction SCA spec (#558) and adds the remaining Strong Customer Authentication surfaces an EU (Striga) partner needs, mirroring the sparkcore handlers' exact routes + JSON shapes. All endpoints are EU-only (409 for providers that don't require SCA). - Factor enrollment: TOTP start/confirm, passkey register start/confirm, list factors, delete passkey. - SCA login / 180-day session: login start/complete, plus record-event. - Beneficiary trust: trust start (issues the SCA challenge + whitelistedId), trust/confirm and untrust/confirm (the whitelisting exemption that lets recurring payees skip per-transaction SCA). - 2FA reset: initiate (201) -> poll status -> complete. Reuses ScaFactor/ScaChallenge/ScaAuthorization from #558. Confirm/login bodies carry the code|passkeyAssertion+origin proof; beneficiary confirm threads the whitelistedId back so confirm never re-whitelists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Name the provider-reported terminal sentinels (LIVENESS_PASSED for 2FA reset, SUCCESS for login) in the status field descriptions so consumers can write polling/branching logic without out-of-band knowledge, while keeping the field a verbatim provider passthrough rather than a locked enum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mt schemas Address faraday review on #600: - Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest and ScaLoginCompleteRequest so a proof-less body fails schema validation, not just at runtime (matching lint-ignore precedent for required-on-shared-props). - Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset start path) to keep the surface provider-neutral; the sparkcore response key is renamed in lockstep. - Document whitelistedId provenance (scoped to this account's own trust start, not reusable across accounts or after expiry). - Clarify eventType is a provider-defined closed vocabulary, not free-form input. Co-Authored-By: Claude <noreply@anthropic.com>
763b88b to
af68ac7
Compare

Summary
The SCA management surface for EU (Striga) customers — the customer-level setup, session, and recovery flows that sit alongside per-payment authorization (not inside it). This single PR collapses the original design's PR3–PR7 (enrollment, passkey, login/session, beneficiary trust, 2FA reset). Stacks on #558, which defines the per-transaction authorization core and the shared schemas this PR reuses. All endpoints are EU-only —
409for providers that don't require SCA (the invisibility guarantee) and mirror the sparkcore handlers' exact routes + JSON shapes. Surfaced after an audit of #558 found these existed only as sparkcore temp types, not in the spec.Covered by this PR
POST /customers/{id}/sca/factors/totp(+/confirm),.../passkey(+/confirm),GET .../sca/factors,DELETE .../passkey/{credentialId}.POST .../sca/login/start(+/complete);POST .../sca/record-event.POST .../external-accounts/{extId}/trust(issues the challenge +whitelistedId),.../trust/confirm,.../untrust/confirm— the whitelisting exemption that lets recurring payees skip per-transaction SCA.POST .../sca/factors/reset(201) →GET .../reset/{resetId}(poll toLIVENESS_PASSED) →POST .../reset/{resetId}/complete.Reuses
ScaFactor/ScaChallenge/ScaAuthorizationfrom #558; confirm/login bodies carry thecode | passkeyAssertion + originproof, and beneficiary confirm threadswhitelistedIdback so confirm never re-whitelists (no duplicate SMS).TwoFactorResetStatus/ScaLoginCompletedocument their provider-passthrough terminal values (LIVENESS_PASSED/SUCCESS).NOT in this PR
authorize/resendendpoints, and inlinescaAuthorization/scaFactor— that's feat(sca): add Strong Customer Authentication surface #558.Known gaps / follow-ups (tracked in the design doc addendum)
confirmbut nostart, and there is no trust-list read — a caller who loseswhitelistedId(or needs the untrust OTP re-sent) is stuck (addendum F4). The trusted-beneficiary exemption benefit (a trusted payee actually skipping per-tx SCA) is also not yet realized in the backend.statusdocumentsLIVENESS_PASSED/PENDINGbut no terminal failure value — infinite-poll risk (F10).409codes (SCA_NOT_REQUIREDvsNO_PENDING_SCA_CHALLENGE) are not yet modeled (F9).Validation
npm run lint:openapi(redocly + spectral, fail-on-error) passes clean; additions are additive / non-breaking. (The OpenAPI build / breaking-changes / lint CI checks run on the base=mainPR #558; they do not run here because this PR targetsfeat/striga-sca.)Notes
to_dict()since Striga's docs only cover the start side — flagged for sandbox verification.Stacking
main→ #558 (per-transaction) → #600 (this PR). Merges after #558; depends on the schemas #558 introduces.🤖 Generated with Claude Code