Skip to content

Comments

✨ server: track user source attribution#674

Merged
cruzdanilo merged 2 commits intomainfrom
source
Jan 23, 2026
Merged

✨ server: track user source attribution#674
cruzdanilo merged 2 commits intomainfrom
source

Conversation

@nfmelendez
Copy link
Contributor

@nfmelendez nfmelendez commented Jan 22, 2026


Open with Devin

Summary by CodeRabbit

  • New Features

    • Credentials now capture an optional client header (validated with a max length) and store a source tag; a default source is applied when absent for both WebAuthn and SIWE sign-ups.
  • Tests

    • Added end-to-end tests for registration and authentication flows verifying credential creation and source-tag behavior.
  • Chores

    • Release changesets added to mark a patch bump.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2026

🦋 Changeset detected

Latest commit: 3430400

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

@gemini-code-assist
Copy link

Summary of Changes

Hello @nfmelendez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new capability to track and store the origin or source of user accounts during their initial registration and subsequent authentication. By capturing an optional client_fid header, the system can now record where a user originated from, which is vital for understanding user acquisition, performing analytics, and enhancing fraud detection mechanisms. The changes span database schema modifications, API endpoint enhancements, and updates to core credential management logic, all supported by comprehensive new tests.

Highlights

  • Database Schema Update: The credentials database table has been updated to include a new source column, allowing for the storage of user attribution information.
  • User Source Attribution: Authentication and registration API endpoints now accept an optional client_fid header. The value of this header, if present, is used to attribute the user's source.
  • Refactored Credential Creation: The createCredential utility function has been refactored to accept an options object, which now includes the source field. This ensures the source is consistently captured and stored.
  • Integration with Customer Service: The user's source (either from the client_fid header or defaulting to 'EXA') is now passed to the customer service (likely for analytics or fraud detection) during credential creation.
  • Enhanced Test Coverage: New and updated test cases have been added for both authentication and registration flows to verify the correct handling, storage, and default behavior of user source attribution.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds optional source attribution to credentials: new source DB column, reads Client-Fid header in authentication and registration flows, updates createCredential to accept/persist an optional source, and extends tests/mocks to cover the behavior.

Changes

Cohort / File(s) Summary
Changesets
\.changeset/better-sites-lead.md, \.changeset/swift-beers-taste.md
Two new changeset files signaling patch bumps for @exactly/server with notes about adding/tracking credential source.
Database Schema
server/database/schema.ts
Added source: text("source") column to the credentials table.
API — Authentication
server/api/auth/authentication.ts
Validates optional Client-Fid header (maxLength 36) and passes its value to createCredential as source.
API — Registration
server/api/auth/registration.ts
Validates optional Client-Fid header and calls createCredential(..., { webauthn, source }).
Credential Utility
server/utils/createCredential.ts
Signature changed from createCredential(c, credentialId, webauthn?) to createCredential(c, credentialId, options?) where options may include webauthn and source; persists source and adds a source tag when calling Sardine.
Tests — Authentication
server/test/api/auth.test.ts
Added SIWE credential creation tests verifying Client-Fid source tagging; updated SIWE/viem mocks and expectations.
Tests — Registration
server/test/api/registration.test.ts
New WebAuthn registration tests asserting credential source persistence and customer tagging; includes WebAuthn/Redis/Sardine mocks.
Mocks
server/test/mocks/sardine.ts
Introduced top-level customer mock and default export; replaced inline mock usage.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as Auth/Registration API
    participant DB as Credentials DB
    participant Sardine as Sardine Service

    Client->>API: POST /auth or /registration (credential data + optional Client-Fid)
    API->>API: Validate headers (Client-Fid maxLength 36)
    API->>API: Call createCredential(c, credentialId, { webauthn?, source: Client-Fid })
    API->>DB: INSERT credential (includes source)
    DB-->>API: Insert result (credential id)
    API->>Sardine: customer(...) with tags including source
    Sardine-->>API: Sardine response
    API-->>Client: 200 OK (credential + customer result)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ✨ server: integrate sardine #594: Modifies server/utils/createCredential.ts and Sardine customer call; overlaps with this PR's changes to createCredential and customer tagging.

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 pull request title accurately describes the main objective: tracking user source attribution across authentication and registration flows.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch source

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

@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 successfully implements user source attribution by introducing a "source" field in the credentials database and integrating it into the authentication and registration flows. The changes are well-tested with new unit tests covering both SIWE and WebAuthn scenarios, including the default source value. The refactoring of the createCredential function to accept an options object is a clean approach, and the update to the sardine customer tracking with the new source tag is correctly handled. The suggested additions of maxLength constraints for client_fid headers are good practices for data integrity.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional flags.

Open in Devin Review

@sentry
Copy link

sentry bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.88%. Comparing base (907ade0) to head (3430400).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
server/utils/createCredential.ts 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #674      +/-   ##
==========================================
- Coverage   63.66%   61.88%   -1.79%     
==========================================
  Files         169      169              
  Lines        5882     5289     -593     
  Branches     1753     1496     -257     
==========================================
- Hits         3745     3273     -472     
+ Misses       1964     1844     -120     
+ Partials      173      172       -1     
Flag Coverage Δ
e2e 60.06% <40.00%> (-2.67%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cruzdanilo
Copy link
Member

@cursor review

@cursor
Copy link

cursor bot commented Jan 22, 2026

PR Summary

User source attribution

  • Adds optional client_fid header validation in authentication and registration APIs and passes it through during SIWE/WebAuthn flows
  • Persists source on new credentials by extending createCredential(options: { source; webauthn }) and adding source column to credentials table
  • Sends source to Sardine via customer call as a tags entry; defaults to "EXA" when header is absent
  • Updates SIWE credential creation paths to include source and keeps existing behavior for WebAuthn
  • Adds tests for both flows and updates mocks to assert source tagging and DB persistence

Written by Cursor Bugbot for commit 81847de. This will update automatically on new commits. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link

@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: 6

Caution

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

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

29-63: Keep source defaults consistent between DB and external tagging.

Line 45 stores raw options?.source while Line 61 defaults to "EXA" for Sardine tags. This can leave DB rows with null while external tagging shows "EXA". Consider deriving a single source value and reusing it.

🛠️ Suggested fix
   const { x, y } = decodePublicKey(publicKey);
   const account = deriveAddress(exaAccountFactoryAddress, { x, y });
+  const source = options?.source ?? "EXA";

   setUser({ id: account });
   const expires = new Date(Date.now() + AUTH_EXPIRY);
   await database.insert(credentials).values([
     {
       account,
       id: credentialId,
       publicKey,
       factory: exaAccountFactoryAddress,
       transports: options?.webauthn?.transports,
       counter: options?.webauthn?.counter,
-      source: options?.source,
+      source,
     },
   ]);
   await Promise.all([
     setSignedCookie(c, "credential_id", credentialId, authSecret, {
       expires,
       httpOnly: true,
       ...(domain === "localhost"
         ? { sameSite: "lax", secure: false }
         : { domain, sameSite: "none", secure: true, partitioned: true }),
     }),
     updateWebhookAddresses(webhookId, [account]).catch((error: unknown) => captureException(error)),
     customer({
       flow: { name: "signup", type: "signup" },
       customer: {
         id: credentialId,
-        tags: [{ name: "source", value: options?.source ?? "EXA", type: "string" }],
+        tags: [{ name: "source", value: source, type: "string" }],
       },
     }).catch((error: unknown) => captureException(error, { level: "error" })),
   ]);
🤖 Fix all issues with AI agents
In @.changeset/better-sites-lead.md:
- Around line 1-5: There are duplicate patch changesets for the same package
"@exactly/server"; consolidate them by merging the two patch entries into a
single changeset (combine the human-readable descriptions like "add source to
credentials database table" and any other notes) and remove the redundant
changeset file so only one .changeset declares a patch for "@exactly/server";
ensure the resulting changeset retains all relevant information and uses the
same YAML header format as the existing changesets.

In `@server/api/auth/authentication.ts`:
- Line 249: The handler is reading client_fid directly via
c.req.header("client_fid") instead of using the validated header produced by the
vValidator(...) middleware; replace that raw access with the validated value
provided by the middleware (use the middleware-provided validated header object
– e.g. the request/context validated store produced by vValidator, such as
c.req.valid("header") or c.validated.header – to read client_fid) so the route
uses the validated header data rather than c.req.header("client_fid").
- Line 327: Update the valibot header schema in this file to require a numeric
string for the Client-Fid header (use pipe(string(), regex(/^\d+$/))) so the
header is validated as a Farcaster FID; locate the headers/schema used to
validate request headers in this file (the schema that governs
c.req.header("client_fid")), replace the optional/string validation with
pipe(string(), regex(/^\d+$/)), and keep the existing usage when passing
c.req.header("client_fid") into createCredential(assertion.id, { source: ... })
so only numeric FIDs are accepted.

In `@server/api/auth/registration.ts`:
- Line 254: The code validates the "Client-Fid" header via vValidator("header",
optional(object({ client_fid: optional(string()) }))) but later directly reads
c.req.header("Client-Fid"); change the latter to use the validated header value
produced by the vValidator (the validated client_fid field) instead of
re-reading the raw header so normalization/validation are preserved—replace
usages of c.req.header("Client-Fid") with the validated client_fid from the
validator result (the same property name client_fid returned by the header
validation).

In `@server/test/api/auth.test.ts`:
- Around line 99-122: The test currently asserts only the credential id after
creating via siwe; update the test (in the "creates a credential using siwe"
case) to also fetch the created credential via
database.query.credentials.findFirst (already used) and assert that the
credential's source field is undefined when no client_fid is provided; locate
the call that checks credentials.id (using credentials.id symbol) and add an
expectation that credential?.source is undefined to mirror the registration
tests and ensure default source is not set.

In `@server/test/api/registration.test.ts`:
- Around line 58-82: The test "creates a credential using webauthn (defaults to
EXA)" asserts the customer tag but doesn't verify the stored credential source;
update the test to assert the credential's source is set to "EXA" by extending
the existing mock verification on customer (or adding a separate expectation) to
include credential: expect.objectContaining({ source: "EXA" }) (or use
expect.any/more specific shape if the code stores it under a different key),
referencing the existing expect(customer).toHaveBeenCalledWith call so the
database/save payload includes credential.source === "EXA".

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 4 additional flags in Devin Review.

Open in Devin Review

@cursor
Copy link

cursor bot commented Jan 23, 2026

Unable to authenticate your request. Please make sure to connect your GitHub account to Cursor. Go to Cursor

@nfmelendez
Copy link
Contributor Author

@cursor review

@cursor
Copy link

cursor bot commented Jan 23, 2026

Skipping Bugbot: Unable to authenticate your request. Please make sure Bugbot is properly installed and configured for this repository.

@nfmelendez
Copy link
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 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.

Copy link

@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 (3)
server/database/schema.ts (1)

23-36: Add migration for credentials.source and configure drizzle output directory.

Schema change to credentials.source lacks a corresponding migration file. Additionally, drizzle.config.ts is missing the out parameter specifying where migrations should be generated. Update the config to include:

out: "server/database/migrations"

Then run drizzle-kit generate to generate the required migration file for the source column addition.

server/api/auth/registration.ts (1)

303-361: Use validated header value instead of re-reading the raw header.

Line 255 validates "Client-Fid", but Line 361 reads the header directly via c.req.header("Client-Fid"). This bypasses the validation and is inconsistent with the established pattern (e.g., how session_id is accessed via c.req.valid("cookie")). Use the validated header value for consistency.

♻️ Suggested refactor
     async (c) => {
       const attestation = c.req.valid("json");
       setContext("auth", attestation);
       const { session_id: sessionId } = c.req.valid("cookie");
+      const header = c.req.valid("header");
       const challenge = await redis.get(sessionId);
       if (!challenge) return c.json({ code: "no registration", legacy: "no registration" }, 400);
       // ... (rest of validation logic)
-      const result = await createCredential(c, attestation.id, { webauthn, source: c.req.header("Client-Fid") });
+      const result = await createCredential(c, attestation.id, { webauthn, source: header?.["Client-Fid"] });
server/utils/createCredential.ts (1)

37-46: Add test assertion to verify counter defaults to 0 in SIWE registration.

The schema correctly defines counter: integer("counter").notNull().default(0). When options?.webauthn is undefined in the SIWE flow, counter will be undefined and Drizzle should apply the default. However, the SIWE tests in server/test/api/auth.test.ts only validate the source field; they should also assert that counter is set to 0 to confirm the default works as expected.

🤖 Fix all issues with AI agents
In `@server/api/auth/authentication.ts`:
- Line 250: The header validation uses the key "Client-Fid" which Hono
normalizes to lowercase so the rule never matches; update the
vValidator('header', ...) schema entries (the object containing "Client-Fid") to
use "client-fid" (lowercase) so the optional(pipe(string(), maxLength(36)))
constraint on client ID is actually enforced — apply this change for both
occurrences referenced in the diff (the vValidator header schemas around the
authentication logic).
♻️ Duplicate comments (1)
server/test/api/auth.test.ts (1)

117-122: Consider verifying source is undefined for the default case.

This test verifies the customer call includes "EXA" as the default source but only asserts credential?.id. For consistency with the "creates a credential with source" test and to ensure the database correctly stores undefined when no Client-Fid is provided, add an assertion for source.

♻️ Suggested addition
     const credential = await database.query.credentials.findFirst({
       where: eq(credentials.id, id),
-      columns: { id: true },
+      columns: { id: true, source: true },
     });
     expect(credential?.id).toBe(id);
+    expect(credential?.source).toBeUndefined();
   });

@cruzdanilo cruzdanilo merged commit 3430400 into main Jan 23, 2026
12 of 15 checks passed
@cruzdanilo cruzdanilo deleted the source branch January 23, 2026 18:48
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.

2 participants