Skip to content

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

Merged
jklein24 merged 10 commits into
mainfrom
feat/striga-sca-management
Jul 17, 2026
Merged

feat(sca): SCA management surface — enrollment, login/session, beneficiary trust, 2FA reset#600
jklein24 merged 10 commits into
mainfrom
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 17, 2026 6:07pm
grid-wallet-demo Ignored Ignored Preview Jul 17, 2026 6:07pm

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
⚠️ grid-openapi studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

grid-ruby studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

⚠️ grid-go studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@1fe4485bac5384b80a875cf3663b8897ecde9a45
⚠️ grid-kotlin studio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

⚠️ grid-python studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/b17c66ee377f16f682424d9b3fa060201e16c7b4/grid-0.0.1-py3-none-any.whl
grid-typescript studio · conflict

Your SDK build had at least one note diagnostic.

grid-php studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅lint ✅test ✅

⚠️ grid-cli studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗


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-17 23:15:33 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/ ⚡ RIFF OF THE SPEC DESCENDS ⚡ \m/
    |===========================================|
    |  S  C  A     M  A  N  A  G  E  M  E  N  T |
    |  ~~ THRASH THE ENROLLMENT FLOWS ~~        |
    |===========================================|
              \m/            \m/

This PR adds the EU SCA management surface — factor enrollment (TOTP + passkey), the 180-day login/session flow, beneficiary trust/untrust, and 2FA liveness-gated reset — as 13 new OpenAPI paths and 22 new schemas, completing design PR3–PR7 that stacked on the per-transaction core in #558.

  • Factor enrollment & management: POST .../sca/factors/totp (+/confirm), .../passkey (+/confirm), GET .../sca/factors, DELETE .../passkey/{credentialId} — all returning 409 for non-SCA regions and mirroring sparkcore route shapes.
  • Login, security events, and 2FA reset: POST .../sca/login/start (+/complete), POST .../sca/record-event, and the POST .../sca/factors/reset → poll → /complete liveness flow with documented LIVENESS_PASSED sentinel after prior review iteration.
  • Beneficiary trust/untrust: POST .../trustPOST .../trust/confirm and POST .../untrust/confirm with a shared BeneficiaryTrustConfirmRequest; the whitelistedId field the PR description says is issued by the start and threaded on confirm is absent from both schemas, and anyOf in the confirm request (and ScaLoginCompleteRequest) should be oneOf to enforce mutual exclusion between OTP and passkey proof types.

Confidence Score: 4/5

Mergeable after resolving two spec gaps: the missing whitelistedId field in the trust start/confirm schemas and the anyOf-vs-oneOf discriminator ambiguity.

The trust-start response schema omits the whitelistedId the PR description says is issued and threaded back on confirm — if the sparkcore backend requires it, every trust-confirm call will fail silently. Separately, anyOf in both confirm-request schemas permits a caller to submit both an OTP code and a passkey assertion simultaneously, leaving the server to receive an ambiguous proof payload with no spec guidance on which to use.

BeneficiaryTrustStart.yaml, BeneficiaryTrustConfirmRequest.yaml, and ScaLoginCompleteRequest.yaml need attention before merge.

Important Files Changed

Filename Overview
openapi/components/schemas/sca/BeneficiaryTrustStart.yaml Trust-start response schema is missing the whitelistedId field the PR description says it returns; the confirm request is also missing it as an input.
openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml Uses anyOf instead of oneOf for the OTP/passkey proof discriminator, allowing both proof types to be submitted in the same request; same issue in BeneficiaryTrustConfirmRequest.
openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml Same anyOf-vs-oneOf issue as ScaLoginCompleteRequest; also missing whitelistedId that the PR description says must be threaded back on confirm.
openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml 2FA reset start endpoint correctly returns 201 and threads resetId through to status/complete; no issues found.
openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust.yaml Trust-start endpoint has no request body (correctly inferred from path) but returns a BeneficiaryTrustStart that omits the whitelistedId the PR description says is issued here.
openapi/paths/customers/customers_{customerId}external-accounts{externalAccountId}_trust_confirm.yaml Trust-confirm endpoint structure is clean; underlying BeneficiaryTrustConfirmRequest schema has the anyOf/whitelistedId issues flagged separately.
openapi/openapi.yaml All 13 new path refs wired correctly; ordering and grouping is consistent with existing paths.
.redocly.lint-ignore.yaml Suppressions added for anyOf branches referencing parent-level properties; comment explains the valid JSON Schema rationale.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Caller
    participant G as Grid API
    participant S as Striga (SCA Provider)

    Note over C,S: Factor Enrollment — TOTP
    C->>G: "POST /customers/{id}/sca/factors/totp"
    G->>S: start TOTP enrollment
    S-->>G: secret + totpUri
    G-->>C: "TotpEnrollmentStart {secret, secretBase32Encoded, totpUri}"
    C->>G: "POST /sca/factors/totp/confirm {secret, code}"
    G->>S: confirm TOTP
    S-->>G: recovery codes
    G-->>C: "TotpEnrollmentConfirmResponse {recoveryCodes}"

    Note over C,S: SCA Login — SMS_OTP
    C->>G: "POST /customers/{id}/sca/login/start {factor: SMS_OTP}"
    G->>S: dispatch OTP
    S-->>G: challengeId + expiresAt
    G-->>C: "ScaLoginStart {factor, challengeId, expiresAt}"
    C->>G: "POST /sca/login/complete {factor, challengeId, code}"
    G->>S: verify code
    S-->>G: status
    G-->>C: "ScaLoginComplete {status: SUCCESS}"

    Note over C,S: Beneficiary Trust
    C->>G: "POST /customers/{id}/external-accounts/{extId}/trust"
    G->>S: start trust
    S-->>G: scaChallenge (optional)
    G-->>C: "BeneficiaryTrustStart {scaChallenge?}"
    C->>G: "POST .../trust/confirm {challengeId?, code|passkeyAssertion+origin}"
    G->>S: confirm trust
    S-->>G: "trusted=true"
    G-->>C: "BeneficiaryTrustConfirm {trusted: true}"

    Note over C,S: 2FA Reset
    C->>G: "POST /customers/{id}/sca/factors/reset {factor}"
    G->>S: start liveness
    S-->>G: resetId + liveness handles
    G-->>C: "TwoFactorResetStart {resetId, livenessAccessToken?, verificationLink?}"
    loop Poll until LIVENESS_PASSED
        C->>G: "GET /sca/factors/reset/{resetId}"
        G->>S: check status
        S-->>G: status
        G-->>C: "TwoFactorResetStatus {status}"
    end
    C->>G: "POST /sca/factors/reset/{resetId}/complete"
    G->>S: complete reset
    G-->>C: 204 No Content
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 C as Caller
    participant G as Grid API
    participant S as Striga (SCA Provider)

    Note over C,S: Factor Enrollment — TOTP
    C->>G: "POST /customers/{id}/sca/factors/totp"
    G->>S: start TOTP enrollment
    S-->>G: secret + totpUri
    G-->>C: "TotpEnrollmentStart {secret, secretBase32Encoded, totpUri}"
    C->>G: "POST /sca/factors/totp/confirm {secret, code}"
    G->>S: confirm TOTP
    S-->>G: recovery codes
    G-->>C: "TotpEnrollmentConfirmResponse {recoveryCodes}"

    Note over C,S: SCA Login — SMS_OTP
    C->>G: "POST /customers/{id}/sca/login/start {factor: SMS_OTP}"
    G->>S: dispatch OTP
    S-->>G: challengeId + expiresAt
    G-->>C: "ScaLoginStart {factor, challengeId, expiresAt}"
    C->>G: "POST /sca/login/complete {factor, challengeId, code}"
    G->>S: verify code
    S-->>G: status
    G-->>C: "ScaLoginComplete {status: SUCCESS}"

    Note over C,S: Beneficiary Trust
    C->>G: "POST /customers/{id}/external-accounts/{extId}/trust"
    G->>S: start trust
    S-->>G: scaChallenge (optional)
    G-->>C: "BeneficiaryTrustStart {scaChallenge?}"
    C->>G: "POST .../trust/confirm {challengeId?, code|passkeyAssertion+origin}"
    G->>S: confirm trust
    S-->>G: "trusted=true"
    G-->>C: "BeneficiaryTrustConfirm {trusted: true}"

    Note over C,S: 2FA Reset
    C->>G: "POST /customers/{id}/sca/factors/reset {factor}"
    G->>S: start liveness
    S-->>G: resetId + liveness handles
    G-->>C: "TwoFactorResetStart {resetId, livenessAccessToken?, verificationLink?}"
    loop Poll until LIVENESS_PASSED
        C->>G: "GET /sca/factors/reset/{resetId}"
        G->>S: check status
        S-->>G: status
        G-->>C: "TwoFactorResetStatus {status}"
    end
    C->>G: "POST /sca/factors/reset/{resetId}/complete"
    G->>S: complete reset
    G-->>C: 204 No Content
Loading

Reviews (7): Last reviewed commit: "docs(sca): enumerate record-event values..." | 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

jklein24 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@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 Outdated
Comment thread openapi/components/schemas/sca/RecordSecurityEventRequest.yaml
Comment thread openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml Outdated
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 added a commit that referenced this pull request Jul 10, 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
jklein24 force-pushed the feat/striga-sca-management branch from 763b88b to af68ac7 Compare July 10, 2026 07:52
Comment thread openapi/components/schemas/sca/RecordSecurityEventRequest.yaml Outdated
Comment thread openapi/components/schemas/sca/BeneficiaryTrustStart.yaml Outdated
jklein24 added a commit that referenced this pull request Jul 14, 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
jklein24 force-pushed the feat/striga-sca-management branch from af68ac7 to d71d44d Compare July 14, 2026 06:29
jklein24 added a commit that referenced this pull request Jul 14, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.

Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24 changed the base branch from feat/striga-sca to graphite-base/600 July 17, 2026 16:45
jklein24 added a commit that referenced this pull request Jul 17, 2026
## 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 of `code` / `passkeyAssertion`; `origin` required with a passkey assertion)

**Per-transaction authorization:**
- `PENDING_AUTHORIZATION` status + readOnly `scaChallenge` on `Quote` and `Transaction` (incl. the `OutgoingTransactionStatus` override) — omitted entirely for non-SCA providers.
- Resource-scoped authorize endpoints: `POST /quotes/{id}/authorize` → `Quote`, `POST /transactions/{id}/authorize` → `Transaction` (both `409` for non-SCA providers).
- SMS code re-send: `POST /quotes/{id}/authorize/resend`, `POST /transactions/{id}/authorize/resend`.
- Inline one-shot proof: optional `scaAuthorization` on `execute` / `transfer-out` (satisfy the challenge in the same call).
- Optional `scaFactor` on `execute` / `transfer-out` to pick `SMS_OTP` (default) or `PASSKEY` for 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:
- Factor **enrollment** — TOTP + passkey register / list / delete
- **Login / 180-day session** — `sca/login/start|complete`
- **Beneficiary trust** whitelisting — `external-accounts/{id}/trust`, `.../untrust/confirm`
- **2FA reset / recovery** — liveness-gated
- Security-event recording — `sca/record-event`

Also 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

- The realtime-funding **`202` + withheld-`paymentInstructions`** path on `POST /quotes` is 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 to `main` on its own. #600 depends on the `ScaFactor` / `ScaChallenge` / `ScaAuthorization` schemas introduced here.

## Invisibility guarantee

`scaChallenge`, `PENDING_AUTHORIZATION`, the authorize/resend endpoints, and `scaFactor` engage **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](https://claude.com/claude-code)
credentialId:
type:
- string
- 'null'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need explicit nulls here? I saw this in another PR but always thought that the field would be excluded vs explicitly null'd out

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good call — dropped them. These are genuinely omitted for TOTP/SMS_OTP, not sent as null, so plain-optional is the right contract and the explicit null was just wrong here.

fwiw the rule I'm going by: nullable and optional are separate axes. Leave a field out of required when the key can be absent; only add 'null' to the type when the server actually puts "field": null on the wire. A few of the newer sca/agents schemas started reaching for explicit null where the field is really just omitted — that's the inconsistency you're seeing. Fixing those as I hit them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait who dis?

jklein24 and others added 8 commits July 17, 2026 10:21
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>
Match the #558 terminology fix: the enrollment / login / trust / reset endpoints
now say "customers in a region where SCA is required (e.g. EU)" / "customers
outside SCA-regulated regions" instead of "customers whose payment provider
requires SCA", and the 409 reads "SCA is not required for this customer".
Incidental references to the underlying provider as a system (provider-reported
status, risk engine, whitelist handle) are left as-is.

Co-Authored-By: Claude <noreply@anthropic.com>
…nt surface

To the integrator, Grid is the provider; the underlying provider must never leak
into the docs. Reworded provider-reported status -> the (reported) status, the
provider's risk engine -> Grid's risk engine, whitelist/liveness handles and
'when the provider does not return one' -> neutral phrasing.

Co-Authored-By: Claude <noreply@anthropic.com>
Per review on #600:
- RecordSecurityEventRequest.eventType is now an explicit enum
  (RESET_PASSWORD_COMPLETED | FAILED_LOGIN_ATTEMPT) with each event's effect,
  instead of a vague free-string "closed vocabulary".
- Drop whitelistedId from the beneficiary-trust surface: BeneficiaryTrustStart
  returns just the scaChallenge, and confirm/untrust identify the beneficiary by
  the {externalAccountId} already in the path. Requires the backend to persist
  the externalAccountId -> whitelist handle mapping (tracked follow-up).

Co-Authored-By: Claude <noreply@anthropic.com>
Untrust previously had only a confirm step, so an SMS_OTP (default, no
enrollment) or PASSKEY customer had no way to obtain the SCA challenge that
untrust/confirm expects — only a self-served TOTP code worked. Add
POST /customers/{customerId}/external-accounts/{externalAccountId}/untrust
(startBeneficiaryUntrust), mirroring startBeneficiaryTrust: it issues the
scaChallenge that untrust/confirm satisfies. Returns BeneficiaryTrustStart.
…docs)

transfer-out has no associated quote and does not offer Strong Customer
Authentication — per-transaction SCA is authorized only on the quote
(POST /quotes/{quoteId}/authorize). Remove the vestigial scaFactor field from
TransferOutRequest and the SCA paragraph from the transfer-out 201 response
description, so the endpoint carries no SCA surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZsLXjVaJZbjjjoswBmhMu
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.

Co-Authored-By: Claude <noreply@anthropic.com>
@jklein24
jklein24 force-pushed the feat/striga-sca-management branch from 94e5d2f to b7f7e5b Compare July 17, 2026 17:24
@jklein24
jklein24 force-pushed the graphite-base/600 branch from 3ab6065 to 9fc8846 Compare July 17, 2026 17:24
@jklein24
jklein24 changed the base branch from graphite-base/600 to main July 17, 2026 17:24

@pengying pengying left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 2 general questions
1. how does someone add sms otp
2. do we need -null as a type if the field is optional or are we explicitly setting it to null.

selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses
the customer's verified phone). Returns the factor-specific material needed to
finish via `POST /sca/factors/confirm`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is enrolling the concept of trusting a user device / auth method. What triggers the sms otp?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. With striga, sms is enrolled by default since creating a customer requires sms and email verification via the contact verification API flows. I could definitely document this a bit better here, though. Striga does that at the top of this page: https://docs.striga.com/reference/enrollment. I'll clarify the SMS_OTP line here to indicate that all customers need a verified phone at customer creation in SCA-regulated regions. See the "verifications" routes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so then when the customer is created, they provide the phone number as part of customer creation. we auto send a sms otp?

What if they change phone numbers? is that a patch to customer.phone_number that triggers a sms otp? or do we make it explicit here?

…llable

These fields are omitted (not sent as null) for TOTP/SMS_OTP factors, so
explicit null was the wrong contract. Drop 'null' from the type; the fields
stay optional via absence from `required`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.

Co-Authored-By: Claude <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Jul 17, 2026, 6:04 PM

@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟡 Building Jul 17, 2026, 6:03 PM

…lment

Expand the enrollment description to explain that SMS_OTP is not enrolled
through POST /sca/factors — every customer in an SCA-regulated region has a
verified phone from customer creation (Contact Verification flows), so SMS is
always available as a factor. TOTP/PASSKEY remain the explicit opt-in factors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c
jklein24 added a commit that referenced this pull request Jul 17, 2026
Match the #558/#600 terminology fix in the embedded SCA docs section: 'customers
in a region where SCA is required (EU)' / 'customers outside SCA-regulated
regions' instead of 'whose payment provider requires SCA'.

Co-Authored-By: Claude <noreply@anthropic.com>
jklein24 added a commit that referenced this pull request Jul 17, 2026
The transfer-out SCA/deprecation language was removed from the endpoint spec in
#600, so drop the "being deprecated" claim from the per-transaction guide and
stop showing transfer-out as an SCA-challenge-producing path in the overview
sequence diagram. transfer-out has no associated quote and offers no SCA-gated
EU debits; the guide points EU callers to the quote + execute flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZsLXjVaJZbjjjoswBmhMu
@jklein24
jklein24 requested a review from pengying July 17, 2026 18:44
@jklein24
jklein24 merged commit 6c4497b into main Jul 17, 2026
12 checks passed
@jklein24
jklein24 deleted the feat/striga-sca-management branch July 17, 2026 23:10
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.

4 participants