Skip to content

✨ server: add panda signature#991

Draft
nfmelendez wants to merge 1 commit intomainfrom
signature
Draft

✨ server: add panda signature#991
nfmelendez wants to merge 1 commit intomainfrom
signature

Conversation

@nfmelendez
Copy link
Copy Markdown
Contributor

@nfmelendez nfmelendez commented Apr 29, 2026

closes #714

Summary by CodeRabbit

  • New Features
    • SIWE and WebAuthn signature flows for card operations and webhooks, including challenge generation and server-side verification; helpers to request nonces and verify signatures.
  • Bug Fixes
    • Prevented decryption attempts when encrypted card fields are missing; PIN retrieval now returns null if required data is absent.
  • Tests
    • Extensive SIWE/WebAuthn test coverage for success, failure, and error mapping scenarios.
  • Chores
    • Added release metadata entry.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 29, 2026

🦋 Changeset detected

Latest commit: adf8f7d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Walkthrough

Adds Panda-backed signature flows: GET /card can return SIWE/WebAuthn challenges; PATCH /card accepts SIWE or WebAuthn assertions and verifies signatures; Panda utilities (nonce, verify, typed-data recovery) and runtime Panda signature checks in webhook spend/collection flows are introduced; tests updated accordingly.

Changes

Cohort / File(s) Summary
Release metadata
\.changeset/quiet-tigers-sign.md
Adds changeset entry: patch bump for @exactly/server with release note "add panda signature".
Card API endpoints
server/api/card.ts
GET / accepts optional sessionid and scope (siwe
Panda utilities
server/utils/panda.ts
Adds getNonce(userId), verify(userId, payload) (supports SIWE and WebAuthn payloads), and verifyPandaSignature(...) which recovers typed-data signer for Collection/Refund flows and validates expected signer.
Webhook transaction handling
server/hooks/panda.ts
Spend webhook schema accepts optional signature and timestamp; refund/capture and prepareCollection paths conditionally perform Panda signature verification, wrap verification in a Sentry span (panda.signature), set signature.valid, and capture exceptions on missing/invalid signatures or verification errors.
Tests
server/test/api/card.test.ts, server/test/utils/panda.test.ts
Adds extensive SIWE and WebAuthn tests for GET challenge and PATCH verification flows, error mappings (403, 400, 500), nonce/verify Panda helper tests, and WebAuthn credential serialization/transport handling.
Client-side decrypt guards
src/components/card/CardDetails.tsx, src/utils/server.ts
Decryption now only runs when both encryptedPan and encryptedCvc are present; getPIN short-circuits to null if encrypted fields missing.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CardAPI as Card API (server/api/card.ts)
    participant Panda as Panda Service (server/utils/panda.ts)
    participant DB as Database

    Client->>CardAPI: GET /card?scope=siwe
    CardAPI->>Panda: getNonce(userId)
    Panda-->>CardAPI: { nonce }
    CardAPI->>DB: fetch card
    DB-->>CardAPI: card data
    CardAPI-->>Client: { challenge, encryptedPan?, ... }

    Note over Client: User signs SIWE message

    Client->>CardAPI: PATCH /card { method:"siwe", message, signature }
    CardAPI->>Panda: verify(userId, { authType:"siwe", message, signature })
    Panda-->>CardAPI: success / error
    CardAPI-->>Client: { verification: "OK" } or error
Loading
sequenceDiagram
    participant Client
    participant CardAPI as Card API (server/api/card.ts)
    participant Panda as Panda Service (server/utils/panda.ts)
    participant DB as Database

    Client->>CardAPI: GET /card?scope=webauthn
    CardAPI->>DB: fetch card
    DB-->>CardAPI: card data
    CardAPI-->>Client: { challenge: authorizationStatement, encryptedPan?, ... }

    Note over Client: User performs WebAuthn assertion

    Client->>CardAPI: PATCH /card { method:"webauthn", assertion }
    CardAPI->>Panda: verify(userId, { authType:"webauthn", assertion })
    Panda-->>CardAPI: success / error
    CardAPI-->>Client: { verification: "OK" } or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding panda signature functionality to the server. However, it uses an emoji which could be considered noise per guidelines.
Linked Issues check ✅ Passed The PR implements panda signature verification across multiple components: GET/PATCH endpoints with SIWE and WebAuthn flows, Panda utility functions (getNonce, verify, verifyPandaSignature), webhook signature validation, and comprehensive test coverage. All changes align with the objective to implement panda dry-run signature verification.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing panda signature verification. The CardDetails and server.ts utility changes ensure encrypted fields are properly validated before decryption, supporting the signature feature implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch signature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for Panda signatures, implementing SIWE and WebAuthn authentication methods for card operations. Key changes include a challenge-response mechanism in the card API, signature verification in transaction hooks, and new utility functions for interacting with Panda signature endpoints. Feedback focuses on handling signature parsing errors more explicitly and externalizing the hardcoded signer address to an environment variable for better maintainability.

Comment thread server/hooks/panda.ts Outdated
Comment thread server/utils/panda.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f5fca28-0eb4-4199-80e0-1f5962737c44

📥 Commits

Reviewing files that changed from the base of the PR and between dfbd4de and e3b5108.

📒 Files selected for processing (6)
  • .changeset/quiet-tigers-sign.md
  • server/api/card.ts
  • server/hooks/panda.ts
  • server/test/api/card.test.ts
  • server/test/utils/panda.test.ts
  • server/utils/panda.ts

Comment thread server/api/card.ts
Comment thread server/api/card.ts
Comment thread server/hooks/panda.ts
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 29, 2026

✅ All tests passed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/card/CardDetails.tsx (1)

41-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear cached PAN/CVC when encrypted fields disappear.

If a previous fetch already populated details, and a later refetch returns a card without encryptedPan/encryptedCvc, this branch does nothing and the old PAN/CVC stay rendered. Reset the local state in the false path before returning.

💡 Suggested fix
   useEffect(() => {
-    if (card?.encryptedPan && card.encryptedCvc) {
-      Promise.all([
-        decrypt(card.encryptedPan.data, card.encryptedPan.iv, card.secret),
-        decrypt(card.encryptedCvc.data, card.encryptedCvc.iv, card.secret),
-      ])
-        .then(([pan, cvc]) => {
-          setDetails({ pan, cvc });
-        })
-        .catch(reportError);
-    }
+    if (!card?.encryptedPan || !card.encryptedCvc) {
+      setDetails({ pan: "", cvc: "" });
+      return;
+    }
+    Promise.all([
+      decrypt(card.encryptedPan.data, card.encryptedPan.iv, card.secret),
+      decrypt(card.encryptedCvc.data, card.encryptedCvc.iv, card.secret),
+    ])
+      .then(([pan, cvc]) => {
+        setDetails({ pan, cvc });
+      })
+      .catch(reportError);
   }, [card]);
♻️ Duplicate comments (1)
server/api/card.ts (1)

695-698: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce the canonical SIWE uri too.

GET /?scope=siwe issues a message with uri: https://${domain}, but PATCH / never checks message.uri. A client can change only the URI, sign it, and still reach { verification: "OK" }.

💡 Suggested fix
-                  if (message.statement !== statement || message.chainId !== chain.id || message.domain !== domain){
+                  if (
+                    message.statement !== statement ||
+                    message.chainId !== chain.id ||
+                    message.domain !== domain ||
+                    message.uri !== `https://${domain}`
+                  ) {
                     return c.json({ code: "bad signature" }, 400);
                   }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 79c4d18c-7f6b-40e4-8f31-150defd9a192

📥 Commits

Reviewing files that changed from the base of the PR and between e3b5108 and 923f790.

📒 Files selected for processing (8)
  • .changeset/quiet-tigers-sign.md
  • server/api/card.ts
  • server/hooks/panda.ts
  • server/test/api/card.test.ts
  • server/test/utils/panda.test.ts
  • server/utils/panda.ts
  • src/components/card/CardDetails.tsx
  • src/utils/server.ts

Comment thread server/api/card.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/card/CardDetails.tsx (1)

41-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset details when the encrypted blobs are absent.

The new guard skips decryption, but it also leaves the previous PAN/CVC in state. If a refetch returns a card without encryptedPan/encryptedCvc, the sheet can keep rendering stale secrets.

🩹 Proposed fix
   useEffect(() => {
     if (card?.encryptedPan && card.encryptedCvc) {
       Promise.all([
         decrypt(card.encryptedPan.data, card.encryptedPan.iv, card.secret),
         decrypt(card.encryptedCvc.data, card.encryptedCvc.iv, card.secret),
       ])
         .then(([pan, cvc]) => {
           setDetails({ pan, cvc });
         })
         .catch(reportError);
+      return;
     }
+    setDetails({ pan: "", cvc: "" });
   }, [card]);
♻️ Duplicate comments (1)
server/api/card.ts (1)

695-707: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Also reject SIWE messages with a mismatched uri.

This path now binds statement, chainId, and domain, but it still accepts a message whose uri differs from the challenge returned by GET /. A client can change that part of the signed consent text and still reach panda.verify(...).

🛡️ Proposed fix
-                      if (m.statement !== statement || m.chainId !== chain.id || m.domain !== domain) {
+                      if (
+                        m.statement !== statement ||
+                        m.chainId !== chain.id ||
+                        m.domain !== domain ||
+                        m.uri !== `https://${domain}`
+                      ) {
                         return false;
                       }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2df97e21-0ca1-4418-92c0-e417dd6a6c01

📥 Commits

Reviewing files that changed from the base of the PR and between 923f790 and adf8f7d.

📒 Files selected for processing (8)
  • .changeset/quiet-tigers-sign.md
  • server/api/card.ts
  • server/hooks/panda.ts
  • server/test/api/card.test.ts
  • server/test/utils/panda.test.ts
  • server/utils/panda.ts
  • src/components/card/CardDetails.tsx
  • src/utils/server.ts

Comment thread server/utils/panda.ts
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.

server: implement panda dry-run signature verification

1 participant