✨ server: exachat v1 - #1297
✨ server: exachat v1#1297nfmelendez wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: 6fe4156 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
WalkthroughAdds WhatsApp credential association, encrypted chat tokens, account-aware chat processing, database-backed conversation context, and runtime configuration for API and worker services. ChangesWhatsApp chat integration
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
Suggested reviewers: Merge Risk: 🔵 Low · up to 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)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
💡 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".
| 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), |
There was a problem hiding this comment.
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 👍 / 👎.
| associate: { | ||
| path: "/whatsapp", |
There was a problem hiding this comment.
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 👍 / 👎.
| bridgeId: text("bridge_id"), | ||
| source: text("source"), | ||
| salt: text("salt").notNull().default(zeroAddress), | ||
| whatsappId: text("whatsapp_id"), |
There was a problem hiding this comment.
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 👍 / 👎.
| responses: { | ||
| 200: { description: "The id is available to associate." }, | ||
| 400: { description: "Bad token, associated with another credential, or this credential already has one." }, | ||
| }, |
There was a problem hiding this comment.
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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Line 992 in 02c0a7a
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".
There was a problem hiding this comment.
💡 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".
| - domain: business.${{ env.APP_DOMAIN }} | ||
| type: ALIAS |
There was a problem hiding this comment.
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 👍 / 👎.
| tags: ["Credential"], | ||
| validateResponse: true, | ||
| }), | ||
| vValidator("header", optional(object({ "account-type": optional(literal("business")) }))), |
There was a problem hiding this comment.
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 👍 / 👎.
| const isBusiness = c.req.valid("header")?.["account-type"] === "business"; | ||
| const credentialId = c.req.valid("cookie").credentialId; | ||
| if (isBusiness) { |
There was a problem hiding this comment.
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 👍 / 👎.
| async function getPendingInquiryTemplate(referenceId: string, scope: AccountScope) { | ||
| if (scope === "business") return (await getAccount(referenceId, scope)) ? undefined : PANDA_BUSINESS_TEMPLATE; | ||
| const unknownAccount = await getUnknownAccount(referenceId); |
There was a problem hiding this comment.
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 👍 / 👎.
| "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", |
There was a problem hiding this comment.
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 👍 / 👎.
| const input = code ? ( | ||
| <BRCodeField value={value} name={code.ownerName} /> |
There was a problem hiding this comment.
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 👍 / 👎.
| if (job.data.origin === "activity" && pending.length > 0) { | ||
| await job.updateData({ ...job.data, assets: pending }); | ||
| throw new Error(NO_BALANCE); |
There was a problem hiding this comment.
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 👍 / 👎.
^ Conflicts: ^ server/test/workers/chat.test.ts
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: f2d41147-ec74-47dc-81e0-734315ce2762
📒 Files selected for processing (23)
.changeset/warm-otters-associate.mdinfra/utils/modules.tsserver/api/bin.tsserver/api/chat.tsserver/api/index.tsserver/database/schema.tsserver/hooks/chat.tsserver/index.tsserver/package.jsonserver/script/openapi.tsserver/test/api/api.test.tsserver/test/api/bin.test.tsserver/test/api/chat.test.tsserver/test/hooks/chat.test.tsserver/test/utils/whatsapp.test.tsserver/test/workers/bin.test.tsserver/test/workers/chat.eval.tsserver/test/workers/chat.test.tsserver/utils/whatsapp.tsserver/vitest.config.mtsserver/workers/chat/bin.tsserver/workers/chat/job.tsserver/workers/chat/worker.ts
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
| // 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); |
There was a problem hiding this comment.
🩺 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 300Repository: 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.
| 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); |
There was a problem hiding this comment.
🔒 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
| "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", |
There was a problem hiding this comment.
📐 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' \) -printRepository: 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' || trueRepository: 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.
| "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; |
There was a problem hiding this comment.
🗄️ 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 infraRepository: 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 || trueRepository: 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 || trueRepository: 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.tsRepository: 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:' || trueRepository: 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)], |
There was a problem hiding this comment.
🎯 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 -120Repository: 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.
| ["card", statuses.find((status) => status === credential?.cards[0]?.status)], | |
| ["card", statuses.find((status) => credential?.cards.some((card) => card.status === status))], |
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
test plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01WRNy8HTnB5tWJWsg3DBBb1
Summary by CodeRabbit
New Features
Improvements