Add encrypted OTP flow#506
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
|
| 💡 Name/Renamed: 406 names were renamed due to language constraints, so fallback names will be used instead. |
⚠️ grid-php studio · code · diff
Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ❗→lint ❗(prev:lint ✅) →test ✅
✅ grid-cli studio · code · diff
Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
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-05-26 23:22:40 UTC
Greptile SummaryThis PR introduces a V3 HPKE-based
Confidence Score: 3/5The new V3 flow is clearly described and the documentation is thorough, but the The core auth flow description, response codes, header additions, and deprecation markings are accurate and internally consistent. The gap is in
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml | Removes otp and clientPublicKey from required and adds encryptedOtpBundle; only type remains required, leaving no schema enforcement that exactly one of the two field sets is present. |
| openapi/paths/auth/auth_credentials_{id}_verify.yaml | Adds V3 two-leg EMAIL_OTP flow with Grid-Wallet-Signature header, a new 202 response schema, and updated 401 conditions; thorough and accurate, though the payloadToSign example has a non-base64url .signature placeholder. |
| openapi/components/schemas/auth/AuthMethodResponse.yaml | Adds optional otpEncryptionTargetBundle string property to the allOf, correctly scoped to EMAIL_OTP with clear one-time-use and re-issue instructions. |
| openapi/components/schemas/auth/AuthSignedRequestChallenge.yaml | Description-only extension to cover the EMAIL_OTP verify retry use case and TEK keypair signing instructions; schema structure is unchanged and correct. |
| openapi/paths/auth/auth_credentials.yaml | Adds otpEncryptionTargetBundle to the 201 EMAIL_OTP response description and example; straightforward and accurate. |
| openapi/paths/auth/auth_credentials_{id}_challenge.yaml | Updates the EMAIL_OTP challenge description and example to surface otpEncryptionTargetBundle on re-issue; clear and consistent with the registration path. |
| openapi.yaml | Generated bundle — reflects all source changes from openapi/; no concerns specific to this file. |
| mintlify/openapi.yaml | Mintlify copy of the generated bundle; mirrors openapi.yaml as expected after make build. |
Sequence Diagram
sequenceDiagram
participant C as Client
participant S as Grid Server
Note over C,S: Registration
C->>S: POST /auth/credentials (EMAIL_OTP)
S-->>C: 201 AuthMethod + otpEncryptionTargetBundle
Note over C,S: V3 Secure OTP Flow
C->>C: Generate ephemeral P-256 TEK keypair
C->>C: "HPKE-encrypt {clientPublicKey, otpCodeAttempt} → encryptedOtpBundle"
C->>S: "POST /auth/credentials/{id}/verify {type, encryptedOtpBundle}"
S-->>C: "202 {payloadToSign, requestId, expiresAt}"
C->>C: Sign payloadToSign with TEK private key → stamp
C->>S: "POST /auth/credentials/{id}/verify + Grid-Wallet-Signature + Request-Id"
S-->>C: "200 AuthSession (TEK public key = session API key)"
Note over C,S: Legacy Flow (deprecated)
C->>S: "POST /auth/credentials/{id}/verify {type, otp, clientPublicKey}"
S-->>C: 200 AuthSession + encryptedSessionSigningKey
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml:1-6
**Schema gap: "provide exactly one set" is undeclared**
The schema declares only `type` as `required`, so a request body of `{"type":"EMAIL_OTP"}` (no OTP fields at all) passes OpenAPI validation and only fails at the server. Likewise, a body containing both `encryptedOtpBundle` and `otp`+`clientPublicKey` (the mutually-exclusive pair) also passes schema validation. Client SDK generators, mock servers, and contract-testing tools all rely on `required` arrays and `oneOf`/`not` constraints — prose-only documentation of the mutual-exclusivity rule won't be picked up by any of those.
### Issue 2 of 2
openapi/paths/auth/auth_credentials_{id}_verify.yaml:172-175
**`payloadToSign` example contains a non-base64url JWT signature segment**
The third JWT segment of the `payloadToSign` example value is the literal string `signature` rather than a base64url-encoded ECDSA signature. Developers who try to programmatically inspect or validate this example (e.g., via `jwt.io`, client-side JWT parsing, or automated contract tests) will get a parse error. A realistic placeholder — even a dummy but structurally valid base64url string — would avoid confusion.
Reviews (1): Last reviewed commit: "Add encrypted OTP flow" | Re-trigger Greptile
| type: object | ||
| required: | ||
| - type | ||
| - otp | ||
| - clientPublicKey | ||
| description: >- | ||
| Verify an email-OTP credential. Two request shapes are accepted; provide | ||
| exactly one set. |
There was a problem hiding this comment.
Schema gap: "provide exactly one set" is undeclared
The schema declares only type as required, so a request body of {"type":"EMAIL_OTP"} (no OTP fields at all) passes OpenAPI validation and only fails at the server. Likewise, a body containing both encryptedOtpBundle and otp+clientPublicKey (the mutually-exclusive pair) also passes schema validation. Client SDK generators, mock servers, and contract-testing tools all rely on required arrays and oneOf/not constraints — prose-only documentation of the mutual-exclusivity rule won't be picked up by any of those.
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml
Line: 1-6
Comment:
**Schema gap: "provide exactly one set" is undeclared**
The schema declares only `type` as `required`, so a request body of `{"type":"EMAIL_OTP"}` (no OTP fields at all) passes OpenAPI validation and only fails at the server. Likewise, a body containing both `encryptedOtpBundle` and `otp`+`clientPublicKey` (the mutually-exclusive pair) also passes schema validation. Client SDK generators, mock servers, and contract-testing tools all rely on `required` arrays and `oneOf`/`not` constraints — prose-only documentation of the mutual-exclusivity rule won't be picked up by any of those.
How can I resolve this? If you propose a fix, please make it concise.| type: EMAIL_OTP | ||
| payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.signature | ||
| requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 | ||
| expiresAt: '2026-04-08T15:35:00Z' |
There was a problem hiding this comment.
payloadToSign example contains a non-base64url JWT signature segment
The third JWT segment of the payloadToSign example value is the literal string signature rather than a base64url-encoded ECDSA signature. Developers who try to programmatically inspect or validate this example (e.g., via jwt.io, client-side JWT parsing, or automated contract tests) will get a parse error. A realistic placeholder — even a dummy but structurally valid base64url string — would avoid confusion.
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/paths/auth/auth_credentials_{id}_verify.yaml
Line: 172-175
Comment:
**`payloadToSign` example contains a non-base64url JWT signature segment**
The third JWT segment of the `payloadToSign` example value is the literal string `signature` rather than a base64url-encoded ECDSA signature. Developers who try to programmatically inspect or validate this example (e.g., via `jwt.io`, client-side JWT parsing, or automated contract tests) will get a parse error. A realistic placeholder — even a dummy but structurally valid base64url string — would avoid confusion.
How can I resolve this? If you propose a fix, please make it concise.36a042d to
20444ac
Compare

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.