Add encrypted OTP flow#506
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript ✅ grid-ruby studio · code
✅ grid-kotlin studio · code
|
Greptile SummaryThis PR introduces a V3 HPKE-based
Confidence Score: 4/5Safe to merge with one schema fix — the legacy EMAIL_OTP fields are completely absent from the new schema rather than marked deprecated, which will break schema validation for any existing integrators still on the plaintext flow. The core V3 HPKE flow is well-specified across the path, schema, and example files. The one concrete defect is in EmailOtpCredentialVerifyRequestFields.yaml: the PR description promises deprecated-but-functional legacy fields, but the schema removes them entirely, leaving legacy callers with no documented path and breaking schema validators.
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml | Replaces legacy otp+clientPublicKey fields with encryptedOtpBundle for V3 HPKE flow; legacy fields are completely removed (not marked deprecated as the PR description states), breaking schema validation for existing legacy callers. |
| mintlify/snippets/global-accounts/client-keys.mdx | Adds the new 'Encrypt the OTP code' section for EMAIL_OTP V3; section 2 still implies a single-call flow returning encryptedSessionSigningKey, which is incorrect for the two-leg EMAIL_OTP V3 flow; line 12 clientPublicKey routing description is now stale. |
| openapi/components/schemas/auth/AuthMethodResponse.yaml | Adds otpEncryptionTargetBundle property to carry the HPKE target bundle on EMAIL_OTP registration and challenge responses; well-documented with clear usage guidance. |
| openapi/paths/auth/auth_credentials_{id}_verify.yaml | Adds Grid-Wallet-Signature header, 202 response schema, updated 401 conditions, and emailOtp/emailOtpSignedRetry examples; the two-leg flow is clearly described and examples are well-structured. |
| openapi/components/schemas/auth/AuthSignedRequestChallenge.yaml | Extends description to cover the EMAIL_OTP verify retry use case; correctly documents that the TEK keypair (not session API keypair) is used for signing in this context. |
| openapi/paths/auth/auth_credentials_{id}_challenge.yaml | Updated to return otpEncryptionTargetBundle in the EMAIL_OTP challenge response; example value and description updated accordingly. |
| openapi/components/schemas/auth/AuthSession.yaml | Adds clarification that EMAIL_OTP sessions omit encryptedSessionSigningKey; accurately reflects the new V3 flow where the client retains the TEK private key. |
| openapi/paths/auth/auth_credentials.yaml | 201 response description updated to mention otpEncryptionTargetBundle for EMAIL_OTP; example value updated; no issues. |
Sequence Diagram
sequenceDiagram
participant C as Client
participant B as Integrator Backend
participant G as Grid API
Note over C,G: Registration
B->>G: POST /auth/credentials
G-->>B: 201 AuthMethodResponse with otpEncryptionTargetBundle
G-)C: OTP email delivered
Note over C,G: V3 EMAIL_OTP Verification
C->>C: Generate TEK keypair
C->>C: HPKE-encrypt otp_code+clientPublicKey
B->>G: POST /auth/credentials/id/verify with encryptedOtpBundle
G-->>B: 202 with payloadToSign and requestId
C->>C: Sign payloadToSign with TEK private key
B->>G: Retry with Grid-Wallet-Signature and Request-Id
G-->>B: 200 AuthSession
Note over C: TEK private key becomes session signing key
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml:1-4
**Legacy `otp`/`clientPublicKey` fields silently removed instead of deprecated**
The PR description states these fields are "marked deprecated" and "will be removed in a future release," but the new schema completely omits them — they no longer appear as properties at all. Any legacy client request with `{"type":"EMAIL_OTP","otp":"123456","clientPublicKey":"04..."}` (without `encryptedOtpBundle`) will fail schema validation because `encryptedOtpBundle` is now listed as required, and validators won't understand the payload shape. SDK generators and contract-testing tools will see no hint that a deprecated alternative ever existed.
If the server-side legacy flow is still intentionally functional (as the PR description implies), the schema should retain `otp` and `clientPublicKey` as optional properties with `deprecated: true` so integrators have a documented migration path rather than a silent break.
### Issue 2 of 3
mintlify/snippets/global-accounts/client-keys.mdx:158-160
**Section 2 description is inaccurate for the new EMAIL_OTP V3 two-leg flow**
"Your backend calls `POST /auth/credentials/{id}/verify` and returns the `encryptedSessionSigningKey` from Grid's response to the client." For EMAIL_OTP in the V3 flow, the first call returns `202` with `payloadToSign` (not an `AuthSession`), and the signed retry returns a `200` `AuthSession` that omits `encryptedSessionSigningKey` entirely. A developer following this section linearly for EMAIL_OTP would expect a single call and an `encryptedSessionSigningKey` they need to decrypt — neither is true. The note at line 156 hints at the omission but doesn't redirect the reader through the correct two-leg retry before arriving at section 3.
### Issue 3 of 3
mintlify/snippets/global-accounts/client-keys.mdx:12
**Stale `clientPublicKey` routing description for EMAIL_OTP**
"for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`" refers to where `clientPublicKey` is sent. In the V3 EMAIL_OTP flow, `clientPublicKey` (the TEK public key) is no longer a standalone field on that endpoint — it's embedded inside the HPKE-encrypted `encryptedOtpBundle`. A developer reading this sentence might try to pass `clientPublicKey` as a top-level request field for EMAIL_OTP, which the new schema doesn't include. The sentence should note that for EMAIL_OTP the public key is sealed inside `encryptedOtpBundle`.
Reviews (3): Last reviewed commit: "Add encrypted OTP flow" | Re-trigger Greptile
36a042d to
20444ac
Compare
20444ac to
2fbd5ec
Compare
|
2fbd5ec to
f9f2bec
Compare
| returned in the response to this public key. The key is ephemeral | ||
| and one-time-use per verification request. | ||
| example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 | ||
| HPKE-encrypted payload binding the client's ephemeral public key |
There was a problem hiding this comment.
is there something that describes the format of this bundle or how to create it?
50e731f to
2c892e7
Compare
2c892e7 to
261efa2
Compare
## Summary Updates documentation to match the V3 secure EMAIL_OTP flow introduced in #506. The OpenAPI schema now requires `encryptedOtpBundle` instead of the deprecated plaintext `otp` + `clientPublicKey` fields. **Changes:** ### Mintlify Documentation - **authentication.mdx**: Updated EMAIL_OTP section with new mermaid diagram, challenge response showing `otpEncryptionTargetBundle`, and two-step verify flow (202 → signed retry → 200) - **walkthrough.mdx**: Updated "Authenticate and sign" section to show the encrypted OTP and signed retry pattern - **sandbox-global-account-magic.mdx**: Updated EMAIL_OTP sandbox docs to show encrypted flow (sandbox runs real HPKE end-to-end) ### Scripts - **scripts/README.md**: Updated offramp guide to use `encrypt-otp` command and two-step verify - **scripts/embedded-wallet-sign.js**: Added `encrypt-otp` command for HPKE-encrypting OTP attempts ### Key behavior changes documented: - The TEK (Target Encryption Key) private key generated by the client becomes the session signing key - EMAIL_OTP no longer returns `encryptedSessionSigningKey` in the response (no decryption step needed) - `/challenge` now returns `otpEncryptionTargetBundle` for EMAIL_OTP - `/verify` is now a two-step flow: first call returns 202 with `payloadToSign`, signed retry returns 200 with session ## Test plan - [ ] Verify mermaid diagrams render correctly in Mintlify dev server - [ ] Test `scripts/embedded-wallet-sign.js encrypt-otp` command works - [ ] Review code samples match the OpenAPI schema examples 🤖 Generated with [Claude Code](https://claude.com/claude-code)

TL;DR
Introduces a secure V3 HPKE-based
EMAIL_OTPverification flow and deprecates the legacy plaintext OTP flow.What changed?
POST /auth/credentials(registration)201response forEMAIL_OTPcredentials now includesotpEncryptionTargetBundle— a one-time HPKE target bundle the client uses to encrypt the OTP attempt before sending it to the server.POST /auth/credentials/{id}/challenge(re-issue)EMAIL_OTPresponse now returns a freshotpEncryptionTargetBundlealongside theAuthMethod, replacing the previous description that said there was no challenge body to surface.POST /auth/credentials/{id}/verify(verification)EMAIL_OTP: the client submits anencryptedOtpBundle(HPKE-encrypted payload containing the TEK public key and OTP code attempt). The server responds with202carrying apayloadToSign(verificationToken). The client signs the token with the TEK private key and retries withGrid-Wallet-SignatureandRequest-Idheaders to receive the issuedAuthSession. The TEK public key becomes the session API key on completion.EMAIL_OTPflow (plaintextotp+clientPublicKey) is now marked deprecated and will be removed in a future release.Grid-Wallet-Signaturerequest header, required on the signed retry leg of the V3EMAIL_OTPflow.Request-Idheader description to cover both theEMAIL_OTPsigned retry andPASSKEYassertion correlation use cases.202response schema (AuthSignedRequestChallenge) to the verify endpoint.401error conditions to coverEMAIL_OTPsigned retry failures (missing/malformed signature, key mismatch, expired challenge)."000000".Schema changes
AuthMethodResponse: addsotpEncryptionTargetBundleproperty.AuthSignedRequestChallenge: extended to cover theEMAIL_OTPverify retry use case; documents that the TEK keypair (not the session API keypair) is used to sign the stamp for this operation.EmailOtpCredentialVerifyRequestFields:otpandclientPublicKeymarked deprecated;encryptedOtpBundleadded;otpandclientPublicKeyremoved fromrequired.How to test?
V3 flow:
EMAIL_OTPcredential viaPOST /auth/credentialsand captureotpEncryptionTargetBundlefrom the response.{clientPublicKey, otpCodeAttempt}underotpEncryptionTargetBundleto produceencryptedOtpBundle.POST /auth/credentials/{id}/verifywithencryptedOtpBundleand expect a202response containingpayloadToSignandrequestId.payloadToSignwith the TEK private key and resubmit withGrid-Wallet-SignatureandRequest-Idheaders; expect a200AuthSession."000000"as the magic value.Legacy flow (still functional, deprecated):
POST /auth/credentials/{id}/verifywith plaintextotpandclientPublicKey; expect a200AuthSessionwithencryptedSessionSigningKey.Why make this change?
The legacy
EMAIL_OTPflow transmits the plaintext OTP code to the server, creating an unnecessary exposure surface. The V3 flow uses HPKE to encrypt the OTP code and the client's public key together so the plaintext code never transits the server. The TEK keypair generated by the client for encryption also becomes the session API key, binding authentication and session establishment into a single cryptographic operation.