Skip to content

✨ server: encrypted kyc#948

Draft
nfmelendez wants to merge 12 commits intowebhookfrom
encrypted-kyc
Draft

✨ server: encrypted kyc#948
nfmelendez wants to merge 12 commits intowebhookfrom
encrypted-kyc

Conversation

@nfmelendez
Copy link
Copy Markdown
Contributor

@nfmelendez nfmelendez commented Apr 9, 2026

Summary by CodeRabbit

  • New Features

    • KYC endpoints to submit, update, and check application status; card endpoints now block actions if KYC not approved.
    • Organization role field and KYC permission controls added.
  • Documentation

    • Guide for creating encrypted KYC payloads with SIWE and note on requesting KYC permissions.
  • Tests

    • Expanded tests and mocks covering KYC flows, encrypted submissions, permissions, and error mappings.
  • Chores

    • Spelling/config update and multiple release metadata entries.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: d7fa395

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

This PR includes changesets to release 2 packages
Name Type
@exactly/server Patch
@exactly/docs 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 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds KYC application workflow: new authenticated POST/PATCH/GET /application endpoints with SIWE verification and payload hashing (encrypted/plaintext), Panda submit/get/update wrappers and schemas, org role and ACL updates, DB column for organization role, docs for encrypted KYC payload, dependency add, and expanded tests/mocks.

Changes

Cohort / File(s) Summary
Changeset Metadata
.changeset/cuddly-streets-like.md, .changeset/four-numbers-worry.md, .changeset/sharp-squids-push.md, .changeset/upset-seas-sink.md, .changeset/calm-tigers-stop.md
Added multiple changeset files declaring patch releases and documenting KYC-related release notes.
Docs
docs/src/content/docs/organization-authentication.md
Added warning and a TypeScript example demonstrating encrypted KYC payload construction (AES-256-GCM + RSA-OAEP key wrap, Base64 fields, payload hash in SIWE statement).
API: KYC endpoints
server/api/kyc.ts
Introduced POST /application, PATCH /application, GET /application with OpenAPI/valibot schemas, SIWE signature/chainId/statement verification, payload canonicalization/hash, org membership/role checks, Panda submit/update/status integration, and error mapping.
API: Card routes
server/api/card.ts
Added KYC approval gating by calling getApplicationStatus() and expanded 403 responses to include kyc not approved in GET and POST handlers.
Panda utilities
server/utils/panda.ts
Removed default export of API key; added exported functions submitApplication, getApplicationStatus, updateApplication; added valibot schemas (Application, SubmitApplicationRequest, UpdateApplicationRequest, kycStatus) and response validation.
Auth & ACL
server/utils/auth.ts
Added kyc resource to access-control config, granted create to admin/owner, and added optional role field to organization plugin additionalFields.
Database schema
server/database/schema.ts
Added nullable exported role text column to organizations table.
Dependencies
server/package.json
Added canonicalize dependency (^2.1.0) for JSON canonicalization.
Tests
server/test/api/card.test.ts, server/test/api/kyc.test.ts, server/test/e2e.ts
Updated tests to stub/mock getApplicationStatus, added comprehensive KYC endpoint tests (SIWE flows, encrypted/plaintext submission, error mapping, updates/status), adjusted card tests to reflect new KYC gating, and extended e2e panda mock.
Config
cspell.json
Added "ciphertext" to spell-check dictionary.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Client
    participant KYC as KYC API
    participant DB as Database
    participant Auth as Auth Service
    participant Panda as Panda Service

    Client->>KYC: POST /application (SIWE + payload {ciphertext|JSON})
    KYC->>KYC: Parse SIWE, verify signature & chainId
    KYC->>KYC: Compute sha256(ciphertext or canonicalized JSON)
    KYC->>DB: Read credential & org membership
    KYC->>Auth: Verify org role includes kyc:create
    KYC->>Panda: submitApplication(payload, encrypted?)
    Panda-->>KYC: { id, applicationStatus }
    KYC->>DB: Persist credentials.pandaId & source
    KYC-->>Client: { status: applicationStatus }

    Client->>KYC: GET /application
    KYC->>DB: Read credentials.pandaId
    KYC->>Panda: getApplicationStatus(pandaId)
    Panda-->>KYC: { id, applicationStatus, reason? }
    KYC-->>Client: { code:"ok", status, reason }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '✨ server: encrypted kyc' is specific to the changeset, referencing the server package and encrypted KYC feature implementation, but could be more descriptive about the core changes (API endpoints, encryption support, role-based access control).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch encrypted-kyc

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

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 implements a new KYC application submission and management flow, including support for encrypted payloads and organization-based permissions. The review identified potential issues with the current organization membership check, the SIWE message parsing, and the payload hashing logic. I have provided suggestions to improve the robustness of the membership lookup, handle potential parsing errors, and correct the hashing implementation.

Comment thread server/api/kyc.ts
Comment thread server/api/kyc.ts Outdated
Comment thread server/api/kyc.ts
@sentry
Copy link
Copy Markdown

sentry bot commented Apr 9, 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: 5

Caution

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

⚠️ Outside diff range comments (1)
server/utils/auth.ts (1)

20-64: ⚠️ Potential issue | 🟡 Minor

Remove unused delete and read kyc actions or implement permission enforcement for them.

Lines 20 define kyc: ["create", "delete", "read"], but only create is granted to admin/owner roles (lines 58, 63), and no KYC endpoints enforce these actions via hasPermission(). The global action definitions are unused dead code—either remove delete and read to match the intended role permissions, or add permission checks to enforce them in KYC endpoints (currently they bypass the access control system).


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 12eeea5f-41ad-44e3-98ac-07be8bf882b0

📥 Commits

Reviewing files that changed from the base of the PR and between 2598ece and 125a498.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • .changeset/cuddly-streets-like.md
  • .changeset/four-numbers-worry.md
  • .changeset/sharp-squids-push.md
  • .changeset/upset-seas-sink.md
  • cspell.json
  • docs/src/content/docs/organization-authentication.md
  • server/api/card.ts
  • server/api/kyc.ts
  • server/database/schema.ts
  • server/package.json
  • server/test/api/card.test.ts
  • server/test/api/kyc.test.ts
  • server/test/e2e.ts
  • server/utils/auth.ts
  • server/utils/panda.ts

Comment thread server/api/card.ts
Comment thread server/api/kyc.ts
Comment thread server/api/kyc.ts Outdated
Comment thread server/api/kyc.ts Outdated
Comment thread server/utils/panda.ts
@cruzdanilo cruzdanilo changed the title Encrypted kyc ✨ server: encrypted kyc Apr 9, 2026
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: 7

♻️ Duplicate comments (1)
server/test/api/kyc.test.ts (1)

1440-1440: ⚠️ Potential issue | 🟠 Major

These test signatures still hash the wrong bytes.

This is the same canonicalize bug flagged earlier: canonicalize() already returns the canonical JSON string, so wrapping it in JSON.stringify() changes the bytes being signed. The plaintext application tests no longer match the server-side hash computation.

💡 Proposed fix
- sha256(Buffer.from(JSON.stringify(canonicalize(applicationPayload)), "utf8"))
+ sha256(Buffer.from(canonicalize(applicationPayload) ?? "", "utf8"))

Also applies to: 1504-1504, 1557-1557, 1739-1739, 1776-1776, 1813-1813, 1842-1842


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b887372-b7ce-4860-af1c-ac3e93fea439

📥 Commits

Reviewing files that changed from the base of the PR and between 125a498 and aeaa4ee.

📒 Files selected for processing (10)
  • .changeset/cuddly-streets-like.md
  • .changeset/sharp-squids-push.md
  • .changeset/upset-seas-sink.md
  • cspell.json
  • docs/src/content/docs/organization-authentication.md
  • server/api/kyc.ts
  • server/database/schema.ts
  • server/test/api/kyc.test.ts
  • server/utils/auth.ts
  • server/utils/panda.ts

Comment thread server/api/kyc.ts
Comment thread server/api/kyc.ts
Comment thread server/api/kyc.ts
Comment thread server/database/schema.ts
Comment thread server/utils/panda.ts Outdated
Comment thread server/utils/panda.ts
Comment thread server/utils/panda.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: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c1ad266c-f3b0-4847-9461-6c6e351826af

📥 Commits

Reviewing files that changed from the base of the PR and between aeaa4ee and 7fafac5.

📒 Files selected for processing (10)
  • .changeset/cuddly-streets-like.md
  • .changeset/sharp-squids-push.md
  • .changeset/upset-seas-sink.md
  • cspell.json
  • docs/src/content/docs/organization-authentication.md
  • server/api/kyc.ts
  • server/database/schema.ts
  • server/test/api/kyc.test.ts
  • server/utils/auth.ts
  • server/utils/panda.ts

Comment thread docs/src/content/docs/organization-authentication.md
Comment thread server/api/kyc.ts Outdated
Comment thread server/test/api/kyc.test.ts Outdated
Comment thread server/utils/auth.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: 2

♻️ Duplicate comments (2)
server/api/card.ts (1)

334-338: ⚠️ Potential issue | 🟠 Major

Move the KYC lookup behind the local already created branch.

This preflight now runs before we decide whether the user already has an active/frozen card, so duplicate POST /card calls can regress from 400 { code: "already created" } to 403/500 when Panda is denied, stale, or unavailable. Putting getApplicationStatus(...) inside the existing try after if (cardCount > 0) keeps the endpoint idempotent and reuses the current noUser(...) mapping for stale pandaIds.

♻️ Proposed fix
-          const kyc = await getApplicationStatus(credential.pandaId);
-          if (kyc.applicationStatus !== "approved") {
-            return c.json({ code: "kyc not approved" }, 403);
-          }
-
           let isUpgradeFromPlatinum = credential.cards.some(
             ({ status, productId }) => status === "DELETED" && productId === PLATINUM_PRODUCT_ID,
           );
@@
           }
           if (cardCount > 0) return c.json({ code: "already created" }, 400);
           try {
+            const kyc = await getApplicationStatus(credential.pandaId);
+            if (kyc.applicationStatus !== "approved") return c.json({ code: "kyc not approved" }, 403);
             const card = await createCard(credential.pandaId, SIGNATURE_PRODUCT_ID);
server/utils/panda.ts (1)

425-431: ⚠️ Potential issue | 🟡 Minor

new Date(...) still accepts impossible calendar dates.

Values like 2026-02-31 pass this check because JavaScript normalizes overflowed dates instead of rejecting them. Round-trip the parsed year/month/day with Date.UTC(...) so only real calendar dates reach Panda.

🛠️ Proposed fix
   birthDate: pipe(
     string(),
     regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD format"),
     check((value) => {
-      const date = new Date(value);
-      return !Number.isNaN(date.getTime());
+      const [year, month, day] = value.split("-").map(Number);
+      if (year === undefined || month === undefined || day === undefined) return false;
+      const date = new Date(Date.UTC(year, month - 1, day));
+      return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
     }, "must be a valid date"),
Does JavaScript `new Date("2026-02-31")` normalize overflowed ISO dates instead of returning `Invalid Date`?

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1fb0a294-b798-4017-b6f4-d185461cd025

📥 Commits

Reviewing files that changed from the base of the PR and between 7fafac5 and 9058e28.

📒 Files selected for processing (15)
  • .changeset/calm-tigers-stop.md
  • .changeset/cuddly-streets-like.md
  • .changeset/four-numbers-worry.md
  • .changeset/sharp-squids-push.md
  • .changeset/upset-seas-sink.md
  • cspell.json
  • docs/src/content/docs/organization-authentication.md
  • server/api/card.ts
  • server/api/kyc.ts
  • server/database/schema.ts
  • server/test/api/card.test.ts
  • server/test/api/kyc.test.ts
  • server/test/e2e.ts
  • server/utils/auth.ts
  • server/utils/panda.ts

Comment thread server/api/kyc.ts
Comment thread server/test/api/kyc.test.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: 1

♻️ Duplicate comments (1)
docs/src/content/docs/organization-authentication.md (1)

226-327: ⚠️ Potential issue | 🟡 Minor

Add the actual /application submit example with encrypted: "true" header.

The sample currently stops at payload logging (Line 323). For encrypted KYC, readers also need the submission contract; otherwise this is easy to copy into a failing integration.

📄 Suggested doc patch
 owner.signMessage({ message })
-  .then((signature) => {
+  .then(async (signature) => {
     const verify = {
       message,
       signature,
       walletAddress: owner.address,
       chainId,
     };
     const { hash, ...payload } = encryptedPayload;
-    console.log("application payload", { ...payload, verify });
+    const applicationPayload = { ...payload, verify };
+    console.log("application payload", applicationPayload);
+
+    const response = await fetch("https://sandbox.exactly.app/api/application", {
+      method: "POST",
+      headers: { "content-type": "application/json", encrypted: "true" },
+      body: JSON.stringify(applicationPayload),
+    });
+    console.log("application submit status", response.status);
   })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8500d69f-ac57-471b-95c2-d4e1e46f3edb

📥 Commits

Reviewing files that changed from the base of the PR and between 9058e28 and 98f2903.

📒 Files selected for processing (11)
  • .changeset/calm-tigers-stop.md
  • .changeset/cuddly-streets-like.md
  • .changeset/sharp-squids-push.md
  • .changeset/upset-seas-sink.md
  • cspell.json
  • docs/src/content/docs/organization-authentication.md
  • server/api/kyc.ts
  • server/database/schema.ts
  • server/test/api/kyc.test.ts
  • server/utils/auth.ts
  • server/utils/panda.ts

Comment thread server/test/api/kyc.test.ts
@nfmelendez nfmelendez force-pushed the webhook branch 2 times, most recently from 8c026bc to fc57eea Compare April 15, 2026 15:36
@nfmelendez nfmelendez force-pushed the webhook branch 2 times, most recently from c985bf7 to 43dc358 Compare April 15, 2026 19:26
@nfmelendez nfmelendez force-pushed the encrypted-kyc branch 2 times, most recently from b095e06 to 987fa1c Compare April 15, 2026 20:39
@nfmelendez
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

3 participants