feat(sca): add Strong Customer Authentication surface#558
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
@greptile review |
✱ 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-cli studio · code · diff
✅ grid-php studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-typescript studio · code · diff
✅ grid-python studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryAdds a complete Strong Customer Authentication (SCA) surface to the Grid API for EU/Striga customers, introducing new schemas (
Confidence Score: 5/5All changes are purely additive API spec additions with no modifications to existing endpoints or schemas; non-SCA callers are completely unaffected. The SCA surface is well-structured and isolated: new schemas, four new path files, new enum values, and optional fields on existing requests. The new endpoints are correctly documented with appropriate response codes. The only open items are documentation nits in ScaChallenge (minItems on availableFactors, misleading "Defaults to" on a required field) and the previously-flagged absence of oneOf enforcement in ScaAuthorization — none of these affect the correctness of existing callers or introduce behavioral regressions. openapi/components/schemas/sca/ScaAuthorization.yaml — the prose-only "exactly one of" and "origin required" constraints have no schema-level enforcement (previously flagged); openapi/components/schemas/sca/ScaChallenge.yaml — minor documentation inconsistencies noted above.
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/sca/ScaAuthorization.yaml | New proof payload schema; prose says "exactly one of code or passkeyAssertion" and "origin required with passkeyAssertion" but no oneOf/anyOf/if-then enforces these constraints — previously flagged, not yet addressed. |
| openapi/components/schemas/sca/ScaChallenge.yaml | New challenge object schema; required fields are well-defined, but availableFactors lacks minItems:1 and the factor description's "Defaults to SMS_OTP" conflicts with it being required. |
| openapi/components/schemas/sca/ScaFactor.yaml | Clean enum definition with clear per-factor descriptions; TOTP dynamic-linking caveat is correctly documented. |
| openapi/paths/quotes/quotes_{quoteId}_authorize.yaml | Well-structured authorize endpoint for realtime-funding quotes; 400/401/404/409/500 responses all present, operationId is unique. |
| openapi/paths/transactions/transactions_{transactionId}_authorize.yaml | Well-structured authorize endpoint for transactions; mirrors quote authorize correctly with TransactionOneOf as response schema. |
| openapi/paths/quotes/quotes_{quoteId}_authorize_resend.yaml | Resend OTP endpoint for quote SCA challenges; no request body so no 400 needed; 204/401/404/409/500 coverage is appropriate. |
| openapi/paths/transactions/transactions_{transactionId}_authorize_resend.yaml | Resend OTP endpoint for transaction SCA challenges; mirrors quote resend correctly; 204/401/404/409/500 coverage is appropriate. |
| openapi/components/schemas/quotes/Quote.yaml | Adds PENDING_AUTHORIZATION to status enum and readOnly scaChallenge field; description clearly scopes the new state to SCA providers only. |
| openapi/components/schemas/transactions/TransactionStatus.yaml | Adds PENDING_AUTHORIZATION enum value with accurate description; change is additive and non-breaking. |
| openapi/paths/quotes/quotes_{quoteId}_execute.yaml | Adds optional ExecuteQuoteRequest body; 200 description now clearly distinguishes upfront inline auth from the post-PENDING_AUTHORIZATION authorize endpoint path. |
| openapi/paths/transfers/transfer_out.yaml | 201 description updated to explain SCA inline authorization flow; change is additive and consistent with execute endpoint documentation. |
| openapi/components/schemas/quotes/ExecuteQuoteRequest.yaml | New optional request body schema for execute; single optional scaAuthorization field, correctly marked not required. |
| openapi/components/schemas/transfers/TransferOutRequest.yaml | Adds optional scaAuthorization field to transfer-out request; documentation matches the execute inline auth pattern. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant API as Grid API
participant Provider as SCA Provider
Note over Client,Provider: Embedded-wallet quote (execute path)
Client->>API: "POST /quotes/{quoteId}/execute"
API->>Provider: Initiate transfer
Provider-->>API: SCA required
API-->>Client: "200 Quote{status: PENDING_AUTHORIZATION, scaChallenge}"
Client->>Client: Customer enters SMS/TOTP code
Client->>API: "POST /transactions/{transactionId}/authorize {code}"
API->>Provider: Submit SCA proof
Provider-->>API: Authorized
API-->>Client: "200 Transaction{status: PROCESSING}"
Note over Client,Provider: Realtime-funding quote (create-quote path)
Client->>API: POST /quotes
API->>Provider: Create quote
Provider-->>API: SCA required (realtime funding)
API-->>Client: "202 Quote{status: PENDING_AUTHORIZATION, scaChallenge, paymentInstructions: null}"
Client->>Client: Customer enters code
Client->>API: "POST /quotes/{quoteId}/authorize {code}"
API->>Provider: Submit SCA proof
Provider-->>API: Authorized
API-->>Client: "200 Quote{status: PENDING, paymentInstructions: populated}"
Note over Client,Provider: Inline one-shot (transfer-out / execute)
Client->>Client: Customer already has code
Client->>API: "POST /transfer-out {scaAuthorization: {code}}"
API->>Provider: Transfer + SCA proof
Provider-->>API: Authorized
API-->>Client: "201 Transaction{status: PROCESSING}"
Note over Client,Provider: Resend OTP
Client->>API: "POST /transactions/{transactionId}/authorize/resend"
API->>Provider: Resend SMS
Provider-->>Client: SMS delivered
API-->>Client: 204
%%{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 API as Grid API
participant Provider as SCA Provider
Note over Client,Provider: Embedded-wallet quote (execute path)
Client->>API: "POST /quotes/{quoteId}/execute"
API->>Provider: Initiate transfer
Provider-->>API: SCA required
API-->>Client: "200 Quote{status: PENDING_AUTHORIZATION, scaChallenge}"
Client->>Client: Customer enters SMS/TOTP code
Client->>API: "POST /transactions/{transactionId}/authorize {code}"
API->>Provider: Submit SCA proof
Provider-->>API: Authorized
API-->>Client: "200 Transaction{status: PROCESSING}"
Note over Client,Provider: Realtime-funding quote (create-quote path)
Client->>API: POST /quotes
API->>Provider: Create quote
Provider-->>API: SCA required (realtime funding)
API-->>Client: "202 Quote{status: PENDING_AUTHORIZATION, scaChallenge, paymentInstructions: null}"
Client->>Client: Customer enters code
Client->>API: "POST /quotes/{quoteId}/authorize {code}"
API->>Provider: Submit SCA proof
Provider-->>API: Authorized
API-->>Client: "200 Quote{status: PENDING, paymentInstructions: populated}"
Note over Client,Provider: Inline one-shot (transfer-out / execute)
Client->>Client: Customer already has code
Client->>API: "POST /transfer-out {scaAuthorization: {code}}"
API->>Provider: Transfer + SCA proof
Provider-->>API: Authorized
API-->>Client: "201 Transaction{status: PROCESSING}"
Note over Client,Provider: Resend OTP
Client->>API: "POST /transactions/{transactionId}/authorize/resend"
API->>Provider: Resend SMS
Provider-->>Client: SMS delivered
API-->>Client: 204
Reviews (7): Last reviewed commit: "feat(sca): complete the per-transaction ..." | Re-trigger Greptile
|
@greptile review |
|
@greptile review |
1 similar comment
|
@greptile review |
|
@greptile review |
ce99ce4 to
79c4671
Compare
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>
|
@greptile review |
1 similar comment
|
@greptile review |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
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>
Adds the SCA surface required for EU (Striga) customers while keeping it
invisible to every other caller:
- New sca/ schemas: ScaFactor, ScaChallenge, ScaAuthorization.
- readOnly scaChallenge on Quote and Transaction, present only while status
is PENDING_AUTHORIZATION (new enum value on TransactionStatus and
Quote.status); omitted entirely for providers that don't require SCA.
- Resource-scoped authorize endpoints: POST /quotes/{quoteId}/authorize
(returns Quote) and POST /transactions/{transactionId}/authorize (returns
Transaction), plus an optional inline scaAuthorization on execute and
transfer-out for one-shot completion.
- Realtime-funding create-quote returns 202 and withholds paymentInstructions
until the challenge is authorized.
Design: webdev docs/plans/2026-06-06-striga-sca-design.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Greptile review: the authorize endpoints are resource-scoped, so the server resolves the active challenge from the quote/transaction context. The id is informational, not a field the client passes back in ScaAuthorization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… only
Address Greptile review: after a quote returns PENDING_AUTHORIZATION, the only
resolution is POST /transactions/{transactionId}/authorize; re-calling execute
409s. scaAuthorization on ExecuteQuoteRequest applies only to the initial call
(to avoid the pending state entirely).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Greptile review: mirror the execute/create-quote treatment so a
transfer-out client has a spec-level signal that the returned transaction may
be PENDING_AUTHORIZATION and require POST /transactions/{transactionId}/authorize.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… resend)
Addresses an independent audit of the SCA surface against Striga's API:
- ScaAuthorization gains `origin` — Striga's wallets/transaction/confirm
requires the WebAuthn origin alongside passkeyAssertion, and the assertion is
origin-bound. Without it a passkey-authorized debit could not be confirmed.
- ScaChallenge surfaces `passkeyAllowedOrigins` (and documents that the relying
party id lives in passkeyAssertionOptions) so the caller knows which origin to
echo back.
- New resend endpoints: POST /quotes/{quoteId}/authorize/resend and
/transactions/{transactionId}/authorize/resend, mapping to Striga's
resend-otp-for-transaction (SMS only; passkey 409). An SMS code that lapses
no longer forces abandoning the challenge.
- Cross-reference the realtime-funding quote authorize path and the resend
endpoint from the execute 200 description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add PENDING_AUTHORIZATION to OutgoingTransactionStatus. SCA-gated debits are outgoing transactions, whose status is overridden by this enum; without the value, generated clients reject the very responses the feature produces. - Add the optional top-level scaFactor field to ExecuteQuoteRequest and TransferOutRequest. The backend already reads it to pick SMS_OTP (default) vs PASSKEY for the per-transaction challenge; it was undiscoverable in the spec. - Clarify where the passkey origin comes from for a per-transaction challenge: such challenges carry passkeyAssertionOptions but omit passkeyAllowedOrigins, so ScaAuthorization.origin and ScaChallenge.passkeyAllowedOrigins now explain the per-transaction case instead of pointing at an absent field. Co-Authored-By: Claude Opus 4.8 <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>
|
@faraday review this |
|
📌 Bolt Status 2026-07-09 06:51:24 UTC — ⚡ Agent |
| exchangeRate: 0.92 | ||
| feesIncluded: 10 | ||
| transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 | ||
| '202': |
There was a problem hiding this comment.
Adding a bare 202 to this existing operation is going to silently break already-deployed SDK clients. I checked the generated Python client - _response_types_map only has an entry for 201 (no 202, no 2XX wildcard), so response_deserialize falls through with response_type=None and create_quote() returns None even though a quote was actually created. That's silent data loss on a money-creation call, not something the caller can catch. Per grid.md §8 a breaking change like this to an existing endpoint needs a new dated blueprint rather than extending this one in place.
There was a problem hiding this comment.
It's fine. I'm not worried about that generated python client. We'll update it before this is actually returned.
| type: string | ||
| enum: | ||
| - PENDING | ||
| - PENDING_AUTHORIZATION |
There was a problem hiding this comment.
We're adding PENDING_AUTHORIZATION to a closed-vocabulary enum on endpoints that are already shipped (same issue applies to TransactionStatus and OutgoingTransactionStatus). The generated SDKs model these as closed enums with no unknown-value fallback - e.g. the Python client's status_validate_enum raises ValueError on anything not in the list. The moment sparkcore emits this status for an SCA-required customer, any integrator still on an older SDK gets a hard validation error on a previously-working GET /transactions/{id} or GET /quotes/{id} call, and this will likely repeat across every generated language (go/kotlin/ts/rust/flutter). This needs version gating per grid.md §8 rather than extending the existing enum in place.
There was a problem hiding this comment.
It's fine. Our real SDKs handle new values gracefully.
…e-target clarity Address faraday review on #558: - Add scaFactor to QuoteRequest so a challenge issued at quote creation (realtime funding) can be SMS_OTP or PASSKEY, matching execute/transfer-out — it was previously unspecified, implying SMS-only. - Reword the execute 200 description, which contradicted itself (claimed the transfer was initiated while also covering the PENDING_AUTHORIZATION case); it now splits the two outcomes and states which authorize endpoint to use. - Document a 429 (RATE_LIMITED) on the authorize and resend endpoints — brute -force protection on the OTP and anti-SMS-spam on resend, mirroring the existing auth/credentials OTP endpoints. - Clarify that resend reuses the existing challenge and does not extend expiresAt (post-expiry recovery tracked as design follow-up F8). - Fix nit: sandbox code wording SMS/TOTP -> SMS (TOTP is invalid per-transaction). 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>

Summary
The per-transaction SCA authorization surface — everything needed to gate and authorize an EU (Striga) money-movement debit with an SMS OTP or passkey. This is the compliance-critical core of the Striga SCA work and the base of the grid-api SCA stack; the management surface (#600) stacks on top, and the sparkcore backend lands separately. Designed to stay invisible to every non-SCA (non-EU) caller.
Covered by this PR
Shared SCA vocabulary (defined here, reused by the whole stack):
ScaFactor(SMS_OTP/TOTP/PASSKEY)ScaChallenge(id,expiresAt,factor,availableFactors,passkeyAssertionOptions,passkeyAllowedOrigins)ScaAuthorization(exactly one ofcode/passkeyAssertion;originrequired with a passkey assertion)Per-transaction authorization:
PENDING_AUTHORIZATIONstatus + readOnlyscaChallengeonQuoteandTransaction(incl. theOutgoingTransactionStatusoverride) — omitted entirely for non-SCA providers.POST /quotes/{id}/authorize→Quote,POST /transactions/{id}/authorize→Transaction(both409for non-SCA providers).POST /quotes/{id}/authorize/resend,POST /transactions/{id}/authorize/resend.scaAuthorizationonexecute/transfer-out(satisfy the challenge in the same call).scaFactoronexecute/transfer-outto pickSMS_OTP(default) orPASSKEYfor the per-transaction challenge.NOT in this PR — see #600 (stacked on top)
The SCA management surface (customer-level setup/recovery, not per-payment authorization) lives in #600 and reuses the schemas defined here:
sca/login/start|completeexternal-accounts/{id}/trust,.../untrust/confirmsca/record-eventAlso out of scope: the sparkcore backend (separate stack) and any client-side WebAuthn ceremony logic (Grid relays opaque passkey blobs; it performs no crypto).
Known caveat
202+ withheld-paymentInstructionspath onPOST /quotesis specified here but not yet reachable — the backend defers it (Striga has no Grid realtime-funding path today; see design doc addendum item F6 / open question 2). It's documented so the contract is stable; treat it as inert until that flow ships.Stacking
main→ #558 (this PR) → #600 (management). #558 is self-contained and independently mergeable — SMS-OTP authorization needs no enrollment or login — so it can merge tomainon its own. #600 depends on theScaFactor/ScaChallenge/ScaAuthorizationschemas introduced here.Invisibility guarantee
scaChallenge,PENDING_AUTHORIZATION, the authorize/resend endpoints, andscaFactorengage only for customers whose provider requires SCA. Non-EU callers see no new fields, no new statuses, and no behavioral change.Validation
npm run lint:openapi(redocly + spectral) passes; additions are additive / non-breaking ("Detect breaking changes" CI green).Design
webdev
docs/plans/2026-06-06-striga-sca-design.md🤖 Generated with Claude Code