Skip to content

docs: Update EMAIL_OTP docs to use encrypted OTP flow#553

Open
claude[bot] wants to merge 1 commit into
mainfrom
docs/sync-20260605
Open

docs: Update EMAIL_OTP docs to use encrypted OTP flow#553
claude[bot] wants to merge 1 commit into
mainfrom
docs/sync-20260605

Conversation

@claude
Copy link
Copy Markdown
Contributor

@claude claude Bot commented Jun 5, 2026

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

The OpenAPI schema now uses the V3 secure EMAIL_OTP flow with
`encryptedOtpBundle` instead of plaintext `otp` + `clientPublicKey`.
This updates all documentation to match:

- 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)
- 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

The TEK (Target Encryption Key) private key generated by the client
becomes the session signing key, so EMAIL_OTP no longer returns
`encryptedSessionSigningKey` in the response.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 5, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Jun 5, 2026 9:21am

Request Review

@claude claude Bot requested review from pengying and shreyav June 5, 2026 09:21
@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Jun 5, 2026

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

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Jun 5, 2026, 9:23 AM

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

Updates documentation and the embedded-wallet-sign.js helper script to reflect the V3 secure EMAIL_OTP flow, replacing plaintext otp + clientPublicKey with HPKE-encrypted encryptedOtpBundle and a new two-step verify pattern (202 → signed retry → 200) where the TEK private key doubles as the session signing key.

  • Mintlify docs (authentication.mdx, walkthrough.mdx, sandbox-global-account-magic.mdx): Mermaid diagrams, curl examples, and prose all updated to show the encrypted bundle flow, otpEncryptionTargetBundle from challenge, and the fact that EMAIL_OTP no longer returns encryptedSessionSigningKey.
  • scripts/embedded-wallet-sign.js: Adds encrypt-otp subcommand that HPKE-encrypts {otp_code, public_key} to the enclave target bundle; updates gen-keypair and decrypt-bundle doc comments for the new flow.
  • scripts/README.md: Rewrites §3 offramp walk-through to follow the new gen-keypairchallengeencrypt-otp → two-step verify sequence.

Confidence Score: 3/5

The Mintlify documentation changes are accurate and well-structured, but the helper script has two issues that would prevent it from working correctly as written.

The encryptOtp function never checks dataSignature before trusting targetPublic, so a substituted challenge response silently directs the client to encrypt against an attacker-controlled key. Separately, the README curl commands embed $ENC_BUNDLE as a raw JSON object rather than the JSON string the API expects, meaning the documented shell flow would fail at runtime.

scripts/embedded-wallet-sign.js (missing bundle signature verification) and scripts/README.md (shell quoting produces wrong JSON type for encryptedOtpBundle).

Security Review

  • Missing bundle signature verification (scripts/embedded-wallet-sign.js, encryptOtp): The function extracts targetPublic from the otpEncryptionTargetBundle without verifying dataSignature against enclaveQuorumPublic. An attacker who can substitute or tamper with the challenge response could supply a malicious targetPublic, causing the client to HPKE-encrypt both the OTP code and its TEK public key to an adversary-controlled key — effectively handing the attacker the session signing key.

Important Files Changed

Filename Overview
scripts/embedded-wallet-sign.js Adds encrypt-otp subcommand with HPKE encryption using @turnkey/crypto; encryptOtp skips verification of dataSignature/enclaveQuorumPublic before trusting targetPublic.
scripts/README.md Updated offramp guide to document two-step encrypted OTP flow; $ENC_BUNDLE interpolation in curl commands embeds bundle as a JSON object instead of the JSON string the API expects.
mintlify/snippets/global-accounts/authentication.mdx Updated EMAIL_OTP section with new mermaid diagram, encrypted bundle flow, and two-step 202→200 verify pattern; curl examples and JSON responses look internally consistent.
mintlify/snippets/global-accounts/walkthrough.mdx Reworked 'Authenticate and sign' steps to reflect TEK-based session key and encrypted OTP verify flow; no issues found.
mintlify/snippets/sandbox-global-account-magic.mdx Updated sandbox EMAIL_OTP section to describe real HPKE end-to-end with magic 000000 code; two-leg curl example and wallet-signature section accurately reflect the new flow.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant IB as Your Backend
    participant G as Grid API
    participant E as Email

    C->>IB: POST /otp/challenge
    IB->>G: "POST /auth/credentials/{id}/challenge"
    G->>E: deliver OTP email
    G-->>IB: 200 + otpEncryptionTargetBundle
    IB-->>C: otpEncryptionTargetBundle
    E-->>C: OTP code

    C->>C: generateKeyPair() → TEK
    C->>C: "HPKE-encrypt {otp_code, public_key} → encryptedOtpBundle"
    C->>IB: "POST /otp/verify { encryptedOtpBundle }"
    IB->>G: "POST /auth/credentials/{id}/verify"
    G-->>IB: "202 { payloadToSign, requestId }"
    IB-->>C: payloadToSign + requestId

    C->>C: sign(payloadToSign, TEK privKey) → stamp
    C->>IB: "POST /otp/verify/complete { stamp, requestId }"
    IB->>G: Retry + Grid-Wallet-Signature + Request-Id
    G-->>IB: 200 AuthSession
    IB-->>C: "{ expiresAt }"

    Note over C: TEK private key IS the session signing key
Loading
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
scripts/README.md:207-221
**`$ENC_BUNDLE` interpolation sends wrong JSON type**

The `encrypt-otp` command writes `JSON.stringify(encBundle)` to stdout, so `$ENC_BUNDLE` holds a JSON object string like `{"encappedPublic":"...","ciphertext":"..."}`. Embedding it as `'"$ENC_BUNDLE"'` inside the `-d` argument makes `encryptedOtpBundle` a **JSON object** in the request body, but every curl example in the docs (and presumably the OpenAPI schema) expects it to be a **JSON string** — with the bundle serialized inside quotes, e.g. `"encryptedOtpBundle": "{\"encappedPublic\":\"...\",\"ciphertext\":\"...\"}"`. The API would likely reject the object-typed value with a 400.

A safe fix is to use `jq` to produce the correct JSON body: `-d "$(jq -n --arg b "$ENC_BUNDLE" '{"type":"EMAIL_OTP","encryptedOtpBundle":$b}')"`. This applies to both the first and second curl commands in §3.4.

### Issue 2 of 2
scripts/embedded-wallet-sign.js:75-88
**Bundle signature not verified before trusting `targetPublic`**

`encryptOtp` blindly trusts the `targetPublic` extracted from the `data` field without verifying `dataSignature` against `enclaveQuorumPublic`. A tampered or MITM-substituted bundle would cause the client to HPKE-encrypt the OTP (and its TEK public key) to an attacker-controlled recipient key, handing an adversary both the OTP value and the key that becomes the session signing key. The `dataSignature` exists precisely to prevent this; omitting the check makes the security guarantee of the enclave bundle worthless from the client's perspective.

Reviews (1): Last reviewed commit: "docs: Update EMAIL_OTP docs to use encry..." | Re-trigger Greptile

Comment thread scripts/README.md
Comment on lines +207 to 221
-d '{"type": "EMAIL_OTP", "encryptedOtpBundle": '"$ENC_BUNDLE"'}' \
"$GRID_BASE_URL/auth/credentials/$CRED_ID/verify")

VERIFY_PAYLOAD=$(echo "$VERIFY1" | jq -r .payloadToSign)
VERIFY_REQ_ID=$(echo "$VERIFY1" | jq -r .requestId)

# Sign the verification token with the TEK private key
VERIFY_STAMP=$($SIGN stamp "$PRIV_HEX" "$VERIFY_PAYLOAD")

# Signed retry — complete login
VERIFY2=$(g -X POST -H 'Content-Type: application/json' \
-H "Grid-Wallet-Signature: $VERIFY_STAMP" \
-H "Request-Id: $VERIFY_REQ_ID" \
-d '{"type": "EMAIL_OTP", "encryptedOtpBundle": '"$ENC_BUNDLE"'}' \
"$GRID_BASE_URL/auth/credentials/$CRED_ID/verify")
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.

P1 $ENC_BUNDLE interpolation sends wrong JSON type

The encrypt-otp command writes JSON.stringify(encBundle) to stdout, so $ENC_BUNDLE holds a JSON object string like {"encappedPublic":"...","ciphertext":"..."}. Embedding it as '"$ENC_BUNDLE"' inside the -d argument makes encryptedOtpBundle a JSON object in the request body, but every curl example in the docs (and presumably the OpenAPI schema) expects it to be a JSON string — with the bundle serialized inside quotes, e.g. "encryptedOtpBundle": "{\"encappedPublic\":\"...\",\"ciphertext\":\"...\"}". The API would likely reject the object-typed value with a 400.

A safe fix is to use jq to produce the correct JSON body: -d "$(jq -n --arg b "$ENC_BUNDLE" '{"type":"EMAIL_OTP","encryptedOtpBundle":$b}')". This applies to both the first and second curl commands in §3.4.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/README.md
Line: 207-221

Comment:
**`$ENC_BUNDLE` interpolation sends wrong JSON type**

The `encrypt-otp` command writes `JSON.stringify(encBundle)` to stdout, so `$ENC_BUNDLE` holds a JSON object string like `{"encappedPublic":"...","ciphertext":"..."}`. Embedding it as `'"$ENC_BUNDLE"'` inside the `-d` argument makes `encryptedOtpBundle` a **JSON object** in the request body, but every curl example in the docs (and presumably the OpenAPI schema) expects it to be a **JSON string** — with the bundle serialized inside quotes, e.g. `"encryptedOtpBundle": "{\"encappedPublic\":\"...\",\"ciphertext\":\"...\"}"`. The API would likely reject the object-typed value with a 400.

A safe fix is to use `jq` to produce the correct JSON body: `-d "$(jq -n --arg b "$ENC_BUNDLE" '{"type":"EMAIL_OTP","encryptedOtpBundle":$b}')"`. This applies to both the first and second curl commands in §3.4.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +75 to +88
function encryptOtp(otpEncryptionTargetBundle, pubHex, otpCode) {
// Extract targetPublic from the signed bundle
const { data } = JSON.parse(otpEncryptionTargetBundle);
const dataJson = Buffer.from(data, "hex").toString("utf8");
const { targetPublic } = JSON.parse(dataJson);

// HPKE-encrypt {otp_code, public_key} to the target
const plainText = JSON.stringify({ otp_code: otpCode, public_key: pubHex });
const plainTextBuf = Buffer.from(plainText, "utf8");
const targetKeyBuf = hexToBytes(targetPublic);

const encryptedBuf = hpkeEncrypt({ plainTextBuf, targetKeyBuf });
return formatHpkeBuf(encryptedBuf);
}
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.

P1 security Bundle signature not verified before trusting targetPublic

encryptOtp blindly trusts the targetPublic extracted from the data field without verifying dataSignature against enclaveQuorumPublic. A tampered or MITM-substituted bundle would cause the client to HPKE-encrypt the OTP (and its TEK public key) to an attacker-controlled recipient key, handing an adversary both the OTP value and the key that becomes the session signing key. The dataSignature exists precisely to prevent this; omitting the check makes the security guarantee of the enclave bundle worthless from the client's perspective.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/embedded-wallet-sign.js
Line: 75-88

Comment:
**Bundle signature not verified before trusting `targetPublic`**

`encryptOtp` blindly trusts the `targetPublic` extracted from the `data` field without verifying `dataSignature` against `enclaveQuorumPublic`. A tampered or MITM-substituted bundle would cause the client to HPKE-encrypt the OTP (and its TEK public key) to an attacker-controlled recipient key, handing an adversary both the OTP value and the key that becomes the session signing key. The `dataSignature` exists precisely to prevent this; omitting the check makes the security guarantee of the enclave bundle worthless from the client's perspective.

How can I resolve this? If you propose a fix, please make it concise.

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.

0 participants