`main` moved next 16.2.6 → 16.2.12 (#441) and applied transitive audit
fixes (#442) since this branch was cut, so both manifests conflicted.
Resolved by taking main's manifests and re-applying only this branch's
`lucide-react` bump, then regenerating package-lock.json with npm 10 —
the version CI's Node 20 ships, so the nested optional-peer @swc/helpers
entry `npm ci` needs is preserved. Verified next 16.2.12, lucide-react
1.27.0, and #442's transitive versions all hold, and `npm ci` succeeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI installs with Node 20 (npm 10), which keeps next-intl's nested
optional-peer @swc/helpers entry; Dependabot generates the lockfile with
npm 11, which drops it, so `npm ci` fails as out-of-sync before any of
lint/typecheck/test/build runs. Regenerated with npm 10 — the only diff
is re-adding that nested entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lockfile-only bumps of indirect dependencies flagged by Dependabot;
no direct dependency or `package.json` entry changes.
- fast-uri 3.1.3 → 3.1.4 (two host-confusion advisories, high)
- js-yaml → 4.3.0 (quadratic CPU via YAML merge-key chains, high)
- hono → 4.12.32 and @hono/node-server → 2.0.12 (cross-request context
disclosure in hono/jsx, serve-static path traversal on Windows)
- body-parser → 2.3.0 (size enforcement silently disabled on an invalid
limit value)
- postcss → 8.5.24
Regenerated with npm 10 to match CI's Node 20, so the nested
`@swc/helpers` optional-peer entry `npm ci` needs is preserved.
Verified: `npm ci`, lint (39 warnings, unchanged from main), typecheck,
645 tests, and `next build` all pass.
Still open after this, and deliberately left alone: `next` (needs a
direct bump — separate branch), `sharp` (major, `--force` only), and
`brace-expansion` nested under the dev-only `@ts-morph/common`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16.2.6 is in the vulnerable range for eight advisories, all patched in
16.2.11: SSRF in rewrites via an attacker-controlled destination
hostname, SSRF in Server Actions on custom servers, middleware/proxy
bypass in App Router, DoS in App Router Server Actions, DoS in the image
optimizer via SVG, unauthenticated disclosure of internal Server
Function endpoints, and two response-body cache-confusion issues.
`eslint-config-next` moves with it to keep the pair on one version.
Lockfile regenerated with npm 10 to match CI's Node 20 — the default
npm 11 drops next-intl's nested optional-peer `@swc/helpers` entry and
breaks `npm ci`.
Verified: lint (39 warnings, unchanged from main), typecheck, 645 tests,
and `next build` all pass on 16.2.12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ensureImageHeaderHandle` fetched the caller-supplied `header_media_url`
server-side with no validation, so a template submit — an action any
authenticated member can perform — could reach loopback, RFC1918 and
cloud-metadata addresses, with the response status and content-type
echoed back through the thrown error.
Wire in the project's existing `isDeliverableUrl` guard, which the two
other outbound-fetch call sites (webhook delivery, automation
`send_webhook`) already use, and reuse their `redirect: 'manual'` +
timeout so a public URL can't 3xx-bounce to an internal one. The
rejection reuses the unreachable-host message verbatim so the failure
doesn't distinguish "blocked" from "down".
Addresses GHSA-6fr5-pxw7-r3xw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The settings page derived its active panel from `useSearchParams()` but
wasn't wrapped in a Suspense boundary. In Next 16 that fails the
production build ("missing Suspense with CSR bailout") and forces the
page to bail to full client-side rendering — shipping a settings screen
whose rail click handlers never wire up. Because the account-menu
Settings link targets `?tab=whatsapp`, users land on the WhatsApp panel
and can't navigate to any other section ("locked on WhatsApp").
Wrap the `useSearchParams` consumer in `<Suspense>` on the three
affected dashboard pages — settings, inbox, automations/new — mirroring
the split login/signup already use. Also add the missing
`Settings.sections.quick-replies` label, which was rendering as a raw
translation key in the settings rail.
Verified: production build now succeeds (previously errored out during
static generation); every settings rail section navigates correctly in a
production build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dependabot's lockfile regeneration drops the nested
node_modules/next-intl/node_modules/@swc/helpers@0.5.23 optional-peer
entry that CI's `npm ci` (Node 20 / npm 10) requires, so the job failed
at install before lint/typecheck/test/build ran. Regenerated
package-lock.json with npm 10 to restore it; no dependency versions change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dependabot's lockfile regeneration drops the nested
node_modules/next-intl/node_modules/@swc/helpers@0.5.23 optional-peer
entry that CI's `npm ci` (Node 20 / npm 10) requires, so the job failed
at install before lint/typecheck/test/build ran. Regenerated
package-lock.json with npm 10 to restore it; no dependency versions change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dependabot's lockfile regeneration drops the nested
node_modules/next-intl/node_modules/@swc/helpers@0.5.23 optional-peer
entry that CI's `npm ci` (Node 20 / npm 10) requires, so the job failed
at install before lint/typecheck/test/build ran. Regenerated
package-lock.json with npm 10 to restore it; no dependency versions change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Login signed in client-side then soft-navigated with router.push into
/dashboard, a route the middleware gates on the Supabase session cookie.
The soft navigation can reach the protected route before the server
observes the freshly written cookies, so the middleware bounces it back
to /login -- which looks like the page "just refreshing" instead of
signing in. It surfaced over ngrok/HTTPS tunnels (added latency widens
the window) while staying hidden on localhost.
- login: navigate with window.location.href after signInWithPassword,
mirroring the deliberate full reload the invite-accept flow already
uses in join/[token]/page.tsx; drops the now-unused useRouter.
- next.config: allow-list common dev tunnel origins (ngrok, cloudflared,
localtunnel) so Next 16 stops 403-ing dev-only resources over the
tunnel, extendable via ALLOWED_DEV_ORIGINS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds messages/ko.json — full next-intl translation covering all 12
namespaces (LoginPage, Sidebar, Header, ModeToggle, Dashboard, Inbox,
Contacts, Pipelines, Broadcasts, Automations, Flows, Settings).
Scoped to the language file only, per maintainer guidance on PR #362:
no changes to the default locale, component text, or any other file.
Verified 1:1 key parity with messages/en.json (1426/1426 leaf keys)
and exact preservation of all interpolation variables and HTML-ish
tags across every message.
An inbound message could create a second conversation for a contact
under a race (Meta retries a delivery, or a batch fans out to concurrent
after() runs). There was no UNIQUE index and no unique-violation
backstop on conversations, unlike contacts (migration 022). Worse, once
two conversations existed the `.single()` lookup errored on every later
message and the webhook created yet another conversation each time,
snowballing into a wall of duplicate chats.
- findOrCreateConversation now resolves oldest-first with .limit(1)
instead of .single(), so pre-existing duplicates converge, and catches
23505 on insert to re-resolve the winning row.
- Same hardening applied to the public-API resolver (resolve-conversation).
- Migration 036 merges existing duplicate conversations into the oldest
thread (re-pointing messages, reactions, deals, flow_runs,
notifications, ai_usage_log — no data loss) then adds a
UNIQUE (account_id, contact_id) index as the authoritative guarantee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add full support for WhatsApp Cloud API interactive messages across the
inbox, automations, and a new quick-replies feature. Extends the
interactive plumbing that previously existed only for the Flows engine.
Sending
- send-message core handles `interactive` (reply buttons ≤3, list ≤10
rows) via the existing Meta senders; validated server-side before send.
- Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style
preview + Meta char limits (button 20, list row 24, body 1024).
- Automation "Send Buttons" / "Send List" steps in the builder.
- Public v1 /messages accepts type=interactive.
Storage / round-trip
- migration 035: messages.interactive_payload JSONB + quick_replies table.
- Outbound interactive persists the structured payload (composer,
automations, and Flows) so the thread re-renders the buttons/rows.
Receiving
- Bubble renders outbound interactive via the shared preview; inbound taps
(button_reply/list_reply) keep the "chosen option" affordance.
Automations
- New `interactive_reply` trigger matches on tapped button/list IDs
(exact), enabling chained multi-step menus.
Quick replies
- New account-scoped quick_replies (text or interactive snippets), managed
in Settings and insertable from the composer; "Save as quick reply" in
the builder.
Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/.
Includes unit tests for the validator, send-param validation, the
interactive_reply trigger match, and the new automation step/trigger
validation. typecheck + lint clean; full suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP registry namespace is case-sensitive and keyed to the GitHub
login: the owner is `ArnasDon`, not `arnasdon`. Publishing as
io.github.arnasdon/wacrm-mcp is rejected 403. Fix the name in
server.json and the matching mcpName in package.json to
io.github.ArnasDon/wacrm-mcp.
Bump to 0.1.1 because the published 0.1.0 baked in the lowercase
mcpName; the registry validates the published package's mcpName
against server.json, so a fresh npm version is required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shorten description to the registry's 100-char limit and migrate the
$schema to the current 2025-12-11 version (2025-10-17 is deprecated).
`mcp-publisher validate` now passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce mcp-server/ — a stdio Model Context Protocol server that
wraps the /api/v1 public API so MCP clients (Claude, Cursor, etc.) can
drive a self-hosted wacrm instance in natural language.
- Thin wrapper over the existing REST API; all auth, scoping, and rate
limiting stay server-side. No new backend surface.
- Read-only by default: write and broadcast tools are only registered
when WACRM_ENABLE_WRITES / WACRM_ENABLE_BROADCASTS are set, layered
on top of the API key's own scopes. send_broadcast also requires an
explicit confirm and is marked destructive.
- Registry-ready: server.json manifest, package.json mcpName linked to
it (io.github.arnasdon/wacrm-mcp), and glama.json for discovery.
- Docs: docs/mcp.md + README bullet + mcp-server/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings in the i18n PR (#348) that landed on main and localizes the new
strings this branch added so nothing regresses the new next-intl
convention:
- ai-config.tsx: resolved import conflict; handoff-target picker strings
moved to Settings.aiConfig (handoffTo / handoffToDesc / handoffQueue).
- message-bubble.tsx: "AI" badge + tooltip → Inbox.bubble
(aiBadge / aiBadgeTitle).
- ai-thread-banner.tsx: take-over/resume banner + toasts → new
Inbox.aiBanner namespace.
The AI Agents section (/agents playground, setup tabs, usage card) is
still English on main, so those strings are left as-is for a future
section-wide i18n pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The security PR (#352) shipped 031_fix_profiles_update_rls.sql while
031_ai_reply_slot_grant.sql (#345) already held version 031. Supabase
keys schema_migrations on the numeric prefix, so applying both to a fresh
DB fails with `duplicate key ... (version)=(031)` (SQLSTATE 23505) — which
is what broke the Supabase Preview check on this PR.
Rename the newer, colliding file to 034 to restore unique versions. It's
idempotent and independent of the AI tables, so re-sequencing after 033 is
safe; a prod DB that already applied 031 will apply this fresh as 034
without side effects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correctness:
- Resume AI now releases ANY assignment (not just the caller's own).
A handoff-assigned thread previously stayed assigned after resume, so
the eligibility gate silently kept the bot muted — Resume was a no-op.
- GET /api/ai/usage: guard days >= 1 (Number(null)===0 is finite, so the
30-day default was unreachable and the window collapsed to 1 day).
- GET /api/ai/usage: align the query cutoff to the start of the oldest
charted local day so rows in the oldest partial day no longer count in
totals while missing from the daily series (totals now match the bars).
- ai-thread-banner: cache account AI-status per accountId and never cache
a transient fetch failure — fixes cross-account leak and the banner
staying hidden all session after one failed /api/ai/config call.
- /api/ai/draft: construct the service-role client inside a guard and
fire-and-forget the usage log, so a missing service-role key (or any
log failure) can't 500 an already-generated, already-billed draft.
Efficiency:
- Don't await usage logging on the reply hot path (auto-reply + draft);
logAiUsage swallows its own errors, so fire-and-forget is safe and
removes a DB round-trip from the customer-facing send.
Reuse / consistency:
- usage route reuses date-utils (daysAgoStart/lastNDayKeys/localDayKey)
and now buckets by LOCAL day like every other dashboard chart (was UTC).
- extract formatCompactNumber (shared with formatCurrencyShort); drop the
hand-rolled month table in favour of date-fns; extract
fetchAccountMembers/memberLabel and use them in the AI settings picker.
Resume still resets the per-conversation reply count (a deliberate,
manual, rate-limited re-engagement — documented in the handler).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>