Skip to content

✨ server: exachat v1 - #1297

Open
nfmelendez wants to merge 3 commits into
subdomainfrom
mastra
Open

nfmelendez wants to merge 3 commits into
subdomainfrom
mastra

Conversation

@nfmelendez

@nfmelendez nfmelendez commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

summary

associate exa accounts to whatsapp ids through a validation code flow, and wire the chat hook and worker to it.

stacked on #1296.

changes

  • add a `whatsappId` column to credentials
  • add the chat association endpoint: preflight an association and send or confirm a validation code
  • send validation codes through the whatsapp utility and expose the chat api in the bin entrypoint
  • update the chat hook and worker to resolve accounts by whatsapp id
  • add api, worker, and eval tests, plus a server changeset

test plan

  • `pnpm nx test server`
  • on base-sepolia, link a whatsapp id from the app and confirm the chat worker answers for that account

🤖 Generated with Claude Code

https://claude.ai/code/session_01WRNy8HTnB5tWJWsg3DBBb1

Summary by CodeRabbit

  • New Features

    • Added WhatsApp chat association, including availability checks and verification codes.
    • Chat conversations now support account-aware assistance for cards, transfers, support, and verification.
    • Added localized sign-in and action links tailored to the customer’s account status.
    • Chat history is retained per WhatsApp number for more consistent conversations.
  • Improvements

    • WhatsApp messages now use business phone number context to deliver more relevant responses.

@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6fe4156

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

This PR includes changesets to release 4 packages
Name Type
@exactly/infra Minor
@exactly/server Patch
@exactly/mobile Patch
@exactly/common 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

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Walkthrough

Adds WhatsApp credential association, encrypted chat tokens, account-aware chat processing, database-backed conversation context, and runtime configuration for API and worker services.

Changes

WhatsApp chat integration

Layer / File(s) Summary
Association API and token contract
server/database/schema.ts, server/utils/whatsapp.ts, server/api/chat.ts, server/api/index.ts, server/test/api/chat.test.ts, server/test/utils/whatsapp.test.ts
Credentials now store unique WhatsApp IDs. The API supports token preflight, code delivery, cooldowns, verification, and reassignment. WhatsApp tokens use encrypted claims with audience, issuer, expiration, and subject validation.
Account-aware chat worker
server/workers/chat/*, server/hooks/chat.ts, server/test/workers/chat.test.ts, server/test/workers/chat.eval.ts, server/test/hooks/chat.test.ts
Chat jobs now carry phoneNumberId. The worker loads account and card context from Postgres, stores conversation history in Redis, exposes account-aware tools, composes localized links, and evaluates tool, language, formatting, latency, and trajectory behavior.
Runtime and deployment wiring
infra/utils/modules.ts, server/index.ts, server/api/bin.ts, server/workers/chat/bin.ts, server/script/openapi.ts, server/test/api/bin.test.ts, server/test/workers/bin.test.ts, server/package.json, server/vitest.config.mts, .changeset/warm-otters-associate.md
API and worker bootstraps resolve WhatsApp, Persona, Postgres, and Redis dependencies. Service configuration and test environments provide the required secrets and variables. The package receives a patch changeset.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~100 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant WhatsApp
  participant ChatWorker
  participant Database
  participant Redis
  participant Persona
  WhatsApp->>ChatWorker: Deliver message with phoneNumberId
  ChatWorker->>Database: Load credential and card context
  ChatWorker->>Redis: Load conversation memory
  ChatWorker->>Persona: Resolve inquiry data
  ChatWorker->>WhatsApp: Send generated reply and links
Loading

Suggested reviewers: mainqueg

Merge Risk: 🔵 Low · up to 9b98b

The change is broadly mergeable, but several localized issues can cause inconsistent chat guidance, temporary association delays, or reduced protection for active validation codes.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the new server-side Exa chat integration, which is related to the main changes. It does not specify the WhatsApp association flow or worker integration, but it remains sufficientl…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mastra
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch mastra

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.

@nfmelendez nfmelendez changed the title ✨ server: associate accounts to whatsapp ids ✨ server: exachat v1 Sep 9, 2026
@nfmelendez
nfmelendez added this pull request to stack #1265 September 9, 2026 12:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95a8fb9d6b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/index.ts
Comment on lines +53 to +56
const chat = createWhatsapp({
from: parse(pipe(string("whatsapp id"), nonEmpty("whatsapp id")), env.WHATSAPP_PHONE_NUMBER_ID),
key: parse(pipe(string("chat"), nonEmpty("chat")), env.CHAT_IDENTITY_KEY),
token: parse(pipe(string("whatsapp token"), nonEmpty("whatsapp token")), env.WHATSAPP_ACCESS_TOKEN),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Provision the chat variables for the monolith

In the DigitalOcean deployment path, .github/workflows/server-deploy.yaml still deploys the image whose server/Dockerfile starts dist/index.cjs, but .do/app.yaml provisions none of WHATSAPP_PHONE_NUMBER_ID, CHAT_IDENTITY_KEY, or WHATSAPP_ACCESS_TOKEN. Because these three values are parsed eagerly here with nonEmpty, that server exits during module initialization in every DigitalOcean environment unless the app specification is updated alongside this change.

Useful? React with 👍 / 👎.

Comment on lines +254 to +255
associate: {
path: "/whatsapp",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the route targeted by association links

The association tool sends users to /whatsapp, but a repo-wide inspection of the reviewed tree (git ls-tree -r 26db2a23) finds no matching Expo route or redirect under src/app; the existing +not-found route explicitly reports that the link does not match a screen. Consequently, every generated association or move link reaches the not-found page, so users cannot call the new API or complete the validation flow.

Useful? React with 👍 / 👎.

Comment thread server/database/schema.ts
bridgeId: text("bridge_id"),
source: text("source"),
salt: text("salt").notNull().default(zeroAddress),
whatsappId: text("whatsapp_id"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Commit the WhatsApp schema migration

This adds a column and unique index to the Drizzle schema without any migration file, while the deployment workflow does not run db:push. Existing databases therefore retain the old credentials table, and the new API and worker queries that select or update whatsapp_id fail with a missing-column error after deployment. Commit and deploy the generated migration with this schema change. .agents/rules/server.mdL28-L32

Useful? React with 👍 / 👎.

Comment thread server/api/chat.ts
Comment on lines +40 to +43
responses: {
200: { description: "The id is available to associate." },
400: { description: "Bad token, associated with another credential, or this credential already has one." },
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Declare schemas for the chat responses

Both new operations describe response statuses without defining any content schemas, and the handlers return unvalidated object literals rather than values checked with satisfies InferOutput. The generated OpenAPI document consequently cannot describe any of the success or error bodies, and response-shape drift will not be caught at compile time; define reusable Valibot response schemas, expose them through resolver(...), and validate each returned body against them. .agents/rules/server.mdL12-L14

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.61017% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.40%. Comparing base (7834d43) to head (9b98b8d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/workers/chat/bin.ts 71.42% 2 Missing ⚠️
server/workers/chat/worker.ts 97.05% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff               @@
##           subdomain    #1297       +/-   ##
==============================================
- Coverage      88.73%   72.40%   -16.33%     
==============================================
  Files             98      300      +202     
  Lines           4766    11361     +6595     
  Branches        1283     3767     +2484     
==============================================
+ Hits            4229     8226     +3997     
- Misses           290     2899     +2609     
+ Partials         247      236       -11     
Flag Coverage Δ
e2e 46.50% <16.66%> (?)

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

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

(proposalManager.delay as Record<number, number>)[chain.id] ?? proposalManager.delay.default * 1000,

P2 Badge Scale chain-specific mutex delays to milliseconds

On Anvil (chain.id === 31337), the lookup returns 1, and operator precedence applies * 1000 only to the default fallback, so withTimeout receives 1 ms rather than the intended 1 second. The related tests and mining code explicitly treat proposalManager.delay[anvil.id] as seconds by multiplying it by 1000; consequently, overlapping authorization, business KYC, or card requests sharing this mutex can time out almost immediately in local and e2e environments. Apply the conversion after selecting either delay value.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fe41560ad

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .do/app.yaml
Comment on lines +28 to +29
- domain: business.${{ env.APP_DOMAIN }}
type: ALIAS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Permit API calls from the business alias

On the newly enabled base-sepolia and sandbox alias, the served bundle still targets https://${APP_DOMAIN}/api (the primary host in src/utils/server.ts), while server/api/index.ts allows CORS and CSRF only from that primary appOrigin. A browser opened at business.${APP_DOMAIN} therefore makes cross-origin API requests whose origin is rejected, so authentication and onboarding cannot start from this alias; add the business origin or make the client use the alias's same-origin API.

Useful? React with 👍 / 👎.

Comment thread server/api/auth/registration.ts Outdated
tags: ["Credential"],
validateResponse: true,
}),
vValidator("header", optional(object({ "account-type": optional(literal("business")) }))),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Send the account type from the business client

Business registration depends on this optional header being present in both challenge requests, but the production client in src/utils/server.ts only adds Client-Platform and Client-Fid, and its registration/authentication calls never send account-type. Thus, even after the business alias can reach the API, registrations initiated by the bundled app store a challenge without accountType and call accountSalt(undefined), creating an ordinary zero-salt personal account instead of a business account.

Useful? React with 👍 / 👎.

Comment thread server/api/kyc.ts
Comment on lines +565 to +567
const isBusiness = c.req.valid("header")?.["account-type"] === "business";
const credentialId = c.req.valid("cookie").credentialId;
if (isBusiness) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive KYB routing from the stored credential

For an authenticated business credential, omitting the optional account-type header makes isBusiness false and sends a valid personal verify payload through the individual application path, which then stores an individual Panda ID. Subsequent business approval calls finalizeBusinessApproval, sees that stored ID is not a user of the company, and throws company user not found, leaving the account unable to finish onboarding; load the credential salt before branching and reject header/type mismatches rather than trusting this per-request header.

Useful? React with 👍 / 👎.

Comment thread server/utils/persona.ts
Comment on lines 324 to 326
async function getPendingInquiryTemplate(referenceId: string, scope: AccountScope) {
if (scope === "business") return (await getAccount(referenceId, scope)) ? undefined : PANDA_BUSINESS_TEMPLATE;
const unknownAccount = await getUnknownAccount(referenceId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Check inquiry status before marking business KYC complete

A business inquiry is created with Persona's auto-create-account option, so its business account exists while the inquiry is still created, pending, or under review. This branch treats that mere account existence as no pending template; consequently GET /kyc?scope=business takes the no-template path in server/api/kyc.ts and returns { code: "ok" } before verification is approved. Determine completion from the business inquiry status instead of account existence.

Useful? React with 👍 / 👎.

Comment thread server/package.json
"db:push": "drizzle-kit push",
"e2e": "tsx script/e2e.ts",
"eval:chat": "tsx test/workers/chat.eval.ts",
"eval:chat": "tsx --env-file=.env test/workers/chat.eval.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the mandatory .env file from the chat eval

eval:chat now passes Node's --env-file=.env; the runtime's --help says this option loads the supplied file, and Node exits with status 9 when it is absent. This turns the previously environment-agnostic eval target into a command that cannot run in the repository's required zero-config setup, where .env files are explicitly forbidden; leave secret injection to the caller rather than requiring this file.

AGENTS.md reference: AGENTS.md:L14-L14

Useful? React with 👍 / 👎.

Comment on lines +287 to +288
const input = code ? (
<BRCodeField value={value} name={code.ownerName} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep a way to edit a parsed BR Code

After a pasted or scanned value becomes parseable, this branch replaces the editable FieldInput with BRCodeField, whose only action is show or hide. If the user scanned the wrong valid code or needs to correct even one character, they cannot clear or replace it and must abandon the entire recipient flow; retain an editable input or add an edit/clear action.

Useful? React with 👍 / 👎.

Comment on lines +151 to +153
if (job.data.origin === "activity" && pending.length > 0) {
await job.updateData({ ...job.data, assets: pending });
throw new Error(NO_BALANCE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enqueue credit after partially successful poke jobs

When one activity job contains multiple received assets, a successful poke can be followed by an asset whose balance is already zero. This branch then replaces the job data with only the pending assets and throws before the credit enqueue below; every retry therefore forgets the successful collateral poke, and if the zero-balance asset exhausts the retries, an existing card is never switched to credit mode. Enqueue credit whenever poked is true before retrying the remaining assets.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f2d41147-ec74-47dc-81e0-734315ce2762

📥 Commits

Reviewing files that changed from the base of the PR and between 7834d43 and 9b98b8d.

📒 Files selected for processing (23)
  • .changeset/warm-otters-associate.md
  • infra/utils/modules.ts
  • server/api/bin.ts
  • server/api/chat.ts
  • server/api/index.ts
  • server/database/schema.ts
  • server/hooks/chat.ts
  • server/index.ts
  • server/package.json
  • server/script/openapi.ts
  • server/test/api/api.test.ts
  • server/test/api/bin.test.ts
  • server/test/api/chat.test.ts
  • server/test/hooks/chat.test.ts
  • server/test/utils/whatsapp.test.ts
  • server/test/workers/bin.test.ts
  • server/test/workers/chat.eval.ts
  • server/test/workers/chat.test.ts
  • server/utils/whatsapp.ts
  • server/vitest.config.mts
  • server/workers/chat/bin.ts
  • server/workers/chat/job.ts
  • server/workers/chat/worker.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread server/api/chat.ts
Comment on lines +94 to +100
// cspell:ignore cooldown
return c.json({ code: "too soon" }, 429);
}
const code = String(randomInt(0, 1_000_000)).padStart(6, "0");
await redis.set(`chat:${credentialId}`, JSON.stringify({ whatsappId, code }), "PX", 10 * 60_000);
await chat.send(whatsappId, `${code} is your Exa validation code. It expires in 10 minutes.`);
return c.json({ code: "sent" }, 200);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- route and surrounding definitions ---'
cat -n server/api/chat.ts | sed -n '1,145p'
printf '%s\n' '--- chat route registration and send-code callers ---'
rg -n -C 4 'chatRoute|/chat|chat\.send|send\(whatsappId|chat:cooldown|chat:\$\{credentialId\}' server --glob '!node_modules'
printf '%s\n' '--- sender definitions and error handling ---'
rg -n -C 5 'function createWhatsapp|const createWhatsapp|createWhatsapp|async send|send\(.*whatsapp|catch \(.*\)|onError' server --glob '!node_modules'

Repository: exactly/exa

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- WhatsApp sender implementation ---'
wc -l server/utils/whatsapp.ts
cat -n server/utils/whatsapp.ts
printf '%s\n' '--- API error handling ---'
cat -n server/supervise.ts | sed -n '1,85p'
printf '%s\n' '--- chat API tests ---'
cat -n server/test/api/chat.test.ts | sed -n '1,260p'
printf '%s\n' '--- client-side chat API references ---'
rg -n -C 4 'chat/(?:\\?|\"|\\x27)|/chat|too soon|no verification|code.*sent|validation code' --glob '!node_modules' --glob '!dist' --glob '!build' . | head -n 300

Repository: exactly/exa

Length of output: 35859


Clear the verification state when chat.send fails.

In server/api/chat.ts, the authenticated token branch stores chat:cooldown:${whatsappId} for 60 seconds and chat:${credentialId} before awaiting chat.send. The bound sender in server/utils/whatsapp.ts rejects on a non-successful WhatsApp response or timeout. The route does not catch that rejection, so the request returns an error while the user has no delivered code. The next request receives too soon until the cooldown expires.

Remove only the entries created by the failed attempt in the rejection path, or persist them after a successful send. Ensure cleanup cannot remove a newer pending verification.

Comment thread server/api/chat.ts
return c.json({ code: "too soon" }, 429);
}
const code = String(randomInt(0, 1_000_000)).padStart(6, "0");
await redis.set(`chat:${credentialId}`, JSON.stringify({ whatsappId, code }), "PX", 10 * 60_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- server/api/chat.ts (top) ---'
sed -n '1,45p' server/api/chat.ts
printf '%s\n' '--- chat route construction and secret-bearing dependencies ---'
rg -n -C 3 'chatRoute|createWhatsapp|authSecret|createHash|hmac|Hmac|createHmac' server/api server/utils server/index.ts server/middleware --glob '*.ts'

Repository: exactly/exa

Length of output: 14996


Sensitive Data Exposure

Reachability: Internal
Exploitability: Difficult
CWE: CWE-522 — Insufficiently Protected Credentials

Store a keyed digest of the validation code instead of the plaintext code.

server/api/chat.ts stores the six-digit code directly in Redis. A Redis reader or persistence snapshot can recover active codes. Use an HMAC keyed with authSecret, then compare the submitted code with the stored digest. A plain SHA-256 hash is insufficient because the code has only one million possible values.

🔒 Proposed fix
- import { randomInt } from "node:crypto";
+ import { createHmac, randomInt, timingSafeEqual } from "node:crypto";

 ...
 export default function route({
   auth,
+  authSecret,
   chat,
   database,
   redis,
 }: {
   auth: Auth;
+  authSecret: string;
   chat: ReturnType<typeof createWhatsapp>;
   database: NodePgDatabase<typeof schema>;
   redis: Redis;
 }) {
 ...
-          await redis.set(`chat:${credentialId}`, JSON.stringify({ whatsappId, code }), "PX", 10 * 60_000);
+          const digest = createHmac("sha256", authSecret).update(code).digest("hex");
+          await redis.set(`chat:${credentialId}`, JSON.stringify({ whatsappId, digest }), "PX", 10 * 60_000);
 ...
-        const { whatsappId, code } = parse(object({ code: string(), whatsappId: string() }), JSON.parse(pending));
-        if (code !== payload.code) return c.json({ code: "bad code" }, 400);
+        const { whatsappId, digest } = parse(object({ digest: string(), whatsappId: string() }), JSON.parse(pending));
+        const expected = createHmac("sha256", authSecret).update(payload.code).digest("hex");
+        if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(digest, "hex")))
+          return c.json({ code: "bad code" }, 400);

Pass authSecret when constructing chatRoute.

Source: Learnings

Comment thread server/package.json
"db:push": "drizzle-kit push",
"e2e": "tsx script/e2e.ts",
"eval:chat": "tsx test/workers/chat.eval.ts",
"eval:chat": "tsx --env-file=.env test/workers/chat.eval.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n -C3 'eval:chat|chat\.eval|CHAT_ANTHROPIC_API_KEY|REDIS_URL' . --glob '!pnpm-lock.yaml' --glob '!node_modules/**'
find . -maxdepth 3 -type f \( -iname '*readme*' -o -iname '*.md' \) -print

Repository: exactly/exa

Length of output: 7268


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- server/package.json ---'
sed -n '1,55p' server/package.json
printf '%s\n' '--- chat.eval.ts ---'
sed -n '1,45p' server/test/workers/chat.eval.ts
printf '%s\n' '--- server guidance ---'
sed -n '1,220p' AGENTS.md
sed -n '1,220p' .agents/rules/server.md
printf '%s\n' '--- environment/documentation candidates ---'
find . -maxdepth 3 -type f \( -iname '.env*' -o -iname '*readme*' -o -iname '*.md' \) -print | sort
printf '%s\n' '--- focused references outside generated/changeset/changelog files ---'
rg -n -i 'eval:chat|chat\.eval|env-file|CHAT_ANTHROPIC_API_KEY|REDIS_URL' \
  README.md AGENTS.md .agents server --glob '!**/CHANGELOG.md' --glob '!**/node_modules/**' \
  --glob '!server/package.json' --glob '!server/test/workers/chat.eval.ts' || true

Repository: exactly/exa

Length of output: 28923


Use --env-file-if-exists so eval:chat supports shell-provided variables without .env.

The server guidance requires test workflows to run without .env files. With --env-file=.env, Node exits before test/workers/chat.eval.ts runs when the file is absent, even when the required variables are set in the shell.

-    "eval:chat": "tsx --env-file=.env test/workers/chat.eval.ts",
+    "eval:chat": "tsx --env-file-if-exists=.env test/workers/chat.eval.ts",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"eval:chat": "tsx --env-file=.env test/workers/chat.eval.ts",
"eval:chat": "tsx --env-file-if-exists=.env test/workers/chat.eval.ts",

export type Job = {
contact?: string;
from: string;
phoneNumberId: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,130p' server/workers/chat/worker.ts
sed -n '1,100p' server/workers/chat/job.ts
rg -n 'chat.*queue|queue.*chat|new Queue|new Worker|removeOn|drain' server infra

Repository: exactly/exa

Length of output: 17112


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- chat queue ---'
cat -n server/workers/chat/queue.ts
printf '%s\n' '--- chat hook ---'
sed -n '1,130p' server/hooks/chat.ts
printf '%s\n' '--- generic queue ---'
cat -n server/workers/queue.ts
printf '%s\n' '--- generic worker ---'
cat -n server/workers/worker.ts
printf '%s\n' '--- chat test payloads and worker setup ---'
sed -n '1,110p' server/test/workers/chat.test.ts
sed -n '280,340p' server/test/workers/chat.test.ts
sed -n '490,535p' server/test/workers/chat.test.ts
printf '%s\n' '--- deployment and startup references ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'workers/chat|createChat|chat/worker|new Worker|queue.*chat|drain\\(|removeOnComplete|removeOnFail|bullmq' .github infra deploy docker-compose.yml Dockerfile server/package.json 2>/dev/null || true

Repository: exactly/exa

Length of output: 18448


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- chat queue ---'
cat -n server/workers/chat/queue.ts
printf '%s\n' '--- chat hook ---'
sed -n '1,130p' server/hooks/chat.ts
printf '%s\n' '--- generic queue ---'
cat -n server/workers/queue.ts
printf '%s\n' '--- generic worker ---'
cat -n server/workers/worker.ts
printf '%s\n' '--- chat tests ---'
sed -n '1,110p' server/test/workers/chat.test.ts
sed -n '280,340p' server/test/workers/chat.test.ts
sed -n '490,535p' server/test/workers/chat.test.ts
printf '%s\n' '--- deployment and startup references ---'
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'workers/chat|createChat|chat/worker|new Worker|queue.*chat|drain\(|removeOnComplete|removeOnFail|bullmq' .github infra deploy docker-compose.yml Dockerfile server/package.json 2>/dev/null || true

Repository: exactly/exa

Length of output: 18560


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- relevant diff ---'
git diff -- server/workers/chat/job.ts server/workers/chat/worker.ts server/hooks/chat.ts server/workers/chat/queue.ts
printf '%s\n' '--- thread references in chat code and tests ---'
rg -n -C 4 'memory:|thread:|persistent thread|phoneNumberId|undefined/' server/workers/chat server/test/workers/chat.test.ts server/hooks/chat.ts

Repository: exactly/exa

Length of output: 8859


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- repository state ---'
git status --short
git rev-parse --verify HEAD
git rev-parse --verify HEAD^ 2>/dev/null || true
printf '%s\n' '--- parent job ---'
git show HEAD^:server/workers/chat/job.ts 2>/dev/null || true
printf '%s\n' '--- parent worker memory call ---'
git show HEAD^:server/workers/chat/worker.ts 2>/dev/null | rg -n -C 5 'memory:|thread:' || true

Repository: exactly/exa

Length of output: 491


🏁 Script executed:

#!/bin/bash
set -e
git show HEAD^:server/workers/chat/worker.ts 2>/dev/null | sed -n '55,110p'

Repository: exactly/exa

Length of output: 2195


Handle legacy chat jobs before building the memory thread.

The chat queue keeps the same name across deployments, and retries use the same queue. The worker receives old job data without runtime validation. When phoneNumberId is absent, ${data.phoneNumberId}/${data.from} becomes undefined/<from> without throwing. That key differs from the canonical <phoneNumberId>/<from> key, so the job can split the sender's conversation history. Make the field optional and use the canonical compatibility fallback, or drain or migrate legacy jobs before deployment. Making the TypeScript field optional alone is not sufficient.

["seen", (await bullmq.exists(`whatsapp:seen:${data.from}`)) === 1],
["account", credential?.account],
["bridgeId", credential?.bridgeId ?? undefined],
["card", statuses.find((status) => status === credential?.cards[0]?.status)],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '35,115p' server/database/schema.ts
sed -n '75,110p' server/workers/chat/worker.ts
rg -n 'ACTIVE|FROZEN|cards\)|insert\(cards|update\(cards' server --glob '*.ts'

Repository: exactly/exa

Length of output: 31347


🤖 get_repo_knowledge executed:

get_repo_knowledge exactly/exa /tmp/coderabbit-repo-knowledge/exactly-exa-f26ca19b/learnings

Length of output: 14175


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- server/api/card.ts:320-380 ---'
sed -n '320,380p' server/api/card.ts
printf '%s\n' '--- server/api/card.ts:570-730 ---'
sed -n '570,730p' server/api/card.ts
printf '%s\n' '--- server/api/card.ts:870-925 ---'
sed -n '870,925p' server/api/card.ts
printf '%s\n' '--- server/utils/panda.ts:900-955 ---'
sed -n '900,955p' server/utils/panda.ts
printf '%s\n' '--- schema and migrations mentioning card uniqueness ---'
rg -n -i 'credential.?id.*unique|unique.*credential.?id|cards_credential|create table.*cards|card.*credential' server/database server --glob '*.sql' --glob '*.ts' | head -120

Repository: exactly/exa

Length of output: 30163


Prefer ACTIVE when selecting the card status.

The cards schema permits multiple rows for one credential. Normal issuance checks existing active cards, but the schema and status-update path do not enforce one-card cardinality or normalize existing rows. A credential can therefore have both ACTIVE and FROZEN cards.

The worker filters both statuses and reads credential?.cards[0]?.status without ordering. This does not define ACTIVE precedence, so the context can use different card statuses for the same credential.

♻️ Proposed fix
-          ["card", statuses.find((status) => status === credential?.cards[0]?.status)],
+          ["card", statuses.find((status) => credential?.cards.some((card) => card.status === status))],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
["card", statuses.find((status) => status === credential?.cards[0]?.status)],
["card", statuses.find((status) => credential?.cards.some((card) => card.status === status))],

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