Skip to content

feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600

Draft
jklein24 wants to merge 3 commits into
feat/striga-scafrom
feat/striga-sca-management
Draft

feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600
jklein24 wants to merge 3 commits into
feat/striga-scafrom
feat/striga-sca-management

Conversation

@jklein24

@jklein24 jklein24 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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 — 409 for 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

  • Factor enrollment (design PR3/PR4): POST /customers/{id}/sca/factors/totp (+/confirm), .../passkey (+/confirm), GET .../sca/factors, DELETE .../passkey/{credentialId}.
  • Login / 180-day session (PR5): POST .../sca/login/start (+/complete); POST .../sca/record-event.
  • Beneficiary trust (PR6): POST .../external-accounts/{extId}/trust (issues the challenge + whitelistedId), .../trust/confirm, .../untrust/confirm — the whitelisting exemption that lets recurring payees skip per-transaction SCA.
  • 2FA reset / recovery (PR7): POST .../sca/factors/reset (201) → GET .../reset/{resetId} (poll to LIVENESS_PASSED) → POST .../reset/{resetId}/complete.

Reuses ScaFactor / ScaChallenge / ScaAuthorization from #558; confirm/login bodies carry the code | passkeyAssertion + origin proof, and beneficiary confirm threads whitelistedId back so confirm never re-whitelists (no duplicate SMS). TwoFactorResetStatus / ScaLoginComplete document their provider-passthrough terminal values (LIVENESS_PASSED / SUCCESS).

NOT in this PR

  • Per-transaction authorization — the challenge on a debit, the authorize / resend endpoints, and inline scaAuthorization / scaFactor — that's feat(sca): add Strong Customer Authentication surface #558.
  • The sparkcore backend for these endpoints (separate stack), and client-side WebAuthn ceremony logic (Grid relays opaque passkey blobs; it performs no crypto).

Known gaps / follow-ups (tracked in the design doc addendum)

  • Untrust has confirm but no start, and there is no trust-list read — a caller who loses whitelistedId (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.
  • Reset status documents LIVENESS_PASSED / PENDING but no terminal failure value — infinite-poll risk (F10).
  • Distinct SCA 409 codes (SCA_NOT_REQUIRED vs NO_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=main PR #558; they do not run here because this PR targets feat/striga-sca.)

Notes

  • Field shapes mirror the sparkcore implementation (the source of truth until the SDK regenerates). A few Striga response bodies (login-complete, reset-status/complete, passkey list/delete) are typed from the sparkcore 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

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 10, 2026 7:52am
grid-wallet-demo Ignored Ignored Preview Jul 10, 2026 7:52am

Request Review

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

go

chore(internal): regenerate SDK with no functional changes

kotlin

chore(internal): regenerate SDK with no functional changes

openapi

feat(api): add SCA factor management, login, 2FA reset, beneficiary trust endpoints

php

chore(internal): regenerate SDK with no functional changes

python

chore(internal): regenerate SDK with no functional changes

ruby

chore(internal): regenerate SDK with no functional changes

typescript

chore(internal): regenerate SDK with no functional changes

Edit this comment to update them. They will appear in their respective SDK's changelogs.

grid-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️

New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-ruby studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@97d951a0948abc2e79d0fde0b80ab13529be260d
New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-kotlin studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/87d5b34125f8e13f3db9268ab6a1e05e5d40fe58/dist.tar.gz
New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/9cf884a8e12a58912577368ae5a2791542f38e89/grid-0.0.1-py3-none-any.whl
New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-php studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅lint ✅test ✅

New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`
grid-cli studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ⏭️lint ⏭️test ❗

New diagnostics (15 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/sca/factors`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/totp/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/passkey/confirm`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /customers/{customerId}/sca/factors/passkey/{credentialId}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/start`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/login/complete`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/record-event`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/sca/factors/reset`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-07-10 07:58:07 UTC

@jklein24

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

    \m/ GRID RIPS THE SPEC \m/
    ########################################
    ##  ____  ____  __    ____  ____  ____  ##
    ## / ___)(  _ \(  )  (  _ \/ ___)(  _ \ ##
    ## \___ \ )___/ )(__  )   /\___ \ )(_) )##
    ## (____/(__)  (____)(_\_)(____/(____/  ##
    ########################################
         SCA MANAGEMENT — ALL FACTORS ENROLLED

This 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 409-for-non-SCA-providers invisibility contract and reuse the ScaFactor/ScaChallenge/ScaAuthorization types from #558.

  • Factor enrollment: Full TOTP (startconfirm) and passkey (startconfirmdelete) lifecycle, plus GET /sca/factors list endpoint. TOTP secret round-trip and passkey credential/passkeyAssertion naming distinction are both correct.
  • SCA login + session: start/complete pair with factor-discriminated response fields (nullable pattern consistent with existing codebase); record-event pass-through for provider risk signals.
  • Beneficiary trust/untrust: POST /trusttrust/confirm and untrust/confirm sharing BeneficiaryTrustConfirmRequest; whitelistedId threading avoids duplicate SMS. completeTwoFactorReset intentionally carries no request body — the resetId path param is the only required input after liveness passes.

Confidence Score: 5/5

Purely 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.

Important Files Changed

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
Loading
%%{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
Loading

Reviews (6): Last reviewed commit: "docs(sca): document terminal status valu..." | Re-trigger Greptile

Comment thread openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
Comment thread openapi/components/schemas/sca/TwoFactorResetStatus.yaml
Comment thread openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml
Comment thread openapi/components/schemas/sca/TwoFactorResetStatus.yaml
@jklein24 jklein24 force-pushed the feat/striga-sca-management branch from 9c598bf to 8b2b873 Compare June 17, 2026 22:35
@jklein24

Copy link
Copy Markdown
Contributor Author

@greptile review

1 similar comment
@jklein24

Copy link
Copy Markdown
Contributor Author

@greptile review

Copy link
Copy Markdown
Contributor Author

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.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@jklein24 jklein24 force-pushed the feat/striga-sca-management branch from 1bca871 to 9689a50 Compare July 8, 2026 20:57
@jklein24 jklein24 force-pushed the feat/striga-sca-management branch from 9689a50 to 3267da9 Compare July 8, 2026 23:57
@lightsparkdev lightsparkdev deleted a comment from greptile-apps Bot Jul 9, 2026
@jklein24 jklein24 marked this pull request as draft July 9, 2026 06:50
@jklein24

jklein24 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@faraday review this

@ls-bolt

ls-bolt Bot commented Jul 9, 2026

Copy link
Copy Markdown

📌 Bolt Status

2026-07-09 06:52:06 UTC — ⚡ Agent bright-palladium(#1) started


Feedback

Comment thread openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
Comment thread openapi/components/schemas/sca/RecordSecurityEventRequest.yaml
Comment thread openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
Comment thread openapi/components/schemas/sca/TwoFactorResetStart.yaml Outdated
@jklein24 jklein24 force-pushed the feat/striga-sca-management branch from 3267da9 to 513676e Compare July 9, 2026 07:20
jklein24 added a commit that referenced this pull request Jul 9, 2026
…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>
jklein24 and others added 3 commits July 10, 2026 00:52
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>
@jklein24 jklein24 force-pushed the feat/striga-sca-management branch from 763b88b to af68ac7 Compare July 10, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant