Commit Graph

183 Commits

Author SHA1 Message Date
Arnas Donauskas
742d9e0caa
Bump next 16.2.4 → 16.2.6 (security)
Patches 11 open Dependabot alerts on Next.js — 10 High + 1 Low:

- SSRF in WebSocket upgrades (GHSA-c4j6-fc7j-m34r)
- DoS in Server Components (GHSA-8h8q-6873-q5fj)
- DoS in Cache Components via connection exhaustion (GHSA-mg66-mrh9-m8jx)
- DoS in Image Optimization API (GHSA-h64f-5h5j-jqjh)
- Middleware/Proxy bypass via segment-prefetch routes
  (GHSA-267c-6grr-h53f, GHSA-26hh-7cqf-hhc6 follow-up)
- Middleware/Proxy bypass via i18n in Pages Router
  (GHSA-36qx-fr4f-26g5)
- Middleware/Proxy bypass via dynamic route param injection
  (GHSA-492v-c6pp-mqqv)
- XSS in beforeInteractive scripts (GHSA-gx5p-jg67-6x7h)
- XSS in CSP-nonce App Router apps (GHSA-ffhc-5mcf-pf4q)
- Cache poisoning in RSC responses (GHSA-wfc6-r584-vfw7)
- Cache-busting collision in RSC (GHSA-vfv6-92ff-j949, Low)
- Cache-poisoned Middleware redirects (GHSA-3g8h-86w9-wvmq, Low)

Also bumps eslint-config-next to 16.2.6 to keep major/minor lockstep
with next, per Vercel's docs.

Verified locally:
  npm run lint    — 0 errors (23 pre-existing warnings)
  npm run typecheck — clean
  npm test         — 89 passed, 7 files
  npm run build    — succeeded

The remaining 11 alerts (postcss/ip-address/fast-uri/hono) are
transitive and ship via the shadcn CLI's tree; they'll be patched
separately via npm overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:25:03 +03:00
Arnas Donauskas
e80189ec97
Merge pull request #92 from ArnasDon/test/foundational-vitest-suite
test: foundational Vitest suite for security + critical pure logic
2026-05-18 16:59:32 +03:00
Arnas Donauskas
db11f76226
test: foundational Vitest suite for security + critical pure logic
Adds the first test layer to the repo. Vitest is wired into the
existing CI workflow between typecheck and build so every PR gets the
same gating before merge. No source code under test was modified; this
PR only adds the test framework + tests.

Coverage focuses on the highest-impact pure modules — security-critical
crypto/HMAC, the phone-number pipeline every outbound message touches,
the rate-limiter that protects send/broadcast, automation activation
validation, and dashboard date math. 89 tests across 7 files; full run
is ~300ms.

Modules covered:
- lib/whatsapp/encryption — GCM round-trip, tamper detection (auth tag,
  ciphertext, IV length, auth-tag length), legacy CBC compat, format
  detection, malformed input
- lib/whatsapp/webhook-signature — valid/invalid HMAC, missing header,
  wrong prefix, length-mismatch guard, fail-closed when secret unset
- lib/whatsapp/phone-utils — sanitize, normalize, phonesMatch with
  trunk-prefix tail fallback, isValidE164 boundaries (7–15 digits,
  non-zero start), phoneVariants insert/remove with dedupe,
  isRecipientNotAllowedError detection
- lib/rate-limit — first-request math, exact-limit boundary, per-key
  isolation, window expiry with fake timers, 429 response shape,
  Retry-After clamp
- lib/automations/validate — every step type's required fields, wait
  amount/unit boundaries, webhook URL validation (missing / non-http /
  malformed), assign_conversation mode branching, recursive condition
  walking with stable dot-paths, unknown step types
- lib/broadcast-status — known status lookups, pulse flag, dark-theme
  class shape, fallback for unknown strings
- lib/dashboard/date-utils — startOfLocalDay, daysAgoStart, localDayKey
  zero-padding, lastNDayKeys (1, N, month crossing), Monday-first DOW

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:06:02 +03:00
Arnas Donauskas
6820314fe2
Merge pull request #90 from ArnasDon/feat/inbox-template-picker
feat(inbox): wire up the template picker dialog
2026-05-18 15:44:11 +03:00
Arnas Donauskas
fbb95eb2a7
Merge branch 'main' of https://github.com/ArnasDon/wacrm into feat/inbox-template-picker
# Conflicts:
#	src/components/inbox/message-thread.tsx
2026-05-18 15:43:39 +03:00
Arnas Donauskas
149a4e67ef
Merge pull request #91 from ArnasDon/chore/remove-broadcast-schedule-stub
chore(broadcasts): drop the inert Schedule for Later tile
2026-05-18 15:39:54 +03:00
Arnas Donauskas
ac55c74ad4
Merge pull request #89 from ArnasDon/fix/inbox-assign-button
feat(inbox): wire conversation assignment dropdown
2026-05-18 15:38:33 +03:00
Arnas Donauskas
8d83220df4
chore(broadcasts): drop the inert Schedule for Later tile
The Pro-badged Schedule for Later tile in step 4 was a non-interactive
div — no handler, no scheduling backend, and no Pro tier in the product.
Send Immediately was the only path that did anything, so the picker
itself was decorative. Remove the section (and the now-unused timing
state / Clock / Lock imports). The wizard still completes Send
Immediately end-to-end via the existing Send Broadcast button below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:26:47 +03:00
Arnas Donauskas
2a86781c85
feat(inbox): wire up the template picker dialog
handleOpenTemplates flipped templateModalOpen but nothing rendered when
true, so both composer template buttons were dead. Add a TemplatePicker
dialog that lists Approved templates, prompts for any {{N}} variables,
then sends through the existing /api/whatsapp/send route with
message_type=template. The rendered body is stored as content_text so
the thread bubble and conversation preview show the actual message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:25:45 +03:00
Arnas Donauskas
5d0944e087
feat(inbox): wire conversation assignment dropdown
Replace the no-op Assign button in the thread header with a dropdown that
reads/writes conversations.assigned_agent_id. Lists profiles visible
under RLS (single-tenant today, ready for shared workspaces) and offers
Unassign when the conversation is already claimed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:14:42 +03:00
Arnas Donauskas
1e6d70a878
Merge pull request #88 from ArnasDon/chore/extract-marketing-site
Extract marketing site to wacrm-site repo
2026-05-18 12:34:18 +03:00
Arnas Donauskas
15a05eb280
Extract marketing site to ArnasDon/wacrm-site
Forks of this repo no longer inherit the wacrm.tech landing page
and docs — those move to the dedicated marketing-site repo at
https://github.com/ArnasDon/wacrm-site (deployed to wacrm.tech).

Changes here:
- Replace src/app/page.tsx with a redirect to /dashboard.
  Middleware already handles the unauth → /login bounce, so this
  works for both auth states.
- Drop src/app/{docs,opengraph-image.tsx,robots.ts,sitemap.ts}.
- Drop src/components/{landing,docs,seo} and src/lib/{seo,docs}.
- Drop the top-level docs/ markdown — moved to wacrm-site/docs.
- Slim src/app/layout.tsx metadata down to product chrome
  (no public OG cards, robots: noindex).
- Drop `marked` from dependencies; refresh package-lock.
- Refocus README on self-hosting; point readers at wacrm.tech/docs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:21:13 +03:00
Arnas Donauskas
a501b95a2b
Merge pull request #79 from ArnasDon/dependabot/npm_and_yarn/typescript-6.0.3
chore(deps-dev): bump typescript from 5.9.3 to 6.0.3
2026-05-18 10:52:51 +03:00
Arnas Donauskas
f92b686055
Merge pull request #80 from ArnasDon/dependabot/npm_and_yarn/base-ui/react-1.4.1
chore(deps): bump @base-ui/react from 1.4.0 to 1.4.1
2026-05-18 10:52:29 +03:00
Arnas Donauskas
8527c6af3e
Merge pull request #87 from ArnasDon/dependabot/npm_and_yarn/supabase-cc07b7ee10
chore(deps): bump the supabase group across 1 directory with 2 updates
2026-05-18 10:52:05 +03:00
dependabot[bot]
aa9e95449f
chore(deps): bump the supabase group across 1 directory with 2 updates
Bumps the supabase group with 1 update in the / directory: [@supabase/ssr](https://github.com/supabase/ssr).


Updates `@supabase/ssr` from 0.10.2 to 0.10.3
- [Release notes](https://github.com/supabase/ssr/releases)
- [Changelog](https://github.com/supabase/ssr/blob/main/CHANGELOG.md)
- [Commits](https://github.com/supabase/ssr/compare/v0.10.2...v0.10.3)

Updates `@supabase/supabase-js` from 2.103.3 to 2.105.4
- [Release notes](https://github.com/supabase/supabase-js/releases)
- [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-js/commits/v2.105.4/packages/core/supabase-js)

---
updated-dependencies:
- dependency-name: "@supabase/ssr"
  dependency-version: 0.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: supabase
- dependency-name: "@supabase/supabase-js"
  dependency-version: 2.105.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: supabase
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 07:45:21 +00:00
Arnas Donauskas
a54a737bb5
Merge pull request #77 from ArnasDon/dependabot/npm_and_yarn/types/node-25.6.0
chore(deps-dev): bump @types/node from 20.19.39 to 25.6.0
2026-05-18 10:43:04 +03:00
Arnas Donauskas
c98d382419
Merge pull request #74 from ArnasDon/dependabot/github_actions/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2026-05-15 16:27:24 +03:00
Arnas Donauskas
3e06698847
Merge pull request #75 from ArnasDon/dependabot/github_actions/actions/setup-node-6
chore(deps): bump actions/setup-node from 4 to 6
2026-05-15 16:26:02 +03:00
Arnas Donauskas
9bdbfead6f
Merge pull request #86 from ArnasDon/chore/footer-trademark-disclaimer
chore: add Meta trademark / non-affiliation disclaimer to public footers
2026-05-06 13:54:43 +03:00
Arnas Donauskas
280e5b8cd9
Merge pull request #85 from ArnasDon/chore/whatsapp-trademark-symbol
chore: add WhatsApp® on first mention across user-facing surfaces
2026-05-06 13:52:23 +03:00
Arnas Donauskas
1f2849df75
Merge remote-tracking branch 'origin/main' into chore/whatsapp-trademark-symbol
# Conflicts:
#	README.md
#	docs/README.md
#	docs/automations-and-cron.md
#	docs/supabase-setup.md
#	docs/whatsapp-setup.md
#	src/app/opengraph-image.tsx
#	src/components/landing/hero.tsx
#	src/lib/seo/site-config.ts
2026-05-06 13:50:03 +03:00
Arnas Donauskas
f44c123267
Merge pull request #84 from ArnasDon/chore/hostinger-accent-palette
chore: switch accent palette to Hostinger violet, drop emerald green
2026-05-06 13:43:10 +03:00
Arnas Donauskas
4bb157c705
Merge remote-tracking branch 'origin/main' into chore/hostinger-accent-palette
# Conflicts:
#	src/app/icon.tsx
#	src/components/docs/shell.tsx
#	src/components/landing/footer.tsx
#	src/components/landing/nav.tsx
2026-05-06 13:35:46 +03:00
Arnas Donauskas
9843fb823f
Merge pull request #83 from ArnasDon/chore/rename-crm-template-for-whatsapp
chore: rename product to "CRM Template for WhatsApp"
2026-05-06 13:23:51 +03:00
Arnas Donauskas
c3dde01229
chore: add Meta trademark / non-affiliation disclaimer to public footers
Per Meta's brand-use guidance for third-party products, sites that
reference WhatsApp must include a clear non-affiliation disclaimer in
the footer. Adds the standard line:

  "WhatsApp® is a registered trademark of Meta Platforms, Inc.
   Hostinger is not affiliated with, endorsed by, or sponsored by
   Meta Platforms, Inc."

Placed in:
- src/components/landing/footer.tsx — marketing footer (every public
  landing page).
- src/components/docs/shell.tsx — public docs pages had no footer at
  all; added a slim one carrying the same disclaimer.

Out of scope:
- Authed dashboard pages don't have a footer (full-screen workspace
  layout). The trademark-fair-use risk is concentrated on public
  surfaces — crawlers, visitors who haven't signed up, social shares.
  Skipping the dashboard keeps the workspace layout intact and the
  disclaimer where it does the most work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:15:08 +03:00
Arnas Donauskas
e46844d527
chore: add WhatsApp® on first mention across user-facing surfaces
Per Meta's brand-use guidance for third-party products, the registered
trademark symbol (®) should appear on the FIRST mention of "WhatsApp"
on each user-facing page. Subsequent mentions on the same page are
left plain.

Surfaces touched (one-per-page first mention):
- Landing hero badge ("Built on the official WhatsApp® Business API").
- src/lib/seo/site-config.ts: SITE_TAGLINE and SITE_DESCRIPTION
  (rendered into <title> and <meta description> on every page).
- src/app/opengraph-image.tsx: hero text in social-share image.
- Settings, Inbox connection banner, Automations page subtitle.
- README.md tagline.
- All public docs pages (docs/README.md, docs/whatsapp-setup.md,
  docs/getting-started.md, docs/supabase-setup.md,
  docs/environment-variables.md, docs/deployment-hostinger.md,
  docs/automations-and-cron.md, docs/architecture.md,
  docs/troubleshooting.md).

Out of scope: SITE_KEYWORDS (the meta-keywords array isn't
user-rendered text — putting ® on every keyword would look broken in
the meta tag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:13:26 +03:00
Arnas Donauskas
77dfa4f165
chore: switch accent palette to Hostinger violet, drop emerald green
The legal brief on third-party trademark use says: avoid the WhatsApp
teal/green palette and any visual treatment that could read as a
WhatsApp/Meta product. Emerald-500 was used as the primary brand colour
across hero, CTAs, sidebar, charts, badges, and the favicon — close
enough in hue to WhatsApp's signature green to muddy the line.

Swapping to Tailwind violet (which matches the Hostinger brand purple
#7c3aed) makes the visual identity unambiguously ours.

Scope:
- Mechanical Tailwind class swap: emerald-* → violet-* across src/.
- src/app/globals.css: --primary, --ring, --sidebar-primary, --sidebar-ring,
  --chart-1 oklch tokens swapped from emerald (oklch 0.696 0.17 162) to
  Hostinger-aligned violet (oklch 0.526 0.247 293).
- src/app/icon.tsx: favicon background #10b981 → #7c3aed.
- src/app/opengraph-image.tsx: badge bg, accent text, gradient.
- src/components/landing/hero.tsx + cta-banner.tsx: radial-gradient hex.
- src/components/landing/mock/analytics-mock.tsx: marketing chart line.
- src/components/dashboard/conversations-chart.tsx + response-time-chart.tsx:
  "Outgoing" series colour. Distinguishable from "Incoming" blue at a
  glance; we can revisit the chart pair later for stronger colourblind
  contrast (separate concern).
- README.md: license badge colour parameter.

Out of scope:
- src/components/settings/tag-manager.tsx still offers "Emerald" as one
  of the user-pickable contact-tag colours alongside Blue / Yellow / Red
  / etc. — that's a user data choice, not a brand mark.

Contrast check: white text on #7c3aed measures ~4.78:1 (WCAG AA passing
for normal body text). Violet-400 (#a78bfa) on slate-950 measures ~10:1
for accent text, well clear of AA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:57:12 +03:00
Arnas Donauskas
faee4eea44
chore: rename product to "CRM Template for WhatsApp"
Drops the "WaCRM" branded name across user-facing surfaces in favour
of a descriptive name. Branded names that imply WhatsApp ownership or
sponsorship are a trademark fair-use risk; a relationship-based name
("CRM Template for WhatsApp") sidesteps that.

Scope:
- Brand text in nav, sidebar, footer, docs shell, mock window chrome.
- SITE_NAME / SITE_DESCRIPTION / legalName in src/lib/seo/site-config.ts.
- README, CONTRIBUTING, docs/, .github/ templates and policies.
- package.json description.
- Auth pages, settings copy, FAQ, code comments.

Out of scope (intentionally — these are infra renames the project
owner should drive separately so links and the package lockfile stay
consistent):
- The github.com/ArnasDon/wacrm repo URL.
- The wacrm.tech domain / homepage field.
- The package.json "name" field.
- The Hostinger application name and Meta system-user name in deploy
  docs (existing-deployment instructions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:52:11 +03:00
Arnas Donauskas
ea0af2fe3e
Merge pull request #82 from ArnasDon/chore/fix-lint-backlog
ci: clear lint error backlog, drop continue-on-error on lint step
2026-04-21 09:43:15 +03:00
Arnas Donauskas
047b981821
ci: clear lint error backlog, drop continue-on-error on lint step
Fixes the 14 pre-existing lint errors surfaced by React 19's new
hook rules so CI can enforce lint on every PR instead of tolerating
failures. The errors split roughly into three camps:

1) react-hooks/refs — "Cannot access refs during render"
   4 occurrences of the "latest ref" pattern that assigned
   `ref.current = x` during render. Moved each assignment into a
   `useEffect(() => { ref.current = x })` — the canonical React-19
   pattern. Consumers only read `.current` inside async callbacks
   that run after commit, so the very slight delay to the update is
   fine.
   Touched: src/hooks/use-realtime.ts (onMessageRef,
   onConversationRef), src/components/inbox/message-thread.tsx
   (onMessagesLoadedRef), src/components/inbox/conversation-list.tsx
   (onConversationsLoadedRef).

2) react-hooks/set-state-in-effect — genuine issues
   - contacts/page.tsx: the `setPage(0)` on search change moved from
     a useEffect into the `onChange` handler of the search input,
     which is the cleaner pattern anyway.
   - settings/page.tsx: the local `tab` state + sync effect were
     removed entirely; the URL query param is now the single source
     of truth, and the `onChange` handler just calls
     `router.replace(...)`. Simpler and fixes the lint error.

3) react-hooks/set-state-in-effect — false positives for
   load-on-mount and prop-driven sync
   Suppressed per-call (or per-block for effects with many setters)
   with explanatory comments. These effects use setters inside
   async Supabase-query callbacks, not synchronously in the effect
   body, so the cascade the rule warns about doesn't apply. Files:
   contacts/page.tsx, pipelines/page.tsx, pipelines/deal-form.tsx,
   pipelines/pipeline-settings.tsx,
   components/inbox/contact-sidebar.tsx.

4) react-hooks/preserve-manual-memoization — one useCallback
   Rule complained that the inferred dep was `contact` but the
   source dep list was `[contact?.phone]`. Broadened the manual dep
   to `[contact]` to match — callback behaviour is unchanged (it
   still only reads `contact?.phone`), and the React Compiler can
   now preserve the memoization.

And:
  - .github/workflows/ci.yml: dropped `continue-on-error: true` on
    the lint step. Lint now blocks merge on any new error.

Verified: `npm run lint` → 0 errors (24 warnings remain; all
pre-existing unused-import / missing-dep / <img> warnings that don't
fail CI). `npm run typecheck` clean. Landing, /docs, /login, and
/settings redirects all still respond correctly from the dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:28:35 +03:00
Arnas Donauskas
93158f4b82
Merge pull request #81 from ArnasDon/docs/readme-arch-alt-hosts
docs: README polish + architecture
2026-04-20 21:52:48 +03:00
Arnas Donauskas
a9df9cd3bf
docs: drop alternative-hosts page, keep Hostinger as the only path
The template is about shipping on Hostinger. Offering Vercel / Railway
/ raw VPS / Docker alongside it blurs that story and creates support
surface for hosts we don't actually test against.

- Delete docs/deployment-alternatives.md.
- README Deploy list drops the "Alternative hosts" bullet.
- docs/README.md reading-order drops step 6 and renumbers.
- docs/architecture.md drops its trailing "Next step" pointer to
  the removed page.
- src/lib/docs/content.ts drops the deployment-alternatives entry;
  remaining pages renumbered to 5..8.

Verified: /docs sidebar now lists 8 pages,
/docs/deployment-alternatives → 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:51:10 +03:00
Arnas Donauskas
7cd0f46369
docs: README polish + architecture + alt hosts
Finishes the template-readiness sequence. All docs — nothing runtime.

README.md
  Rewrites the landing page of the repo to actually sell what it is
  to someone finding the repo cold on GitHub search.
  - Shields.io badges: MIT, CI status, Next.js 16, Supabase, GitHub
    stars.
  - "What you get out of the box" bullet list — inbox, contacts,
    pipelines, broadcasts, automations, dashboard, account
    management — so the feature surface is visible before the reader
    scrolls.
  - "Why fork this?" section: full ownership, full customisation,
    zero ops to start, real security primitives. Frames WaCRM as a
    template, not an SDK.
  - Docs section reorganised into Setup / Deploy / Reference to
    match the in-app /docs viewer and docs/README.md.
  - Contributing section rewrites the stale "Pull requests welcome"
    line to match the fork-first framing from PR #73.

docs/architecture.md  (new)
  One-page tour for forkers: stack table, full folder layout,
  two ASCII request-lifecycle diagrams (inbound webhook + outbound
  send), security primitives rundown, and a "where to change things"
  index that maps common customisation tasks to source files. This
  is the thing I wish I'd had the first time I opened a forked
  template.

docs/deployment-alternatives.md  (new)
  Short blocks for Vercel, Railway, raw VPS, and Docker. Each covers
  env-var setup, cron strategy (Vercel cron without custom headers
  is a footgun worth calling out), and trade-offs. Keeps Hostinger
  as the primary recommendation — this page is for people whose
  constraints rule it out.

docs/README.md
  - Reading order grouped as Setup / Deploy / Reference, adding the
    two new pages.
  - Stack-at-a-glance updated: encryption is AES-256-GCM, not CBC
    (the PR #71 change that earlier docs hadn't caught up to).

src/lib/docs/content.ts
  Registers the two new markdown files in the ORDER map so they
  appear in the in-app /docs sidebar under the Reference section,
  with prev/next pagination wiring.

Verified: /docs, /docs/architecture, /docs/deployment-alternatives,
and /docs/getting-started all return 200 from the dev server.
Sidebar shows 9 pages grouped Setup / Deploy / Reference with the
new entries. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:43:42 +03:00
dependabot[bot]
59be9c740b
chore(deps): bump @base-ui/react from 1.4.0 to 1.4.1
Bumps [@base-ui/react](https://github.com/mui/base-ui/tree/HEAD/packages/react) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/mui/base-ui/releases)
- [Changelog](https://github.com/mui/base-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/base-ui/commits/v1.4.1/packages/react)

---
updated-dependencies:
- dependency-name: "@base-ui/react"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:36:41 +00:00
dependabot[bot]
869222aef3
chore(deps-dev): bump typescript from 5.9.3 to 6.0.3
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:36:29 +00:00
dependabot[bot]
46bf366660
chore(deps-dev): bump @types/node from 20.19.39 to 25.6.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.39 to 25.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:36:19 +00:00
dependabot[bot]
ee7f7a8916
chore(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:35:36 +00:00
dependabot[bot]
d8b6b86525
chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:35:35 +00:00
Arnas Donauskas
9c25971c27
Merge pull request #73 from ArnasDon/chore/contributor-experience
chore: fork-first contributor docs + housekeeping
2026-04-20 21:34:52 +03:00
Arnas Donauskas
45574ec7ee
chore: reframe contributor docs fork-first
WaCRM is a template — users are expected to fork and customise their
own deployment, not to send feature work back upstream. The previous
CONTRIBUTING.md read like a standard collaborative-project onboarding
guide and set the wrong expectation.

Reframed docs:

- CONTRIBUTING.md is now "Using WaCRM". Opens with "fork it and make
  it yours", covers forking + keeping-in-sync commands, explains
  which PRs do tend to land (security, correctness, docs, small
  nits) vs. which belong in a fork (new features, stack swaps,
  opinionated refactors). Keeps the filename so GitHub's built-in
  "Contributing guidelines" link still surfaces it.

- .github/ISSUE_TEMPLATE/feature_request.yml: the intro block now
  tells the forker up front that feature requests usually become
  "build it in your fork", and describes the narrow cases where an
  upstream request is useful.

- .github/ISSUE_TEMPLATE/config.yml: adds a third contact link
  pointing at CONTRIBUTING.md so people see the fork-first framing
  before filing.

- .github/pull_request_template.md: prepends a heads-up that most
  changes belong in the forker's own repo, with a pointer to
  CONTRIBUTING.md.

Nothing else in the PR changes — CoC, Dependabot, EditorConfig,
Prettier configs, issue bug_report.yml, and the package.json changes
are orthogonal to the fork-first framing and stay as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:31:50 +03:00
Arnas Donauskas
3799c224d8
chore: contributor experience — CoC, templates, Dependabot, editor configs
Nine paperwork files + one devDep. All support work for a repo that's
about to be public; zero runtime behaviour change.

- CONTRIBUTING.md at root. Dev loop, script reference, PR style, code
  style, review process. Short on prose, long on specifics.
- .github/CODE_OF_CONDUCT.md adopts Contributor Covenant 2.1 by
  reference (no inlined text). Points reports to the maintainer's
  email and GitHub's private flow.
- .github/ISSUE_TEMPLATE/ — structured YAML forms for bug and feature
  reports, plus config.yml that disables blank issues and surfaces
  the security-disclosure + docs links. The bug form asks for
  version, runtime, and logs up front so triage doesn't start with
  a round of "where is it running?".
- .github/pull_request_template.md — short checklist; nudges toward
  typecheck / lint / build before submit without being long enough
  to skip.
- .github/dependabot.yml — weekly npm + github-actions scans,
  grouped so Supabase / @types / dev-only patches batch into single
  PRs instead of six per week. Next / React / Tailwind / eslint-
  config-next are ignored so majors stay manual.
- .editorconfig — LF, UTF-8, 2 spaces, final newline, strip trailing
  whitespace (except in Markdown where trailing spaces are line
  breaks).
- .prettierrc — single quotes + trailing-comma es5, matches the
  plurality quote style in the codebase (336 single vs 236 double
  in imports). Tailwind plugin sorts class names.
- .prettierignore — node_modules, build output, lockfile, migrations.
- package.json adds "format" / "format:check" scripts and pins
  prettier + prettier-plugin-tailwindcss as devDeps.
- package-lock.json refreshed for the new devDeps.

Important: existing source files are NOT reformatted in this PR. 153
files would change if we ran `npm run format`, which would balloon
the diff and make review impossible. The reformat pass is a separate,
deliberate PR.

Typecheck clean; `npx prettier --check` on the new config files
passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:16:21 +03:00
Arnas Donauskas
1965075ea2
Merge pull request #72 from ArnasDon/chore/oss-metadata
chore: OSS metadata + LICENSE + SECURITY + complete env example
2026-04-20 20:45:03 +03:00
Arnas Donauskas
bce345ed03
chore: OSS metadata + LICENSE + SECURITY + complete env example
Prep work so the repo is ready to be public. None of this is runtime
code; it's the governance a forker expects on day one.

- LICENSE: MIT at repo root. README already claimed MIT; this makes
  GitHub actually recognise it and show the license badge.

- .github/SECURITY.md: private disclosure path via GitHub Security
  Advisories or email. Spells out scope, safe harbour, and what the
  reporter should expect — so a researcher finds it before they try
  to file a public issue.

- .env.local.example: fleshed out with the three vars it was missing
  — META_APP_SECRET (required, from PR 1), NEXT_PUBLIC_SITE_URL
  (recommended), and AUTOMATION_CRON_SECRET (optional). Grouped as
  Required / Recommended / Optional with a one-line comment per
  entry. Includes the openssl/node generators for the secrets.

- package.json: description, repository, bugs, homepage, keywords,
  license, author, engines.node >=20. No runtime impact; feeds npm
  registry metadata, GitHub's repo sidebar, and prevents forkers from
  silently installing on Node <20 (Next 16's floor).

Typecheck clean; npm install run to refresh the lockfile for the
package.json metadata additions (no dependency changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:41:14 +03:00
Arnas Donauskas
d6a4677e6c
Merge pull request #71 from ArnasDon/security/encrypt-aes-gcm
security: switch WhatsApp token encryption from AES-CBC to AES-GCM
2026-04-20 20:32:24 +03:00
Arnas Donauskas
e570e1a8f2
security: switch WhatsApp token encryption from AES-CBC to AES-GCM
CBC without a MAC is unauthenticated — anyone who can write rows to
whatsapp_config can flip bits in the stored access_token or
verify_token without the decrypt throwing. You'd silently get garbled
bytes; worst case, if the mutation happens to produce a valid token,
messages go out under a spoofed account. GCM appends a 16-byte
authentication tag; any tampering fails the decrypt.

## What changed

src/lib/whatsapp/encryption.ts
  - encrypt() now emits AES-256-GCM ciphertexts with a 12-byte IV,
    formatted as `<iv>:<ciphertext>:<authTag>` (three colons).
  - decrypt() auto-detects format by splitting on `:`:
      3 parts → GCM, validated via auth tag.
      2 parts → legacy CBC, read-only compatibility path.
    Callers are unchanged; same signature, same return type.
  - isLegacyFormat() exposes the format check so hot paths can
    opportunistically upgrade old rows.
  - IV length is validated on decrypt (12 for GCM, 16 for CBC) to
    fail fast on corrupted input.

## Self-healing existing rows

Two call sites opportunistically upgrade a legacy row to GCM after a
successful decrypt — fire-and-forget, no latency impact on the request
path, idempotent under concurrent writes.

  - src/app/api/whatsapp/send/route.ts (hot path)
      After decrypting access_token, if it was CBC, we schedule an
      UPDATE that writes a fresh GCM ciphertext back to the same row.
      The first send after deploy upgrades the token; subsequent
      sends are no-ops because isLegacyFormat returns false.

  - src/app/api/whatsapp/webhook/route.ts (subscribe handshake)
      When a config's verify_token matches the inbound
      hub.verify_token, we upgrade it in place if it was legacy.
      Runs during Meta's webhook re-subscription events, which
      happen on config changes and periodic refreshes.

No schema migration required — the DB column holds whatever string
encrypt() produces. Operators who want to force an upgrade for a
particular user can clear and re-save the WhatsApp settings form.

## Verified (tsx smoke test)
  - GCM roundtrip works (plaintext in → same plaintext out).
  - Legacy CBC decrypt works on 2-part format.
  - isLegacyFormat distinguishes new/legacy correctly.
  - Bit-flipped GCM ciphertext throws on decrypt.
  - Forged auth tag throws on decrypt.
  - Same plaintext produces different ciphertexts across calls
    (IV randomness).

## Rollback

Revert this file. Existing GCM rows would then fail to decrypt
because the previous encrypt.ts has no branch for the 3-part format.
Safer rollback: keep the decrypt branches, revert only the encrypt()
body to CBC — but since GCM roundtrips cleanly and the PR's bug
surface is small (pure crypto utility), a straight revert should only
matter if a forker had time between merging this PR and reverting to
accumulate GCM rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:25:48 +03:00
Arnas Donauskas
7182abe7d8
Merge pull request #70 from ArnasDon/fix/inbox-messages-stale-after-deeplink
fix(inbox): don't drop messages when ?c= URL param updates
2026-04-20 20:04:32 +03:00
Arnas Donauskas
6cceb81c46
fix(inbox): don't drop messages when ?c= URL param updates
Reports: clicking a conversation sometimes left the message thread
stuck on "No messages yet" until the user hit browser refresh.

Root cause — a three-way race kicked off by any click on a conversation
in the list:

1. handleSelectConversation runs:
      setActiveConversation(B)
      setMessages([])
      router.replace("/inbox?c=B")

2. The URL change flips `deepLinkConvId` (from useSearchParams).
   handleConversationsLoaded is a useCallback with
   [deepLinkConvId], so its identity changes. That cascades into
   ConversationList, whose fetchConversations useCallback has
   [onConversationsLoaded], whose useEffect has [fetchConversations] —
   so the list refetches conversations purely because the URL changed.

3. When the refetch lands, handleConversationsLoaded runs its deep-link
   auto-select branch. autoSelectedForDeepLinkRef still points at its
   previous value (null, or the previous deep-link), so the guard
   `ref !== deepLinkConvId` passes and the branch re-applies B:
      setActiveConversation(B)     // no-op, same id
      setMessages([])              // BLOWS AWAY the messages
                                   // MessageThread just fetched

4. MessageThread's fetch effect only re-runs when conversationId
   changes. B is still B. It does not refetch. Messages stuck at [].

A hard reload unsticks things because state starts from scratch and
the initial deep-link flow is the only path that runs the auto-select.

Three defense-in-depth fixes:

- handleSelectConversation now writes the clicked id into
  autoSelectedForDeepLinkRef BEFORE the router.replace, so by the
  time any downstream refetch lands, the auto-select branch sees
  ref === deepLinkConvId and skips.

- handleConversationsLoaded early-returns if the deep-linked
  conversation is already the active one. This is redundant with
  the ref update above but protects against any other code path
  we don't foresee triggering a refetch.

- ConversationList's fetch is now driven by a plain useEffect with
  [] deps, using a ref for the latest onConversationsLoaded
  callback. URL changes no longer cause a conversations refetch.
  Matches the same stabilisation pattern used in
  src/hooks/use-auth.tsx and src/components/inbox/message-thread.tsx.

Verified: typecheck passes; lint count unchanged (37 pre-existing
problems, zero added). Couldn't exercise the rendered /inbox from the
preview sandbox — it's auth-gated and no test credentials are
available. Please confirm in the authenticated inbox:
  - Click back and forth between two conversations with different
    message histories; messages should populate every time.
  - Open /inbox?c=<id> directly; thread opens with messages.
  - Switch conversations after a deep-link landing; still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:01:59 +03:00
Arnas Donauskas
460b5bf259
Merge pull request #69 from ArnasDon/security/rate-limit-send-broadcast
security: rate-limit send and broadcast per user
2026-04-20 19:51:01 +03:00
Arnas Donauskas
1f295a831a
security: rate-limit send and broadcast per user
A fork running WaCRM publicly had no server-side guard on the
WhatsApp send endpoints — a compromised session or an aggressive UI
loop could call /api/whatsapp/send or /broadcast as fast as the
network allowed and blow through Meta's per-number quota (or worse,
get the number suspended).

This PR adds a minimal in-memory fixed-window rate limiter keyed by
user id.

Budgets (tuned for a template default — forkers can edit):
  - /api/whatsapp/send      60 per minute per user  (1/s sustained)
  - /api/whatsapp/broadcast  5 per minute per user  (campaign dispatch
                                                    rate, not fan-out)

Behaviour:
  - Check runs right after the Supabase auth check; unauthenticated
    calls 401 as before and never consume a bucket entry.
  - When a user exceeds the budget the route returns 429 with
    Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining,
    X-RateLimit-Reset headers.
  - Send and broadcast buckets are independent; two users are
    independent.

Design trade-off:
  The limiter lives in a process-local Map. On a single Hostinger
  Managed Node.js instance — the template's default deployment —
  that's the whole limit. On a horizontally-scaled deploy (multiple
  regions, serverless fan-out) each instance has its own Map and the
  effective rate is N * limit. Swap src/lib/rate-limit.ts for a
  Redis/Upstash/Durable-Objects backend if you scale past one
  instance; keep the checkRateLimit return shape and the call sites
  don't move.

Memory is self-cleaning: a 1-in-1000 opportunistic sweep drops
expired keys without a background timer, so the Map stays bounded
without running anything outside the request path.

Smoke test (tsx):
  - 7 broadcasts → 5 ok, 2 denied.
  - Different user → independent bucket.
  - Same user, different route → independent bucket.
  - Retry-after = 60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:48:30 +03:00