Skip to content

✨ server: add runtime entrypoints for api and hooks - #1226

Closed
cruzdanilo wants to merge 9 commits into
allowfrom
infra
Closed

✨ server: add runtime entrypoints for api and hooks#1226
cruzdanilo wants to merge 9 commits into
allowfrom
infra

Conversation

@cruzdanilo

@cruzdanilo cruzdanilo commented Aug 14, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added deployable API and service hook entrypoints, including activity, block, bridge, chat, Manteca, Panda, and Persona services.
    • Added WhatsApp chat support with an AI assistant, secure webhook handling, messaging, account association, verification codes, and credential reassignment.
    • Added secure runtime configuration, signer assignments, and launchable package exports.
  • Deployment
    • Updated the server base workflow to run from the infrastructure branch.
  • Tests
    • Added comprehensive coverage for service startup, chat workflows, security, readiness, and failure handling.

@cruzdanilo
cruzdanilo requested a review from nfmelendez as a code owner August 14, 2026 15:55
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fb91a71

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

This PR includes changesets to release 2 packages
Name Type
@exactly/infra Minor
@exactly/server Patch

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

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

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cruzdanilo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 431d85db-d650-4d63-a0a7-a54a4d1a2354

📥 Commits

Reviewing files that changed from the base of the PR and between 46da69c and fb91a71.

⛔ Files ignored due to path filters (2)
  • src/assets/images/isotype.svg is excluded by !**/*.svg
  • src/assets/images/whatsapp.svg is excluded by !**/*.svg
📒 Files selected for processing (10)
  • cspell.json
  • infra/utils/modules.ts
  • server/api/chat.ts
  • server/index.ts
  • src/app/(main)/whatsapp.tsx
  • src/components/whatsapp/Whatsapp.tsx
  • src/i18n/es-AR.json
  • src/i18n/es.json
  • src/i18n/pt.json
  • src/utils/server.ts

Walkthrough

Added a WhatsApp chat assistant, webhook, and authenticated association API. Added database support, runtime entrypoints, service configuration, lifecycle wiring, package exports, evaluations, and tests for chat and existing API and hook binaries.

Changes

Chat service

Layer / File(s) Summary
Chat utilities and assistant
server/utils/assistant.ts, server/utils/chat.ts, server/test/utils/*
Added encrypted chat tokens, WhatsApp messaging, assistant tools, memory, localized links, and request-context handling.
Chat association API
server/api/chat.ts, server/api/index.ts, server/database/schema.ts, server/test/api/chat.test.ts
Added authenticated token and validation-code association flows. Added unique whatsappId credential storage and integration coverage.
WhatsApp webhook and lifecycle
server/hooks/chat.ts, server/index.ts, server/hooks/bin/chat.ts, server/test/hooks/chat.test.ts, server/test/hooks/hook.test.ts
Added webhook verification, HMAC validation, event parsing, message grouping, assistant execution, delivery, observability, cleanup, and supervised startup.
Chat evaluation and dependency support
server/test/evals/chat.eval.ts, server/package.json, server/instrument.cjs, package.json, pnpm-workspace.yaml, server/vitest.config.mts, cspell.json, .changeset/warm-otters-associate.md
Added chat evaluations, Mastra dependencies, Sentry configuration, test settings, workspace configuration, spelling entries, and a release changeset.

Runtime entrypoints

Layer / File(s) Summary
Service registration and release wiring
infra/utils/modules.ts, server/package.json, server/hooks/bin/chat.ts, .github/workflows/server-base-sepolia.yaml, .changeset/*
Registered API and hook services, exported runtime binaries, updated the workflow branch, and added release changesets.
API startup and validation
server/api/bin.ts, server/test/api/bin.test.ts
Resolved API secrets concurrently, constructed the API, supervised it as "api", and tested configuration, supervision, and health responses.
Hook startup and validation
server/hooks/bin/*, server/test/hooks/bin.test.ts
Added supervised startup for activity, block, bridge, manteca, panda, and persona. Tests verify secret and signer loading, construction, readiness, supervision, and missing-account failures.

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

Merge Risk: 🔴 Critical · up to 46da6

This change adds API and webhook entrypoints, but the current version may fail to start, accept unsigned webhook requests, and send invalid WhatsApp requests. Merge should be blocked until the startup, authentication, and request-format issues are fixed; dependency-policy and evaluation follow-ups also remain.

Sequence Diagram(s)

sequenceDiagram
  participant WhatsApp
  participant ChatWebhook
  participant Assistant
  participant Redis
  participant Database
  WhatsApp->>ChatWebhook: send signed message webhook
  ChatWebhook->>Database: resolve credential by whatsappId
  ChatWebhook->>Redis: load conversation memory
  ChatWebhook->>Assistant: generate response with request context
  Assistant-->>ChatWebhook: return response and links
  ChatWebhook->>WhatsApp: send reply
Loading

Possibly related PRs

  • exactly/exa#1157: Extends the same chat-association implementation, including credential schema, API routes, utilities, and tests.

Suggested reviewers: nfmelendez

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the addition of server runtime entrypoints for the API and hooks, which is a central change in the pull request.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch infra
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch infra

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.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 39.33333% with 91 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.17%. Comparing base (9b1ad09) to head (4cbc9cf).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/api/chat.ts 6.25% 30 Missing ⚠️
server/hooks/chat.ts 27.77% 26 Missing ⚠️
server/utils/assistant.ts 47.50% 21 Missing ⚠️
server/utils/chat.ts 17.64% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            allow    #1226      +/-   ##
==========================================
- Coverage   74.56%   74.17%   -0.39%     
==========================================
  Files         285      296      +11     
  Lines       14295    14496     +201     
  Branches     5071     5110      +39     
==========================================
+ Hits        10659    10753      +94     
- Misses       3282     3387     +105     
- Partials      354      356       +2     
Flag Coverage Δ
e2e 74.17% <39.33%> (-0.39%) ⬇️

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

Here are some automated review suggestions for this pull request.

Reviewed commit: 96beaa79e9

ℹ️ 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 infra/utils/modules.ts
export default define({
common: ["redis-url", "sentry-dsn"],
crema: ["redis-address", "redis-password", "redis-username"],
services: {

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 Set APP_DOMAIN on the new Cloud Run services

When these entries are materialized by infra/index.ts:65-68, the container gets APP_STACK/NODE_ENV/SENTRY_DSN but no APP_DOMAIN. server/utils/appOrigin.ts derives origins from common/domain.js:1-2, which falls back to sandbox.exactly.app, so any non-sandbox Cloud Run api/hook service will use the sandbox origin for CORS/auth and for activity/block webhook registration. Pass the deployment domain into these service envs before enabling them.

Useful? React with 👍 / 👎.

Comment thread infra/utils/modules.ts
common: ["redis-url", "sentry-dsn"],
crema: ["redis-address", "redis-password", "redis-username"],
services: {
api: {

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 Pass ALCHEMY_ACTIVITY_ID to the api service

For the new split api service, this config doesn't set ALCHEMY_ACTIVITY_ID; unlike the monolith, it no longer runs hooks/activity in the same process to update server/utils/activityWebhook.ts:7-10. When server/workers/subscribe/queue.ts:20-22 takes its direct Alchemy fallback after a queue enqueue failure, webhookId is undefined, so new accounts are not subscribed to activity webhooks (the caller then swallows the error in server/utils/createCredential.ts:86). Add the same env: { ALCHEMY_ACTIVITY_ID: "alchemyActivityId" } used by the subscribe worker to the api service.

Useful? React with 👍 / 👎.

Comment thread server/api/bin.ts
segmentKey,
walletExtensionSecret,
]) =>
api({

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 Preserve the /api prefix in the api entrypoint

When the new api service is reached through the existing client URL shape, requests still include the /api prefix (src/utils/server.ts:74), but this entrypoint serves the bare api.app whose routes are /activity, /card, etc.; the monolith previously mounted it with app.route("/api", api.app) in server/index.ts:116. Without either mounting under /api here or configuring a path-stripping proxy, every existing /api/* request to this service will 404.

Useful? React with 👍 / 👎.

secret("activity-postgres-url"),
secret("redis-url"),
]).then(([alchemyKey, onesignalKey, postgresUrl, redisUrl]) =>
activity({ alchemyKey, onesignalKey, postgresUrl, redisUrl }),

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 Preserve hook path prefixes in the hook entrypoints

The split hook services supervise each bare hook app, and those apps only register POST "/"; the monolith mounted them under /hooks/<name> (server/index.ts:117-122), and activity/block even self-register webhook URLs at ${appOrigin}/hooks/activity and ${appOrigin}/hooks/block. If these new services receive the existing provider callbacks at /hooks/<name>, the requests will miss the only route and return 404 unless the entrypoint mounts the app at the same prefix or the ingress strips it.

Useful? React with 👍 / 👎.

Comment thread server/hooks/bin/block.ts
secret("block-alchemy-webhooks-key"),
secret("block-onesignal-api-key"),
secret("redis-url"),
signer("executor"),

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 Use the keeper signer for block execution

signer("executor") selects a new KMS key named for executor, but the block hook is still performing the keeper-backed executions that the monolith wired with executor: keeper (server/index.ts:62-66), and the plugin validates these operations against the keeper role (contracts/src/ExaPlugin.sol:373-408). In Cloud Run, proposals and withdrawals signed by an executor key that is not the deployed keeper will revert instead of being processed; wire this to the keeper signer or authorize the new key on-chain.

Useful? React with 👍 / 👎.

Comment thread server/hooks/bin/panda.ts
secret("panda-sardine-api-key"),
secret("sardine-api-url"),
secret("panda-segment-write-key"),
signer("settler"),

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 Use the keeper signer for panda settlement

signer("settler") selects a distinct KMS key, while the monolith passed the deployed keeper as settler (server/index.ts:87-97) and the Panda hook sends keeper-restricted collectDebit/collectInstallments calls (server/hooks/panda.ts:1192 and server/hooks/panda.ts:1235; contracts/src/ExaPlugin.sol:398-402). On Cloud Run, debit and installment settlements signed by an unauthorized settler key will revert, so wire this to the keeper signer or authorize the new key on-chain.

Useful? React with 👍 / 👎.

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

ℹ️ 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 infra/utils/modules.ts
Comment on lines +4 to +5
services: {
api: {

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 module prerequisites before deploying services

When this commit is deployed to a stack that has not been manually pre-provisioned, every added service depends on identities, secrets, and signer keys that are created only by infra/meta.ts from modules.services (infra/meta.ts:185-210,251-263), while the checked deployment path in .github/workflows/server-deploy.yaml:38-41 runs only the ordinary stack. Consequently, infra/index.ts looks up service accounts such as ${stack}-api before they exist, so the Pulumi deployment fails instead of creating these services; run/update the meta stack as an explicit prerequisite in the deployment workflow.

Useful? React with 👍 / 👎.

Comment thread infra/utils/modules.ts
],
},
activity: { secrets: ["alchemy-webhooks-key", "onesignal-api-key", "postgres-url"] },
block: { secrets: ["alchemy-webhooks-key", "onesignal-api-key"], signers: ["executor"] },

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 Avoid running two block schedulers during the service rollout

When the new block service starts with an authorized executor and Redis contains pending withdrawals or proposals, it loads and schedules every entry in server/hooks/block.ts:89-101, but the checked deployment workflow still deploys the DigitalOcean monolith in parallel (.github/workflows/server-deploy.yaml:42-56), where the same block hook performs the same startup scan. The mutex in block.ts is process-local, so both processes can submit the same keeper operation and race to remove the Redis entry, producing competing on-chain executions and duplicate side effects; the rollout must stop the monolith scheduler or add a distributed claim/lock before enabling this service.

Useful? React with 👍 / 👎.

Comment thread infra/utils/modules.ts
Comment on lines +40 to +43
panda: {
secrets: ["onesignal-api-key", "panda-api-key", "postgres-url", "sardine-api-key", "segment-write-key"],
shared: ["panda-api-url", "sardine-api-url"],
signers: ["settler", "issuer"],

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 Preserve CPU for Panda's post-response work

When a non-transaction Panda webhook needs forwarding, server/hooks/panda.ts:150-154 starts publish() without awaiting it and returns the response immediately, while this Cloud Run service receives neither an always-on CPU setting nor a committed pandaResources override through infra/index.ts:62,117. Under Cloud Run's default request-scoped CPU allocation, the instance can be throttled or terminated as soon as that response completes, interrupting the downstream webhook calls and retries in publish(); await or queue this work, or deploy the service with instance-based CPU allocation.

Useful? React with 👍 / 👎.

@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: 8b15d51093

ℹ️ 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 infra/utils/modules.ts
],
},
activity: { secrets: ["alchemy-webhooks-key", "onesignal-api-key", "postgres-url"] },
block: { secrets: ["alchemy-webhooks-key", "onesignal-api-key"], signers: ["executor"] },

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 instance-based CPU for block timers

When this Cloud Run service becomes the sole block scheduler, proposals and withdrawals are persisted and then executed by detached timers in server/hooks/block.ts:482-507 and 598-612. The checked service definition reads only an optional blockResources override, but no such override is committed, so Cloud Run's default request-scoped CPU can be throttled or the instance can scale to zero after the webhook response completes; with no subsequent request, the Redis entry remains pending indefinitely. Configure instance-based CPU (for example, cpuIdle: false) or move the delayed execution to a worker.

Useful? React with 👍 / 👎.

Comment thread server/hooks/bin/block.ts
secret("block-alchemy-webhooks-key"),
secret("block-onesignal-api-key"),
secret("redis-url"),
signer("executor"),

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 Pass the fallback signing key to the block hook

When a block instance cold-starts while Alchemy's webhook-management API is temporarily unavailable, initializeAlchemy only suppresses the failure if its signing-key set is already populated (server/hooks/block.ts:102-104). This entrypoint never passes blockKey, unlike the monolith's ALCHEMY_BLOCK_KEY wiring, so the readiness promise rejects and the HTTP server never starts even though the existing signing key could validate incoming callbacks. Provision the block signing key and pass it into the hook.

Useful? React with 👍 / 👎.

@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: 4cbc9cfe3a

ℹ️ 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 infra/utils/modules.ts
Comment on lines +21 to +28
shared: [
"bridge-api-url",
"manteca-api-url",
"panda-api-url",
"pax-api-url",
"persona-api-url",
"sardine-api-url",
],

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 Grant the api service access to the chat secrets

The api entrypoint now reads chat-identity-key, chat-whatsapp-access-token, and chat-whatsapp-phone-number-id, but this service configuration grants its account access only to the listed api-prefixed secrets and URL shares. On Cloud Run, the Promise.all in server/api/bin.ts therefore rejects with Secret Manager permission errors and the api service never starts; add these three chat secrets to the api service's shared secrets.

Useful? React with 👍 / 👎.

Comment thread infra/utils/modules.ts
"whatsapp-phone-number-id",
"whatsapp-verify-token",
],
shared: ["chat-identity-key"],

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 duplicate chat identity secret

For the chat module, identity-key is first prefixed to chat-identity-key by modules() and then the same name is added again through shared. Although the meta stack deduplicates secret creation, infra/index.ts:85-93 creates one SecretIamMember per list entry using the same logical name, so Pulumi encounters duplicate chat-chat-identity-key-access resources and cannot deploy the services.

Useful? React with 👍 / 👎.

Comment thread server/database/schema.ts
Comment on lines +38 to +43
whatsappId: text("whatsapp_id"),
},
({ account, bridgeId }) => [uniqueIndex("account_index").on(account), uniqueIndex("bridge_id_index").on(bridgeId)],
({ account, bridgeId, whatsappId }) => [
uniqueIndex("account_index").on(account),
uniqueIndex("bridge_id_index").on(bridgeId),
uniqueIndex("whatsapp_id_index").on(whatsappId),

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 a migration for the WhatsApp credential column

This adds whatsapp_id and its unique index only to the Drizzle schema, with no migration in the commit. Existing databases therefore lack the column, causing both the association queries and the chat webhook's credential lookup to fail at runtime as soon as these routes are used; generate and include the corresponding database migration. .agents/rules/server.mdL28-L32

Useful? React with 👍 / 👎.

Comment thread server/index.ts
authSecret: parse(pipe(string("auth"), nonEmpty("auth")), env.AUTH_SECRET),
bridgeKey: parse(pipe(string("bridge key"), nonEmpty("bridge key")), env.BRIDGE_API_KEY),
bridgeUrl: parse(pipe(string("bridge url"), nonEmpty("bridge url")), env.BRIDGE_API_URL),
chatKey: parse(pipe(string("chat"), nonEmpty("chat")), env.CHAT_IDENTITY_KEY),

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 chat variables to the monolith deployment

The checked .github/workflows/server-deploy.yaml still deploys the DigitalOcean monolith from .do/app.yaml, whose complete server environment list has none of CHAT_IDENTITY_KEY, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, or ANTHROPIC_API_KEY. This new required parse therefore throws while importing server/index.ts—before the HTTP server starts—on the sandbox, base, and production deployment paths; add the chat configuration to that service or do not initialize chat in the monolith.

Useful? React with 👍 / 👎.

Comment thread server/utils/assistant.ts
app: { path: "" },
handoff: { path: "", intro: "You can continue this in the app." },
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

Every association tool response sends the user to ${appOrigin}/whatsapp, but a repo-wide search for whatsapp and inspection of the Expo Router tree found no src/app/whatsapp route and no client code invoking the new /api/chat endpoint. The server's static rewrites consequently have no exported page to serve at this URL, so users following either the associate or move link reach a 404 and cannot complete the association flow.

Useful? React with 👍 / 👎.

Comment thread server/hooks/chat.ts
vValidator("json", event, validatorHook({ code: "bad chat" })),
async (c) => {
// TODO implement queue
const messages = new Map(parse(c.req.valid("json")).map((message) => [message.id, message] as const));

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 Deduplicate message IDs across webhook deliveries

When WhatsApp redelivers a payload after a timeout or non-2xx response, this request-local Map is reconstructed and no longer remembers IDs from the earlier delivery. For example, if one sender group succeeds and another rejects, line 125 makes the whole webhook response fail even though the successful group already sent its reply; the retry then invokes the model and sends that reply again. Persist processed message IDs or move the work to an idempotent queue.

Useful? React with 👍 / 👎.

Comment thread server/package.json
"generate:broadcasts": "[ \"$CHAIN_ID\" != 31337 ] || NODE_ENV=development tsx -e 'require(\"./test/anvil\").default({ provide: () => undefined }).then((teardown) => teardown())'",
"db:push": "drizzle-kit push",
"e2e": "tsx script/e2e.ts",
"eval:chat": "tsx --env-file=.env test/evals/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 Make the chat eval run without a .env file

This target unconditionally asks tsx to load .env, but the repository deliberately provides and permits no such file, so pnpm nx eval:chat server exits on the missing file before starting the evaluation in the supported development environment. Remove the file dependency and consume explicitly supplied runtime configuration or project defaults instead.

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

Useful? React with 👍 / 👎.

@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: 46da69cc8d

ℹ️ 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/hooks/chat.ts
}

function verify(body: string, signature?: string, secret?: string) {
if (!secret) return true;

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 Require the WhatsApp webhook secret

When the monolith is made to start but WHATSAPP_APP_SECRET remains absent—as it currently is in .do/app.yaml—this branch treats every request as authenticated. This is distinct from the already-noted missing startup variables: an Internet caller can POST fabricated message events, trigger Anthropic work and credential lookups, and make the service send WhatsApp messages to attacker-selected recipients; make the secret required and fail startup rather than failing open. .agents/rules/server.mdL61-L65

Useful? React with 👍 / 👎.

Comment thread server/api/chat.ts
Comment on lines +39 to +42
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.

P2 Badge Add JSON response schemas for the chat routes

When the OpenAPI specification is generated, these description-only responses omit the actual JSON contracts, including the success and conflict codes and the confirmed whatsappId; the POST response block has the same problem. As a result, generated documentation and consumers cannot derive the endpoint's response types despite this being a schema-first API, so define Valibot response schemas and attach them with resolver. .agents/rules/server.mdL12-L15

Useful? React with 👍 / 👎.

@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: 46da69cc8d

ℹ️ 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/hooks/chat.ts
}

function verify(body: string, signature?: string, secret?: string) {
if (!secret) return true;

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 Require the WhatsApp webhook secret

When the monolith is made to start but WHATSAPP_APP_SECRET remains absent—as it currently is in .do/app.yaml—this branch treats every request as authenticated. This is distinct from the already-noted missing startup variables: an Internet caller can POST fabricated message events, trigger Anthropic work and credential lookups, and make the service send WhatsApp messages to attacker-selected recipients; make the secret required and fail startup rather than failing open. .agents/rules/server.mdL61-L65

Useful? React with 👍 / 👎.

Comment thread server/api/chat.ts
Comment on lines +39 to +42
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.

P2 Badge Add JSON response schemas for the chat routes

When the OpenAPI specification is generated, these description-only responses omit the actual JSON contracts, including the success and conflict codes and the confirmed whatsappId; the POST response block has the same problem. As a result, generated documentation and consumers cannot derive the endpoint's response types despite this being a schema-first API, so define Valibot response schemas and attach them with resolver. .agents/rules/server.mdL12-L15

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: Pro Plus

Run ID: b9313286-d080-4efb-8f5f-3849fb86278a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b15d51 and 46da69c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .changeset/warm-otters-associate.md
  • cspell.json
  • infra/utils/modules.ts
  • package.json
  • pnpm-workspace.yaml
  • server/api/bin.ts
  • server/api/chat.ts
  • server/api/index.ts
  • server/database/schema.ts
  • server/hooks/bin/chat.ts
  • server/hooks/chat.ts
  • server/index.ts
  • server/instrument.cjs
  • 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/evals/chat.eval.ts
  • server/test/hooks/chat.test.ts
  • server/test/hooks/hook.test.ts
  • server/test/utils/assistant.test.ts
  • server/test/utils/chat.test.ts
  • server/utils/assistant.ts
  • server/utils/chat.ts
  • server/vitest.config.mts

Comment thread infra/utils/modules.ts
Comment on lines +21 to +28
shared: [
"bridge-api-url",
"manteca-api-url",
"panda-api-url",
"pax-api-url",
"persona-api-url",
"sardine-api-url",
],

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 | 🔴 Critical | ⚡ Quick win

Authorize the API service to read the chat secrets.

server/api/bin.ts resolves chat-identity-key, chat-whatsapp-access-token, and chat-whatsapp-phone-number-id. This module exposes only API-prefixed, common, and listed shared secrets. Secret resolution will reject during API startup, and supervise will terminate the process.

Add the three chat secret names to services.api.shared.

Proposed fix
       shared: [
         "bridge-api-url",
+        "chat-identity-key",
+        "chat-whatsapp-access-token",
+        "chat-whatsapp-phone-number-id",
         "manteca-api-url",
         "panda-api-url",
         "pax-api-url",
📝 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
shared: [
"bridge-api-url",
"manteca-api-url",
"panda-api-url",
"pax-api-url",
"persona-api-url",
"sardine-api-url",
],
shared: [
"bridge-api-url",
"chat-identity-key",
"chat-whatsapp-access-token",
"chat-whatsapp-phone-number-id",
"manteca-api-url",
"panda-api-url",
"pax-api-url",
"persona-api-url",
"sardine-api-url",
],

Comment thread pnpm-workspace.yaml
ignoreWorkspaceRootCheck: true
includeWorkspaceRoot: true
minimumReleaseAge: 1440
minimumReleaseAgeExclude: ["@mastra/*"]

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'minimumReleaseAge|minimumReleaseAgeExclude' pnpm-workspace.yaml
rg -n -C 2 '"`@mastra/`' server/package.json
fd -a '^pnpm-lock\.yaml$' . -x rg -n -C 1 '`@mastra/`' {}

Repository: exactly/exa

Length of output: 8469


🌐 Web query:

pnpm minimumReleaseAgeExclude package pattern version syntax documentation

💡 Result:

In pnpm, the minimumReleaseAgeExclude setting is used to exempt specific dependencies from the minimumReleaseAge restriction [1][2]. This configuration is defined in your pnpm-workspace.yaml file [3][2]. The setting supports a flexible syntax that allows you to specify packages by name, pattern (glob), or specific version [4][2]. Supported Syntax and Patterns: 1. Package Name: You can exclude an entire package regardless of its version [2]. minimumReleaseAgeExclude: - webpack 2. Glob Patterns: You can use wildcards to exclude all packages within a scope or pattern [2][5]. minimumReleaseAgeExclude: - '@myorg/*' 3. Version Pinning: You can exclude a specific version of a package [4][2]. minimumReleaseAgeExclude: - nx@21.6.5 4. Version Ranges: You can use standard disjunction syntax (||) to exclude multiple specific versions [2][6]. minimumReleaseAgeExclude: - webpack@4.47.0 || 5.102.1 When configured, these exclusions allow pnpm to install the specified packages immediately, bypassing the wait time typically enforced by minimumReleaseAge [1][2]. Note that minimumReleaseAgeExclude was introduced in pnpm v10.16.0 [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

text = Path("pnpm-workspace.yaml").read_text()
for key in ("minimumReleaseAge", "minimumReleaseAgeExclude"):
    m = re.search(rf"(?m)^{re.escape(key)}:.*$", text)
    print(f"{key}: {m.group(0) if m else 'not found'}")

lock = Path("pnpm-lock.yaml").read_text()
names = sorted(set(re.findall(r"(?m)^\s*'(`@mastra/`[^']+)@([^':(]+)", lock)))
print("lockfile Mastra package/version keys:")
for name, version in names:
    print(f"  {name}@{version}")
PY

Repository: exactly/exa

Length of output: 1932


Restrict the release-age bypass to required package versions.

@mastra/* bypasses the 24-hour policy for every current and future package version in the @mastra scope. Replace it with exact package@version selectors for the versions required by this update.

Comment thread server/hooks/chat.ts
Comment on lines +139 to +145
function verify(body: string, signature?: string, secret?: string) {
if (!secret) return true;
if (!signature) return false;
const expected = Buffer.from(`sha256=${createHmac("sha256", secret).update(body).digest("hex")}`);
const received = Buffer.from(signature);
return received.length === expected.length && timingSafeEqual(received, expected);
}

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 | 🟠 Major | ⚡ Quick win

Fail closed when webhook signing configuration is absent.

verify accepts every request when secret is absent. server/index.ts can pass an absent WHATSAPP_APP_SECRET into this branch. An unauthenticated caller can then submit a valid payload that invokes the assistant and outbound WhatsApp delivery.

  • server/hooks/chat.ts#L139-L145: Return false when secret is absent, or require a non-optional secret in the hook configuration.
  • server/index.ts#L83-L92: Parse WHATSAPP_APP_SECRET as a non-empty required value before createChat.
  • server/test/hooks/chat.test.ts#L72-L76: Add a request test that confirms an unsigned payload is rejected when no signing secret is configured.

Based on learnings, validate required environment variables at module import time and throw when they are absent.

📍 Affects 3 files
  • server/hooks/chat.ts#L139-L145 (this comment)
  • server/index.ts#L83-L92
  • server/test/hooks/chat.test.ts#L72-L76

Source: Learnings

Comment on lines +318 to +330
const requestContext = new RequestContext<InferPublicSchema<typeof context>>([
["account", associated ? account : undefined],
["credentialId", associated ? "credential" : undefined],
["whatsappId", whatsappId],
]);
const messages = [
...history.flatMap(({ user, assistant: answer }) => [
{ role: "user" as const, content: user },
{ role: "assistant" as const, content: answer },
]),
{ role: "user" as const, content: input },
];
const { text, toolCalls, totalUsage } = await reply(messages, { requestContext });

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'async function reply|agent\.generate|AgentExecutionOptions' server/utils/assistant.ts
rg -n -C 4 'new RedisStore|reply\(|requestContext|memory:|thread:|resource:' server/test/evals/chat.eval.ts
fd -a '^pnpm-lock\.yaml$' . -x rg -n -C 2 '`@mastra/`(core|memory|redis)' {}

Repository: exactly/exa

Length of output: 9919


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- assistant setup and memory configuration ---'
sed -n '1,180p' server/utils/assistant.ts
rg -n -C 5 'Memory|memory|RedisStore|resource|thread|generate\(' server --glob '*.ts' --glob '*.tsx'

printf '%s\n' '--- evaluation structure and memory cases ---'
sed -n '1,80p' server/test/evals/chat.eval.ts
sed -n '250,370p' server/test/evals/chat.eval.ts
rg -n -C 8 'name:.*memory|memory' server/test/evals/chat.eval.ts

printf '%s\n' '--- local Mastra type/source references, if present ---'
git ls-files | rg '(^|/)(node_modules|packages)/.*(`@mastra`|mastra)' || true
rg -n -C 4 'interface AgentExecutionOptions|type AgentExecutionOptions|memory\??:|thread\??:|resource\??:' . --glob '*.d.ts' --glob '*.ts' --glob '*.tsx' --glob '!pnpm-lock.yaml' || true

Repository: exactly/exa

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- production memory identifiers ---'
rg -n -C 10 'memory: \{ resource, thread \}|resource:.*thread:|thread:.*resource:|assistant\(' server/hooks server --glob '*.ts' --glob '!server/test/**' --glob '!server/test/evals/chat.eval.ts' --glob '!server/utils/assistant.ts'

printf '%s\n' '--- memory evaluation cases and runner ---'
sed -n '20,175p' server/test/evals/chat.eval.ts
sed -n '280,355p' server/test/evals/chat.eval.ts
sed -n '355,430p' server/test/evals/chat.eval.ts

printf '%s\n' '--- assistant tests covering memory options ---'
sed -n '80,120p' server/test/utils/assistant.test.ts
sed -n '1,45p' server/test/hooks/chat.test.ts
rg -n -C 6 'reply\(|memory:|resource|thread' server/test/hooks/chat.test.ts server/test/utils/assistant.test.ts

Repository: exactly/exa

Length of output: 23907


🌐 Web query:

Mastra v1.59 Agent memory AgentExecutionOptions memory resource thread behavior missing resource thread

💡 Result:

In Mastra, memory and thread behavior are managed through the AgentExecutionOptions interface, which allows you to define how an agent interacts with conversation persistence [1][2][3]. The core identifiers for memory management are the resource and the thread [1][4][2]: 1. Resource (resourceId): This typically identifies the user or entity (e.g., a specific user ID) [1][2]. It serves as a persistent scope for data like working memory and observational logs [4][5]. 2. Thread (threadId): This identifies a specific conversation session [1][2]. If you are experiencing issues with thread behavior or memory, ensure both identifiers are correctly provided in your agent call [1][2]: const response = await agent.generate('Your prompt', { memory: { thread: 'your-thread-id', resource: 'your-resource-id', }, }) Regarding scoping behavior: - Resource-Scoped Memory (Default): If agents share the same resourceId, they can share observations, working memory, and embeddings across different threads [4][5]. - Thread-Scoped Memory: If agents use the same resourceId and threadId, they share the full message history, allowing for tight coupling where agents build upon each other's outputs [4]. Working memory can be explicitly set to 'thread' scope if you need isolation per conversation [5]. If a thread is missing or memory is not persisting as expected, verify that: - Your storage provider is correctly configured for the memory instance [6][7]. - The same resourceId and threadId are being passed consistently for the relevant context [4]. - You are not accidentally switching between thread-scoped and resource-scoped memory configurations, as they are isolated from one another [5]. Note that "v1.59" appears to be a versioning reference; Mastra continues to evolve its memory capabilities, including "vnext" features for RAG and working memory improvements [7]. Always ensure your implementation aligns with the documented Memory class and AgentExecutionOptions for your specific version [6][1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from urllib.request import urlopen
urls = [
    "https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/agent.ts",
    "https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/index.ts",
]
terms = ("memory", "resource", "thread", "generate")
for url in urls:
    print(f"--- {url} ---")
    try:
        text = urlopen(url, timeout=10).read().decode()
    except Exception as exc:
        print(type(exc).__name__, exc)
        continue
    lines = text.splitlines()
    for index, line in enumerate(lines):
        if any(term in line.lower() for term in terms):
            start = max(0, index - 3)
            end = min(len(lines), index + 4)
            print("\n".join(f"{i + 1}:{lines[i]}" for i in range(start, end)))
            print("---")
PY

Repository: exactly/exa

Length of output: 631


🌐 Web query:

Mastra Agent.generate without memory resource thread missing memory options throws error or disables memory

💡 Result:

In Mastra, when you configure an agent with memory but provide only a resourceId (or missing/undefined threadId) in the agent.generate() call, the operation may throw an error or fail to persist data because Mastra requires both a resourceId and a threadId to properly scope and store conversation history [1][2][3][4]. The error "A resourceId and a threadId must be provided when using Memory" typically occurs because the agent's memory preparation step fails to validate the required identifiers [3]. Mastra organizes conversations by these two identifiers, and providing only one is insufficient for the system to determine where to read from or write to [5][6]. To resolve this issue: 1. Ensure you are passing both resource (the owner/user entity) and thread (the conversation session ID) in the memory object when calling agent.generate() [7][5][8]. 2. If you are working in an environment (such as datasets or middleware) where only a resourceId is automatically available, you must explicitly supply a threadId (e.g., by generating a unique ID) to complete the required memory configuration [3]. 3. Verify that the agent instance itself has been correctly initialized with a Memory instance that includes a configured storage backend [4][9]. Without a storage backend, memory features are effectively disabled, and attempting to use them may lead to errors [4][10]. If you provide a threadId and resourceId but find that memory is not working as expected (e.g., nothing is saved), check for circular dependencies in your code that might result in the memory configuration being passed as undefined to the agent [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from urllib.request import urlopen

urls = [
    "https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/agent.ts",
    "https://raw.githubusercontent.com/mastra-ai/mastra/main/packages/core/src/agent/index.ts",
]
terms = ("memory", "resource", "thread", "generate")

for url in urls:
    print(f"--- {url} ---")
    try:
        text = urlopen(url, timeout=10).read().decode()
    except Exception as exc:
        print(type(exc).__name__, exc)
        continue

    lines = text.splitlines()
    for index, line in enumerate(lines):
        if any(term in line.lower() for term in terms):
            start = max(0, index - 3)
            end = min(len(lines), index + 4)
            print("\n".join(f"{i + 1}:{lines[i]}" for i in range(start, end)))
            print("---")
PY

Repository: exactly/exa

Length of output: 631


Pass memory scopes to the evaluations.

assistant uses the Redis-backed Memory, but these calls omit memory.resource and memory.thread. Mastra requires both identifiers for persisted memory. Add an isolated scope for each case and add a multi-turn case that uses the same scope across both calls to test Redis-backed retrieval and conversation isolation.

Comment thread server/utils/chat.ts
Comment on lines +32 to +39
async function send(recipient: string, text: string) {
await request(object({}), `/${from}/messages`, {
messaging_product: "whatsapp",
recipient_type: "individual",
recipient,
type: "text",
text: { body: text },
});

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- server/utils/chat.ts ---'
cat -n server/utils/chat.ts
printf '%s\n' '--- chat.send usages ---'
rg -n -C 5 'chat\.send|send\(' server/api server/utils server/test 2>/dev/null || true
printf '%s\n' '--- request helper definitions/usages ---'
rg -n -C 4 'function request|const request|export .*request|request\(' server
printf '%s\n' '--- related tests ---'
fd -t f . server | rg 'chat|test' | head -80

Repository: exactly/exa

Length of output: 50367


🌐 Web query:

WhatsApp Cloud API send text message request body recipient field to recipient_type

💡 Result:

In the WhatsApp Cloud API, the recipient_type field is a required parameter within the request body when sending messages [1][2]. It defines the nature of the message recipient and must be set to one of the following two values [1][3][2]: individual: Used for 1:1 messaging. When this is specified, the to field should contain the recipient's phone number [1][2]. group: Used for group messaging. When this is specified, the to field should contain the group-id [1][2]. This field is part of the common syntax required for all send message requests to the /messages endpoint [3][4]. For example, when sending a standard text message, the request body is structured as follows [3][5][6]: { "messaging_product": "whatsapp", "recipient_type": "individual", "to": "<WHATSAPP_USER_PHONE_NUMBER>", "type": "text", "text": { "body": "<BODY_TEXT>" } }

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- server/api/chat.ts ---'
sed -n '70,120p' server/api/chat.ts
printf '%s\n' '--- server test layout ---'
fd -t d -d 3 . server/test | sort
printf '%s\n' '--- chat-related tests and fetch mocks ---'
rg -n -C 5 'WhatsApp|whatsapp|graph\.facebook|globalThis\.fetch|fetch\(' server/test | head -240
printf '%s\n' '--- package test configuration ---'
rg -n -C 3 'vitest|server/test|test' package.json vitest.config.* 2>/dev/null || true

Repository: exactly/exa

Length of output: 22717


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import json
payload = {
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "recipient": "+15551234567",
    "type": "text",
    "text": {"body": "test"},
}
print("serialized payload:", json.dumps(payload, separators=(",", ":")))
print("has required `to`:", "to" in payload)
print("has nonstandard `recipient`:", "recipient" in payload)
PY

Repository: exactly/exa

Length of output: 355


Use to for the WhatsApp recipient.

The WhatsApp Cloud API requires to; recipient is not a valid field. Without to, Meta rejects the request and the validation code is not sent. Update the existing assertion in server/test/hooks/chat.test.ts.

proposed fix
-      recipient,
+      to: recipient,
📝 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
async function send(recipient: string, text: string) {
await request(object({}), `/${from}/messages`, {
messaging_product: "whatsapp",
recipient_type: "individual",
recipient,
type: "text",
text: { body: text },
});
async function send(recipient: string, text: string) {
await request(object({}), `/${from}/messages`, {
messaging_product: "whatsapp",
recipient_type: "individual",
to: recipient,
type: "text",
text: { body: text },
});

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

ℹ️ 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/hooks/chat.ts
v.array(
v.object({
id: v.string(),
from_user_id: v.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.

P1 Badge Accept standard WhatsApp sender fields

When Meta delivers the normal phone-number webhook payload, the sender is in messages[].from; from_user_id is the scoped-ID variant used for certain users. Requiring only from_user_id makes Valibot reject the former payloads with 400 before parse() runs, so affected users' inbound messages never reach the assistant. Accept both sender-field variants and normalize them before processing.

Useful? React with 👍 / 👎.


const failure = associate.error
? associate.error instanceof APIError && associate.error.text === "bad code"
? t("That code isn't correct. Check it and try again.")

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 Do not invite retry after consuming the code

When a user mistypes the verification code once, the API has already removed the pending verification via getdel in server/api/chat.ts, but this message keeps the user on the code screen and explicitly tells them to try again. Every subsequent submission therefore returns no verification, making the advertised retry impossible; either preserve the pending value on a mismatch or immediately direct the user through the resend flow.

Useful? React with 👍 / 👎.

Comment thread server/api/chat.ts
if ("token" in payload) {
const whatsappId = await chat.decode(payload.token).catch(() => null);
if (!whatsappId) return c.json({ code: "bad token" }, 400);
if (!(await redis.set(`chat:cooldown:${whatsappId}`, "1", "PX", 60_000, "NX"))) {

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 Distinguish another credential's cooldown

When the same WhatsApp ID requested a code for a different credential during the previous minute, this shared cooldown returns 429 before creating chat:${credentialId} for the current account. The client treats every 429 as evidence that its code was sent and opens the verification screen, but any submitted code then returns no verification; this is especially reachable while moving a number between accounts. Return a distinct result unless a pending verification exists for the current credential, or make the client handle this case as an unsent code.

Useful? React with 👍 / 👎.

queryKey: ["chat", "preflight", value],
queryFn: () => {
if (!value) throw new Error("missing token");
return preflightChat(value);

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 Preserve the association link through sign-in

When the link is opened on a fresh browser or device with no cached credential, this main-group screen calls preflightChat() immediately, whose auth() path attempts a WebAuthn assertion using the default method. There is no redirect to the authentication screens or preserved return URL, so new users and fresh SIWE users only see the generic connection failure and cannot complete the association. Gate this query on authentication and route unauthenticated users through sign-in or account creation while preserving the token.

Useful? React with 👍 / 👎.

<Button
primary
onPress={() => {
openBrowser("https://wa.me", { external: true }).catch(reportError); // TODO append bot number

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 Include the bot number in the WhatsApp CTA

When a user completes the association or needs to request a replacement link, this CTA passes the bare https://wa.me root to Linking.openURL. Without a recipient number it cannot deep-link to the Exa bot conversation, so the user cannot perform the next action described by the surrounding copy. Build the URL with the bot's public WhatsApp number.

Useful? React with 👍 / 👎.

<YStack gap="$s6" paddingHorizontal="$s6" width="100%">
<Feature
icon={<FileChartColumn size={24} color="$uiBrandSecondary" />}
title={t("Check your account 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 Do not advertise an unavailable balance action

When a user follows this advertised capability and asks the bot for their balance, the assistant registers no balance or portfolio tool in server/utils/assistant.ts:28-66 and receives only the account address; no RPC or API balance lookup occurs. The backend therefore cannot supply the promised balance through WhatsApp. Add the corresponding tool before presenting this capability, or remove the claim.

Useful? React with 👍 / 👎.

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