Skip to content

feat(newsletter): migrate to listmonk#283

Merged
dmnktoe merged 10 commits intomainfrom
cursor/listmonk-newsletter-216b
Apr 23, 2026
Merged

feat(newsletter): migrate to listmonk#283
dmnktoe merged 10 commits intomainfrom
cursor/listmonk-newsletter-216b

Conversation

@dmnktoe
Copy link
Copy Markdown
Owner

@dmnktoe dmnktoe commented Apr 23, 2026

Summary

  • Migrated newsletter subscribe to native listmonk integration; removed legacy confirm/unsubscribe routes.
  • ALTCHA runs fully within Next.js (widget v3 + altcha-lib v2).
  • Fixed double opt-in emails by not explicitly triggering opt-in after create.
  • Hardening: deriveHmacKeySecret import uses altcha-lib/frameworks/shared (not nextjs); listmonk subscribe only treats 409 as silent duplicate; 400 is logged and returns 500; UUID lookup validates canonical UUIDs before building SQL.
  • altcha-widget typings use react/jsx-runtime module augmentation.

Test Plan

  • pnpm test
  • pnpm typecheck
  • pnpm lint

Summary by CodeRabbit

  • New Features

    • Newsletter backend migrated to Listmonk; subscriptions now rely on Listmonk double-opt-in.
  • Refactor

    • ALTCHA verification upgraded to v2/v3 flow with updated token format.
  • Removed

    • Built-in subscription confirmation and unsubscribe pages and email templates removed (flows now handled by Listmonk).
  • Style

    • Adjusted header spacing on small screens.
  • Chores

    • Updated ALTCHA-related package versions.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sentiment Ready Ready Preview, Comment Apr 23, 2026 9:32pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@cursor[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 51 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 51 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 474a1ee2-d865-4ae1-896d-61ec7bc167a9

📥 Commits

Reviewing files that changed from the base of the PR and between 417bc3e and b8d5d85.

📒 Files selected for processing (1)
  • src/components/templates/NewsletterForm.tsx
📝 Walkthrough

Walkthrough

Migrates newsletter backend from Strapi to Listmonk, adds a Listmonk client and env vars, upgrades ALTCHA to v3 with derived HMAC key flow, removes server-side confirmation/unsubscribe pages and email templates, and updates tests and widget typings to match the new flows.

Changes

Cohort / File(s) Summary
Environment & Manifest
\.env.example, package.json, tsconfig.json
Added Listmonk env vars (LISTMONK_BASE_URL, LISTMONK_API_USER, LISTMONK_API_KEY, LISTMONK_LIST_ID); bumped altcha/altcha-lib versions; changed TS module resolution to bundler.
Listmonk Client & Constants
src/lib/listmonk.ts, src/constant/env.ts, src/__tests__/lib/listmonk.test.ts
New Listmonk API client with request helper, ListmonkError, subscriber helpers (create/find/unsubscribe/opt-in); added server-side env exports and tests covering auth, error parsing, and endpoints.
Subscribe Flow & Challenge
src/app/api/newsletter/subscribe/route.ts, src/app/api/newsletter/challenge/route.ts, src/lib/newsletter-schema.ts
Refactored ALTCHA to derive HMAC key secret and use PBKDF2/SHA-256 parameters; subscribe now verifies ALTCHA v3 payload shape and creates subscribers via Listmonk (handles 409 duplicates, maps 400/other errors to 500).
Removed Strapi Endpoints & Email Templates
src/app/api/newsletter/confirm/route.ts, src/app/api/newsletter/unsubscribe/route.ts, src/emails/confirm-subscription.tsx, src/emails/goodbye.tsx
Deleted Strapi-based confirm/unsubscribe endpoints and server-side email templates; confirmation/unsubscribe flows are removed in favor of Listmonk-managed lifecycle.
Pages Removed
src/app/newsletter/success/page.tsx, src/app/newsletter/unsubscribed/page.tsx
Removed success/unsubscribed client pages related to the old confirm/unsubscribe redirects.
Tests Updated/Removed
src/__tests__/app/api/newsletter/subscribe.test.ts, src/__tests__/app/api/newsletter/confirm.test.ts, src/__tests__/app/api/newsletter/unsubscribe.test.ts
Subscribe tests rewired to mock Listmonk client and ALTCHA v2/v3 fixtures; confirm and unsubscribe test suites removed.
ALTCHA Widget & Types
src/components/templates/NewsletterForm.tsx, src/components/helpers/AltchaScript.tsx, src/types/altcha.d.ts
Widget attrs switched from challengeurl to challenge; added JSX intrinsic typings for altcha-widget; adjusted widget CSS typing and imports.
Minor UI Change
src/components/layout/Header.tsx
Reduced header spacing at sm breakpoint (sm:gap-32sm:gap-16).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as Browser/Client
    participant ChallengeAPI as /api/newsletter/challenge
    participant ALTCHA as ALTCHA Service

    User->>Client: Open newsletter form
    Client->>ChallengeAPI: GET /api/newsletter/challenge
    ChallengeAPI->>ALTCHA: deriveKey + deriveHmacKeySecret -> createChallenge (PBKDF2/SHA-256)
    ALTCHA-->>ChallengeAPI: { challenge, expiresAt }
    ChallengeAPI-->>Client: Return challenge JSON
    Client->>Client: Render altcha-widget with challenge
Loading
sequenceDiagram
    participant User
    participant Client as Browser/Client
    participant SubscribeAPI as /api/newsletter/subscribe
    participant ALTCHA as ALTCHA Verification
    participant Listmonk as Listmonk API

    User->>Client: Submit email + ALTCHA payload
    Client->>SubscribeAPI: POST (email, base64 payload)
    SubscribeAPI->>ALTCHA: verifySolution (derived keys)
    alt verification fails
        ALTCHA-->>SubscribeAPI: verified = false
        SubscribeAPI-->>Client: { error: 'Challenge verification failed' }
    else verification succeeds
        ALTCHA-->>SubscribeAPI: verified = true
        SubscribeAPI->>Listmonk: POST /api/subscribers (email, listIds)
        alt success / already exists (200/201/409 handled)
            Listmonk-->>SubscribeAPI: subscriber info / 409
            SubscribeAPI-->>Client: { success: true } (409 -> generic thank-you)
        else validation/error
            Listmonk-->>SubscribeAPI: 400/other error
            SubscribeAPI-->>Client: 500 { error: 'An internal error occurred' }
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through code with a twitchy nose,

Swapped Strapi mail for Listmonk's flows,
Secrets derived and challenges spun,
ALTCHA v3 — the puzzle is done!
Hooray, inboxes await the sun.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'feat(newsletter): migrate to listmonk' accurately describes the primary change: migrating the newsletter system from a custom Strapi flow to listmonk integration.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/listmonk-newsletter-216b

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 and usage tips.

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 90.29126% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.70%. Comparing base (473441d) to head (b8d5d85).

Files with missing lines Patch % Lines
src/app/api/newsletter/challenge/route.ts 0.00% 6 Missing ⚠️
src/app/api/newsletter/subscribe/route.ts 92.30% 2 Missing ⚠️
src/lib/listmonk.ts 96.87% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #283      +/-   ##
==========================================
+ Coverage   63.77%   65.70%   +1.93%     
==========================================
  Files          67       62       -5     
  Lines         784      729      -55     
  Branches      161      153       -8     
==========================================
- Hits          500      479      -21     
+ Misses        284      250      -34     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dmnktoe
Copy link
Copy Markdown
Owner Author

dmnktoe commented Apr 23, 2026

📝 Changed routes:

2 deleted routes:

  • /newsletter/success
  • /newsletter/unsubscribed

Commit b8d5d85 (https://sentiment-4hvhrl3ke-yl33ly.vercel.app).

@dmnktoe dmnktoe marked this pull request as ready for review April 23, 2026 21:16
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
.env.example (1)

18-24: Optional: reorder Listmonk keys to match dotenv-linter's alphabetical convention.

dotenv-linter flags LISTMONK_API_USER and LISTMONK_API_KEY as out of order relative to LISTMONK_BASE_URL. Purely cosmetic but will silence the lint warnings.

💄 Suggested reorder
 # Server-side only — listmonk newsletter integration
-# Base URL of listmonk instance (no trailing slash preferred)
-LISTMONK_BASE_URL=https://newsletter.project-sentiment.org
-# listmonk API user (basic auth username)
-LISTMONK_API_USER=auth
 # listmonk API key (basic auth password)
 LISTMONK_API_KEY=your_listmonk_api_key
+# listmonk API user (basic auth username)
+LISTMONK_API_USER=auth
+# Base URL of listmonk instance (no trailing slash preferred)
+LISTMONK_BASE_URL=https://newsletter.project-sentiment.org
 # list ID to subscribe/unsubscribe against
 LISTMONK_LIST_ID=1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 18 - 24, The dotenv example's keys are out of
alphabetical order per dotenv-linter: reorder the LISTMONK variables so their
environment variable names are alphabetized (e.g., ensure LISTMONK_API_KEY and
LISTMONK_API_USER appear in proper alphabetical order relative to
LISTMONK_BASE_URL and LISTMONK_LIST_ID) so entries LISTMONK_API_KEY,
LISTMONK_API_USER, LISTMONK_BASE_URL, LISTMONK_LIST_ID (or the correct
alphabetical sequence) are placed accordingly to silence the linter.
src/components/templates/NewsletterForm.tsx (2)

82-99: Nit: polling for altcha-widget is fragile on slow networks.

The 100 ms setInterval with a 5 s hard stop silently gives up if the widget's custom element hasn't registered yet (slow network, blocked CDN, AltchaScript's dynamic import('altcha') still pending). The user then sees "Bot protection loading..." forever and cannot submit, with no error surfaced.

Consider listening to customElements.whenDefined('altcha-widget') instead, which resolves deterministically as soon as the element is registered and avoids the arbitrary 5 s cutoff:

♻️ Optional refactor
 useEffect(() => {
-  const checkWidget = setInterval(() => {
-    const widget = document.querySelector('altcha-widget');
-    if (widget) {
-      setWidgetReady(true);
-      clearInterval(checkWidget);
-    }
-  }, 100);
-
-  const timeout = setTimeout(() => {
-    clearInterval(checkWidget);
-  }, 5000);
-
-  return () => {
-    clearInterval(checkWidget);
-    clearTimeout(timeout);
-  };
+  if (typeof window === 'undefined' || !window.customElements) return;
+  let cancelled = false;
+  window.customElements.whenDefined('altcha-widget').then(() => {
+    if (!cancelled && document.querySelector('altcha-widget')) {
+      setWidgetReady(true);
+    }
+  });
+  return () => {
+    cancelled = true;
+  };
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/templates/NewsletterForm.tsx` around lines 82 - 99, Replace
the fragile polling inside the useEffect that queries
document.querySelector('altcha-widget') and the 5s timeout with a deterministic
registration listener using customElements.whenDefined('altcha-widget') to
setWidgetReady(true) as soon as the element is defined; also add a fallback
error path (e.g., set an error state or surface a log) if whenDefined rejects or
a configurable timeout elapses, and ensure you cancel any pending promise
resolution on unmount (use an abort flag or AbortController) so the whenDefined
handler and any timeout cleanup do not call setWidgetReady after the component
is unmounted.

5-6: Duplicate type-only import.

Lines 5 and 6 are identical (import type {} from 'altcha/types/react';). One copy is enough to pull in the module-augmentation types — remove the duplicate. The same unused/empty import also appears in src/components/helpers/AltchaScript.tsx, so a single import here is all you need.

♻️ Suggested fix
 import type { WidgetAttributes } from 'altcha/types';
 import type {} from 'altcha/types/react';
-import type {} from 'altcha/types/react';
 import { useTheme } from 'next-themes';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/templates/NewsletterForm.tsx` around lines 5 - 6, Remove the
duplicate type-only import line "import type {} from 'altcha/types/react';" from
NewsletterForm.tsx so only one such import remains in the file; do the same in
AltchaScript.tsx if you want to avoid redundant empty imports across the
codebase, but at minimum delete the repeated import in NewsletterForm.tsx.
src/lib/listmonk.ts (2)

67-76: Minor: spreading init.headers won't preserve Headers/[k,v][] forms.

Object spread only copies own enumerable keys, so if a future caller passes headers: new Headers({...}) or a [string, string][], those entries will be silently dropped and the request will only carry the defaults set here. Today all call sites use plain objects so this is latent, but normalizing via new Headers(init.headers) (or an explicit HeadersInit narrow) avoids the footgun.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/listmonk.ts` around lines 67 - 76, The fetch call builds headers by
object-spreading init.headers which drops non-plain forms (Headers instance or
[k,v][]); change the headers creation in the fetch invocation to normalize via
new Headers(init.headers) and then set/append the defaults (Accept,
Authorization from getAuthHeader(), and Content-Type when init.body exists) on
that Headers instance so Headers forms are preserved while keeping
controller.signal and the rest of init intact.

62-92: Nit: timeout surfaces as AbortError, not ListmonkError.

On timeout, the AbortController causes fetch to reject with a DOMException/AbortError before the !res.ok branch runs, so callers can't uniformly rely on err instanceof ListmonkError (e.g., the subscribe route's duplicate-handling branch won't fire and the timeout will fall through to the generic 500). Wrapping abort signals into ListmonkError with a dedicated status (e.g., 0 or 504) would give callers one error shape to reason about.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/listmonk.ts` around lines 62 - 92, The fetch can reject with an
AbortError on timeout so callers don't get a ListmonkError; update the
implementation around the fetch call (the AbortController/timeoutMs logic and
try/finally surrounding fetch) to catch exceptions from fetch, detect aborts
(the AbortError/DOMException coming from controller.abort()) and rethrow a
ListmonkError instead (use a dedicated status like 0 or 504 and include any
parsed body or contextual message), while still preserving other errors and
clearing timeoutId in finally; reference AbortController, timeoutMs,
controller.abort, the fetch call, ListmonkError and parseJsonBestEffort to
locate the change.
src/app/api/newsletter/subscribe/route.ts (1)

67-86: Use altcha-lib's exported types instead of as never casts for better type safety.

The as never cast works because never is assignable to every type, but it defeats type-checking. altcha-lib v2 exports Challenge and Solution types—import these from the library and use them to narrow the unknown fields instead of bypassing validation. This surfaces structural issues early and catches future breaking changes in the library.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/newsletter/subscribe/route.ts` around lines 67 - 86, Replace the
unsafe `as never` casts by importing and using altcha-lib v2's exported types
(Challenge and Solution): update the parsed variable's type to { challenge?:
Challenge; solution?: Solution } (or cast parsed.challenge as Challenge and
parsed.solution as Solution) and pass those typed values into verifySolution
(the call in this block that uses verifySolution, deriveKey,
hmacSignatureSecret, and hmacKeySignatureSecret). Ensure you add the import for
Challenge and Solution from altcha-lib v2 and remove the `as never` casts so
TypeScript enforces the correct structure before calling verifySolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/newsletter/challenge/route.ts`:
- Around line 1-3: The import for deriveHmacKeySecret is coming from the wrong
subpath; update the import statements in both route files so deriveHmacKeySecret
is imported from the package root 'altcha-lib' (not
'altcha-lib/frameworks/nextjs'), e.g. replace any occurrence importing
deriveHmacKeySecret with an import from 'altcha-lib' alongside
createChallenge/randomInt/deriveKey so the symbol deriveHmacKeySecret resolves
at runtime.

In `@src/app/api/newsletter/subscribe/route.ts`:
- Around line 160-182: The current catch block in route.ts silences both
ListmonkError 400 and 409; change it so only 409 (duplicate) is treated as a
silent-success for user-enumeration protection: update the conditional that
checks err instanceof ListmonkError to test only err.status === 409 and return
the friendly NextResponse.json message for that case, while allowing 400 errors
to fall through to the generic error handling path (log the error via
console.error and return a 500 or propagate the 400 as appropriate) so
validation failures from createSubscriber are surfaced and logged.

In `@src/lib/listmonk.ts`:
- Around line 127-138: findSubscriberByUuid currently embeds the raw uuid into
the Listmonk query DSL after only URL-encoding, which allows
malformed/quote-containing input to break the query or enable injection; add a
strict UUID format check at the start of findSubscriberByUuid (e.g. validate
against a canonical UUID regex for v1/v4) and fail fast (return null or throw a
clear error) for invalid values, only building the query and calling
listmonkRequest when the uuid passes validation; keep encodeURIComponent on the
validated value but do not attempt to embed unvalidated input into the
`subscribers.uuid = '...'` expression.

In `@src/types/altcha.d.ts`:
- Around line 3-25: Replace the global JSX namespace augmentation with module
augmentation for 'react/jsx-runtime' so the custom element <altcha-widget> and
its props (challenge, challengeUrl/challengeurl, hideLogo/hidelogo,
hideFooter/hidefooter, name, strings, style) are recognized under React 19's new
JSX transform; move the IntrinsicElements interface into a "declare module
'react/jsx-runtime' { namespace JSX { interface IntrinsicElements {
'altcha-widget': ... } } }" form, preserving the same prop names and types used
in the existing declaration to ensure type checking for altcha-widget usages.

---

Nitpick comments:
In @.env.example:
- Around line 18-24: The dotenv example's keys are out of alphabetical order per
dotenv-linter: reorder the LISTMONK variables so their environment variable
names are alphabetized (e.g., ensure LISTMONK_API_KEY and LISTMONK_API_USER
appear in proper alphabetical order relative to LISTMONK_BASE_URL and
LISTMONK_LIST_ID) so entries LISTMONK_API_KEY, LISTMONK_API_USER,
LISTMONK_BASE_URL, LISTMONK_LIST_ID (or the correct alphabetical sequence) are
placed accordingly to silence the linter.

In `@src/app/api/newsletter/subscribe/route.ts`:
- Around line 67-86: Replace the unsafe `as never` casts by importing and using
altcha-lib v2's exported types (Challenge and Solution): update the parsed
variable's type to { challenge?: Challenge; solution?: Solution } (or cast
parsed.challenge as Challenge and parsed.solution as Solution) and pass those
typed values into verifySolution (the call in this block that uses
verifySolution, deriveKey, hmacSignatureSecret, and hmacKeySignatureSecret).
Ensure you add the import for Challenge and Solution from altcha-lib v2 and
remove the `as never` casts so TypeScript enforces the correct structure before
calling verifySolution.

In `@src/components/templates/NewsletterForm.tsx`:
- Around line 82-99: Replace the fragile polling inside the useEffect that
queries document.querySelector('altcha-widget') and the 5s timeout with a
deterministic registration listener using
customElements.whenDefined('altcha-widget') to setWidgetReady(true) as soon as
the element is defined; also add a fallback error path (e.g., set an error state
or surface a log) if whenDefined rejects or a configurable timeout elapses, and
ensure you cancel any pending promise resolution on unmount (use an abort flag
or AbortController) so the whenDefined handler and any timeout cleanup do not
call setWidgetReady after the component is unmounted.
- Around line 5-6: Remove the duplicate type-only import line "import type {}
from 'altcha/types/react';" from NewsletterForm.tsx so only one such import
remains in the file; do the same in AltchaScript.tsx if you want to avoid
redundant empty imports across the codebase, but at minimum delete the repeated
import in NewsletterForm.tsx.

In `@src/lib/listmonk.ts`:
- Around line 67-76: The fetch call builds headers by object-spreading
init.headers which drops non-plain forms (Headers instance or [k,v][]); change
the headers creation in the fetch invocation to normalize via new
Headers(init.headers) and then set/append the defaults (Accept, Authorization
from getAuthHeader(), and Content-Type when init.body exists) on that Headers
instance so Headers forms are preserved while keeping controller.signal and the
rest of init intact.
- Around line 62-92: The fetch can reject with an AbortError on timeout so
callers don't get a ListmonkError; update the implementation around the fetch
call (the AbortController/timeoutMs logic and try/finally surrounding fetch) to
catch exceptions from fetch, detect aborts (the AbortError/DOMException coming
from controller.abort()) and rethrow a ListmonkError instead (use a dedicated
status like 0 or 504 and include any parsed body or contextual message), while
still preserving other errors and clearing timeoutId in finally; reference
AbortController, timeoutMs, controller.abort, the fetch call, ListmonkError and
parseJsonBestEffort to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: caf40077-f9c3-4c76-8821-094490552bee

📥 Commits

Reviewing files that changed from the base of the PR and between 473441d and 257f6db.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/images/services/listmonk/sentiment_listmonk-logo.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • .env.example
  • package.json
  • src/__tests__/app/api/newsletter/confirm.test.ts
  • src/__tests__/app/api/newsletter/subscribe.test.ts
  • src/__tests__/app/api/newsletter/unsubscribe.test.ts
  • src/__tests__/lib/listmonk.test.ts
  • src/app/api/newsletter/challenge/route.ts
  • src/app/api/newsletter/confirm/route.ts
  • src/app/api/newsletter/subscribe/route.ts
  • src/app/api/newsletter/unsubscribe/route.ts
  • src/app/newsletter/success/page.tsx
  • src/app/newsletter/unsubscribed/page.tsx
  • src/components/helpers/AltchaScript.tsx
  • src/components/layout/Header.tsx
  • src/components/templates/NewsletterForm.tsx
  • src/constant/env.ts
  • src/emails/confirm-subscription.tsx
  • src/emails/goodbye.tsx
  • src/lib/listmonk.ts
  • src/lib/newsletter-schema.ts
  • src/types/altcha.d.ts
  • tsconfig.json
💤 Files with no reviewable changes (8)
  • src/app/newsletter/success/page.tsx
  • src/emails/goodbye.tsx
  • src/app/newsletter/unsubscribed/page.tsx
  • src/app/api/newsletter/unsubscribe/route.ts
  • src/tests/app/api/newsletter/confirm.test.ts
  • src/tests/app/api/newsletter/unsubscribe.test.ts
  • src/app/api/newsletter/confirm/route.ts
  • src/emails/confirm-subscription.tsx

Comment on lines +1 to +3
import { createChallenge, randomInt } from 'altcha-lib';
import { deriveKey } from 'altcha-lib/algorithms/pbkdf2';
import { deriveHmacKeySecret } from 'altcha-lib/frameworks/nextjs';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the subpath is exposed by the installed altcha-lib version.
fd -t f '^package\.json$' --max-depth 3 --exec sh -c '
  echo "== {} ==";
  jq "{altcha_lib: .dependencies[\"altcha-lib\"], altcha_widget: .dependencies.altcha}" {}
' \;
# Look up the installed package's exports map if node_modules is present
fd -t d '^altcha-lib$' -p 'node_modules/altcha-lib$' --max-depth 4 --exec sh -c '
  echo "== {} ==";
  jq ".exports // .main" {}/package.json;
  ls {}/frameworks 2>/dev/null || echo "(no frameworks dir)"
' \;

Repository: dmnktoe/sentiment

Length of output: 266


🌐 Web query:

altcha-lib npm frameworks/nextjs deriveHmacKeySecret export

💡 Result:

altcha-lib is an npm package (https://www.npmjs.com/package/altcha-lib) for creating and verifying ALTCHA proof-of-work challenges on the server side. It supports Node.js, Bun, Deno, and browsers via Web Crypto. Key exports include: - createChallenge(options): Creates a challenge with HMAC signing using hmacKey (v1) or hmacSignatureSecret (v2). - verifySolution(payload, hmacKey) or verifySolution(options): Verifies client-submitted solutions. - deriveHmacKeySecret: Utility function imported alongside others like createChallenge, randomInt, verifySolution. Used in v2 PoW mode for deriving HMAC keys/secrets, e.g., from a base secret for hmacKeySignatureSecret in deterministic challenges. For Next.js (frameworks/nextjs): Install via npm i altcha-lib. Use in API routes or server actions: ts // app/api/challenge/route.ts import { createChallenge } from 'altcha-lib'; export async function GET { const challenge = await createChallenge({ hmacKey: process.env.ALTCHA_HMAC_KEY! }); return Response.json(challenge); } // app/api/verify/route.ts import { verifySolution } from 'altcha-lib'; export async function POST(request: Request) { const formData = await request.formData; const payload = formData.get('altcha') as string; const valid = await verifySolution(payload, process.env.ALTCHA_HMAC_KEY!); return Response.json({ valid }); } For v2 (beta channel: npm i altcha-lib@beta), import deriveHmacKeySecret and use with deriveKey from 'altcha-lib/algorithms/pbkdf2': ts import { createChallenge, deriveHmacKeySecret, randomInt, verifySolution } from 'altcha-lib'; import { deriveKey } from 'altcha-lib/algorithms/pbkdf2'; const HMAC_SECRET = process.env.ALTCHA_HMAC_SECRET!; const HMAC_KEY_SECRET = deriveHmacKeySecret(HMAC_SECRET); // Derives key signature secret const challenge = await createChallenge({ algorithm: 'PBKDF2/SHA-256', cost: 5000, counter: randomInt(5000, 10000), deriveKey, hmacSignatureSecret: HMAC_SECRET, hmacKeySignatureSecret: HMAC_KEY_SECRET, }); Client widget: Use after importing 'altcha'.

Citations:


🏁 Script executed:

# Search for all uses of deriveHmacKeySecret in the codebase
rg "deriveHmacKeySecret" -A 2 -B 2

# Search for all imports from altcha-lib to see the current pattern
rg "from\s+['\"]altcha-lib" -A 1 -B 1

# Check if frameworks/nextjs path exists in node_modules structure
fd "frameworks" -t d --max-depth 5 2>/dev/null | head -20

Repository: dmnktoe/sentiment

Length of output: 2966


Fix incorrect import path for deriveHmacKeySecret in both challenge and subscribe routes.

The code imports deriveHmacKeySecret from 'altcha-lib/frameworks/nextjs', but according to the official altcha-lib v2 documentation, this function is exported from the package root ('altcha-lib'). The frameworks/nextjs subpath is not documented as an export source. Update the import statement in src/app/api/newsletter/challenge/route.ts and src/app/api/newsletter/subscribe/route.ts:

- import { deriveHmacKeySecret } from 'altcha-lib/frameworks/nextjs';
+ import { deriveHmacKeySecret } from 'altcha-lib';

This will resolve the module import error at runtime.

📝 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
import { createChallenge, randomInt } from 'altcha-lib';
import { deriveKey } from 'altcha-lib/algorithms/pbkdf2';
import { deriveHmacKeySecret } from 'altcha-lib/frameworks/nextjs';
import { createChallenge, randomInt } from 'altcha-lib';
import { deriveKey } from 'altcha-lib/algorithms/pbkdf2';
import { deriveHmacKeySecret } from 'altcha-lib';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/newsletter/challenge/route.ts` around lines 1 - 3, The import for
deriveHmacKeySecret is coming from the wrong subpath; update the import
statements in both route files so deriveHmacKeySecret is imported from the
package root 'altcha-lib' (not 'altcha-lib/frameworks/nextjs'), e.g. replace any
occurrence importing deriveHmacKeySecret with an import from 'altcha-lib'
alongside createChallenge/randomInt/deriveKey so the symbol deriveHmacKeySecret
resolves at runtime.

Comment thread src/app/api/newsletter/subscribe/route.ts
Comment thread src/lib/listmonk.ts
Comment thread src/types/altcha.d.ts Outdated
@dmnktoe dmnktoe merged commit f05ca54 into main Apr 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants