⚙️

Tech Stack

Frontend
🌐
Vanilla HTML / CSS / JS
No frontend framework — pure browser APIs
  • Static HTML 23 server-rendered pages
  • CSS admin.css · dashboard.css · designer.css · main.css · storefront.css
  • JS dashboard.js · product.js · storefront.js
  • Stripe.js Loaded from js.stripe.com for checkout
  • MooChedda iframe wallet at v1.moochedda.com:3002
Backend
🟢
Node.js + Express 4
Single-process, port 8800
  • Entry src/server.js
  • Routes 21 route modules in src/routes/
  • Middleware auth · paywall · upload
  • Operations public/SystemCheck.html + /api/system/check
  • Watch mode node --watch for dev
  • Stripe Platform billing + webhooks
Database
🗄️
MariaDB / MySQL
localhost:3306, pool size 10
  • Driver mysql2/promise (parameterized queries)
  • DB name launchcart
  • User shopify
  • Schema Auto-initialised on startup via initSchema()
  • Migrations Inline ALTER TABLE IF NOT EXISTS pattern
Infrastructure
🖧
Single VPS · Apache
67.212.134.72
  • Reverse proxy Apache 2 + mod_proxy
  • TLS Certbot (Let's Encrypt), auto-renewed
  • Web root /var/www/html/torvali/websites
  • Apache sites /etc/apache2/sites-available
  • Custom domains Provisioned via execFile() + Apache vhosts
🗺️

System Architecture

CLIENTS EDGE / PROXY APPLICATION PERSISTENCE EXTERNAL APIs Merchant Browser Dashboard / Editor Customer Browser Storefront / Checkout Designer Browser Marketplace Portal Admin Browser Platform Dashboard Employee Browser Merchant Sub-account Apache 2 Reverse Proxy TLS Termination (Certbot / Let's Encrypt) · mod_proxy → localhost:8800 X-Store-Name header injection for custom domain routing · Static file serving Node.js / Express 4 — src/server.js (port 8800) CSP Headers auth.js JWT paywall.js upload.js Domain Router 20 Route Modules Static Files MariaDB localhost:3306 / launchcart 24 tables · pool: 10 File System uploads/ (logos · banners · themes products · layouts · custom-code) Apache VHosts /etc/apache2/sites-available Custom domain provisioning Stripe Billing · Webhooks MooChedda v1.moochedda.com:3002 Nodemailer / SMTP Per-merchant config Certbot / Let's Encrypt SSL provisioning
🗃️

Database Schema Overview

24
Tables
18
FK Relationships
6
ENUM Columns
5
Roles / User Types
1
JSON Column
merchants
Root tenant entity. ~50 columns. References nothing. Referenced by most tables.
categories
→ merchants, → self (parent_id, tree). Cascade delete.
products
→ merchants, → categories. Rich 40-col product record.
product_images
→ products. Primary flag + position ordering.
product_specifications
→ products. K/V spec pairs for PDPs.
product_variants
→ products. Up to 3 option axes per variant.
orders
→ merchants, → customers. 5-state order lifecycle.
order_items
→ orders, → products (SET NULL), → product_variants.
shipping_zones
→ merchants. Legacy flat-rate table.
shipping_rates
→ merchants. Region-based rates (US state / INT). Unique: merchant+region.
customers
→ merchants. Scoped per store. TOTP columns.
product_reviews
→ products, → customers (NULL). Unique: customer+product.
pages
→ merchants. CMS pages. Nav/footer visibility flags.
storefront_sections
→ merchants. Drag-and-drop section JSON blobs.
layouts
→ designers (nullable). CSS layout packages. Approval workflow.
marketplace_themes
→ designers. Full skin CSS. Approval workflow.
admins
Platform super-admins. TOTP. Last-login tracking.
designers
Theme / layout creators. TOTP. Bio + avatar.
merchant_employees
→ merchants. JSON permissions. Invite-token flow. TOTP.
merchant_email_config
→ merchants (1:1). SMTP credentials per store.
merchant_email_templates
→ merchants. 6 trigger types. Unique: merchant+type.
merchant_custom_code
→ merchants. CSS/JS file URLs, sort_order.
support_tickets
No FK. Open / in_progress / closed lifecycle.
redemption_codes
→ merchants (used_by, NULL). 6-char promo codes.
Relationship Summary

merchants is the central root entity — all merchant-owned tables carry a merchant_id FK with ON DELETE CASCADE to ensure clean multi-tenant data isolation. customers are scoped per merchant with a composite unique key (merchant_id, email). orders retain items via ON DELETE CASCADE but preserve product references on deletion with ON DELETE SET NULL. The designers table is a peer entity to merchants — designers upload assets but do not own storefronts. admins is entirely independent with no FK dependencies.

🔌

API Surface Area

32
Route Namespaces
14
Paywalled Namespaces
8
Public Namespaces
9
JWT-Protected
Namespace Module Access Role(s) Key Endpoints
/api/authroutes/auth.jsPublicPOST /signup · /login · /logout · /totp/setup · /forgot-password · /reset-password
/api/billingroutes/billing.jsProtectedMerchantGET /status · POST /checkout · /portal · /redeem · /webhook (raw, Stripe sig)
/api/meroutes/me.jsProtectedMerchantGET / · PATCH / · PATCH /password · PATCH /stripe-keys · PATCH /moochedda
/api/me/emailroutes/email.jsProtectedMerchantGET,PATCH /config · GET,PUT /templates/:type · POST /test
/api/productsroutes/products.jsPaywallMerchantGET,POST / · GET,PATCH,DELETE /:id · POST /:id/images · PATCH /:id/images/:imgId
/api/ordersroutes/orders.jsPaywallMerchantGET,POST / · GET,PATCH /:id · PATCH /:id/status · POST /:id/invoice
/api/categoriesroutes/categories.jsPaywallMerchantGET,POST / · PATCH,DELETE /:id
/api/shippingroutes/shipping.jsPaywallMerchantGET,PUT /zones · GET,PUT /rates · GET /config
/api/pagesroutes/pages.jsPaywallMerchantGET,POST / · GET,PATCH,DELETE /:id
/api/sectionsroutes/sections.jsPaywallMerchantGET,POST / · PATCH /:id · DELETE /:id · POST /reorder
/api/domainroutes/domain.jsPaywallMerchantGET,POST,DELETE /
/api/customersroutes/customers.jsPaywallMerchantGET / · GET /:id · DELETE /:id
/api/employeesroutes/employees.jsPaywall + OwnerMerchant OwnerGET,POST / · PATCH,DELETE /:id · PATCH /:id/status · POST /:id/reinvite
/api/employeeroutes/employees.jsProtectedEmployeeGET /me · PATCH /me/password · GET,POST /me/totp
/api/employee/inviteroutes/employees.jsPublicGET /:token · POST /:token/activate
/api/themesroutes/themes.jsMixedMerchant (write)GET / · POST /apply · GET /marketplace · GET /preview/:slug
/api/layoutsroutes/layouts.jsMixedMerchant (apply)GET / · POST /apply · GET /preview/:slug
/api/designer-authroutes/designer-auth.jsPublicPOST /signup · /login · GET /me · PATCH /me/password · GET,POST /me/totp
/api/designerroutes/designer.jsProtectedDesignerGET,PATCH /profile · GET,POST /themes · PATCH,DELETE /themes/:id · GET,POST /layouts
/api/admin-authroutes/admin-auth.jsMixedGET /check · POST /bootstrap · POST /login · GET /me · PATCH /me/password · GET,POST /me/totp
/api/adminroutes/admin.jsProtectedAdminGET /merchants · PATCH /merchants/:id/status · /custom-js · GET /support · GET /layouts · PATCH /layouts/:id/status · GET /themes · PATCH /themes/:id/status · GET /codes · POST /codes
/api/public/:storeroutes/public.jsPublicGET /info · /products · /products/:id · /categories · /featured · /search · POST /orders · /cart/shipping · /reviews
/api/public/:store/customerroutes/customer-auth.jsPublicPOST /signup · /login · GET /me · PATCH /me · GET /orders · /totp: setup, verify, disable
/api/moocheddaroutes/moochedda.jsProtectedMerchantPOST /wallet/create · GET /wallet · GET /transactions · GET /token-price · POST /checkout
/api/supportroutes/support.jsPublicPOST / (submit ticket)
/api/systemroutes/system.jsPublicGET /check (API, DB, FS, Stripe config, MooChedda TLS)
🔐

Authentication & Authorization

JWT Implementation
Libraryjsonwebtoken ^9.0.2
AlgorithmHS256 (default)
Secret sourceJWT_SECRET env var
Merchant expiry7 days (JWT_EXPIRES_IN)
Admin expiry12 hours
Designer expiry30 days
Customer expiry30 days
TransportAuthorization: Bearer header
Token type claimrole (admin/designer) or type (employee/customer)
Password Hashing
Librarybcryptjs ^2.4.3
Cost factor12 (all user types)
Minimum length8 characters (validated server-side)
Timing attacksbcrypt.compare (constant-time)
Password resetcrypto.randomBytes(32) token, stored hashed (customers)
Rate limiting✓ 10 req / 15 min on auth endpoints
2FA / TOTP Status
Merchants
✓ TOTP Available
speakeasy + QRCode. Optional per-merchant.
Admins
✓ TOTP Available
Same speakeasy flow. Strongly recommended.
Designers
✓ TOTP Available
Column + endpoints implemented.
Employees
✓ TOTP Available
Enforced at login if enabled.
Customers
✓ TOTP Available
Per-store customer accounts.
ROLE-BASED ACCESS CONTROL
Admin
Platform superuser. role:'admin' claim. 12h token. Admin-only routes: merchant management, theme approval, support tickets, redemption codes.
Full platform access
Merchant
Store owner. 7d token. Access to own store data only via merchant_id scoping. Subscription / trial gated by paywall middleware.
Store-scoped CRUD
Employee
type:'employee' token. Carries merchant_id + JSON permissions array. requireMerchantOnly blocks employees from owner-only actions.
Scoped permissions
Designer
role:'designer' claim. Access only to own themes/layouts. Cannot access merchant or admin endpoints. Blocked by requireAuth role check.
Designer portal only
Customer
type:'customer' + store claim. Isolated to specific merchant store. Used for order history, review authorship, and account management.
Per-store account
📊

Security Scorecard

Input Validation & Injection Prevention
B
mysql2 parameterized queries prevent SQL injection. Basic field validation on inputs. No schema-level request validation library (Zod/Joi).
Authentication Strength
A-
bcrypt-12, TOTP across all roles, secure token/expiry differentiation by role. Startup guard in server.js refuses to start if JWT_SECRET is missing or shorter than 32 characters — hardcoded fallback secrets removed.
Authorization & Access Control
A
Strong role segregation at middleware level. Merchant-id scoping on all DB queries. Employee permission system. Paywall gatekeeping.
Transport & Header Security
B-
CSP present on storefront with per-request nonce — 'unsafe-inline' removed from script-src. TLS via Apache. Auth endpoints have strict rate limiting (10/15m). Remaining gaps: style-src retains 'unsafe-inline' for inline style attributes; no HSTS/CORS middleware in Express layer.
Data Protection at Rest
B-
Passwords hashed (bcrypt-12). SMTP passwords and per-merchant Stripe secret keys now AES-256-GCM encrypted via src/crypto.js with key stored outside the DB. Remaining gaps: TOTP secrets stored as plaintext base32 strings; no full DB-level encryption.
External Service Security
C
MooChedda client now enforces certificate validation (system CA or pinned CA). Residual gaps: no SSRF guardrails in domain provisioning. Stripe webhook signature verification remains correct.
Error Handling & Information Exposure
B-
A res.json interceptor in server.js strips internal error details from all 5xx responses platform-wide, replacing them with a generic message and a short correlation ID logged server-side. 4xx validation errors pass through unchanged. The existing global next(err) handler was also updated to not leak err.message.
Shell Command Safety
A-
All child_process.exec() calls replaced with execFile(). Commands (a2ensite, a2dissite, systemctl, certbot) now receive arguments as discrete arrays — no shell interpolation possible even if domain regex were bypassed.
Dependency Currency
B
10 dependencies, generally recent versions. speakeasy is unmaintained (last release 2018). No automated dependency audit in CI pipeline.
🔍

Security Findings

✅ Remediated (Closed)

CLOSED MooChedda TLS Certificate Validation Enforced src/routes/moochedda.js · src/routes/system.js · .env
The previous rejectUnauthorized: false setting was removed. Outbound MooChedda calls now validate certificates using the system CA bundle by default, with optional CA pinning via MOOCHEDDA_CA_CERT for private/self-signed PKI environments.
Validation: /api/system/check and /SystemCheck.html include dedicated MooChedda TLS mode and reachability checks.
CLOSED Auth Rate Limiting Applied Across Login/Signup/Reset Endpoints src/server.js · src/routes/system.js · package.json
A strict express-rate-limit policy is now enforced for authentication-sensitive routes across merchant, admin, designer, and customer flows. The configured threshold is 10 requests per 15 minutes per IP with standard rate-limit headers enabled.
Validation: /api/system/check now includes auth_rate_limit to verify limiter configuration coverage.
CLOSED Hardcoded JWT Secret Fallbacks Removed — Startup Guard Added src/server.js · src/middleware/auth.js · src/routes/public.js · src/routes/designer-auth.js
All || 'changeme' and || 'dev_secret_change_me' fallback JWT secrets have been removed from source code. src/server.js now includes a startup guard that calls process.exit(1) if JWT_SECRET is missing or shorter than 32 characters, preventing any misconfigured deployment from starting.
Validation: Server will refuse to start and log FATAL: JWT_SECRET env var is missing or too short. Refusing to start. if the env var is absent or weak.
CLOSED SMTP Passwords and Stripe Secret Keys Encrypted at Rest (AES-256-GCM) src/crypto.js · src/routes/email.js · src/routes/me.js · src/mailer.js · src/routes/public.js · .env
Per-merchant SMTP passwords (merchant_email_config.smtp_pass) and Stripe secret keys (merchants.stripe_secret_key) are now encrypted with AES-256-GCM before every INSERT/UPDATE and decrypted transparently after SELECT. The ENCRYPTION_KEY is stored in the environment outside the database. A startup guard in server.js refuses to start if the key is missing or malformed. Both DB columns were widened from VARCHAR(255) to VARCHAR(512) to accommodate the encrypted format (iv:authTag:ciphertext hex). A one-time migration script (scripts/encrypt-existing-secrets.js) handles existing plaintext rows — it widens the columns, skips already-encrypted values, and re-encrypts plaintext ones idempotently.
Validation: src/crypto.js enforces a 32-byte key; auth tag verification catches key mismatch or data corruption at read time. Column-widening migrations are also included in src/database.js so they apply automatically on server start.
CLOSED Internal Error Details No Longer Exposed to API Clients src/server.js (res.json interceptor + global error handler)
A res.json response interceptor middleware was added to src/server.js that fires before every route handler. For any response with an HTTP 5xx status, it replaces the JSON body with { error: 'Internal server error', id: 'ERR-XXXXXXXX' } and logs the original error and detail fields server-side with the correlation ID. All 100+ existing res.status(500).json({ error: err.message }) catch blocks across 18+ route files are protected without any per-file changes. 4xx validation error messages are intentionally passed through unchanged.
Validation: Any 500 response now returns { "error": "Internal server error", "id": "ERR-XXXXXXXX" }. The real detail appears in server logs as [ERR-XXXXXXXX] METHOD /path originalError.

🟡 Medium Severity

CLOSED Shell Command Injection Risk Eliminated — exec() Replaced with execFile() src/routes/domain.js
All child_process.exec() calls in domain.js were replaced with execFile(). The run(cmd, args) helper now accepts a command string and a discrete argument array — no string is ever passed through a shell. The five affected invocations (a2ensite, a2dissite ×2, systemctl reload apache2 ×2, certbot certonly, certbot delete) all pass domain values as array elements, not interpolated strings.
Validation: require('child_process').execFile is used exclusively; exec is no longer imported in domain.js.
CLOSED CSP script-src 'unsafe-inline' Removed — Per-Request Nonce Policy src/server.js (_htmlCache, _injectNonce, sendPage, sendHTML, CSP middleware)
A per-request nonce is now generated via crypto.randomBytes(16).toString('base64'), attached to req.cspNonce, and injected into all 23 inline <script> blocks across 14 HTML files via a single regex — without modifying any HTML file. The CSP script-src directive was changed from 'unsafe-inline' to 'nonce-${nonce}'. sendPage() and sendHTML() were rewritten to read from an in-memory HTML cache (_htmlCache) and send via res.type('html').send(). The style-src directive intentionally retains 'unsafe-inline' because inline style= attributes are used throughout the HTML — nonces apply only to <script> blocks.
Validation: Every storefront page response includes a unique nonce in the CSP header and in all inline <script> tags. External scripts (with src=) are unaffected.

🔵 Low Severity

LOW No CORS Policy Defined src/server.js
No CORS middleware is configured. Express defaults to allowing all origins for API responses. Malicious third-party sites could make credentialed cross-origin API requests on behalf of authenticated merchants.
Fix: Install cors package and restrict allowed origins to the platform domain.
LOW Missing Security Headers (HSTS, X-Frame-Options, etc.) src/server.js
No Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, or Referrer-Policy headers are set. These protect against downgrade attacks, MIME sniffing, clickjacking, and information leakage.
Fix: Use helmet package for comprehensive security header coverage with a single app.use(helmet()).
LOW Unmaintained Dependency: speakeasy package.json — speakeasy ^2.0.0
speakeasy has had no releases since 2018. While the TOTP algorithm is stable, the library receives no security patches. Future vulnerabilities in its dependencies would go unaddressed.
Fix: Replace with @otplib/preset-default (actively maintained) or otpauth.
LOW Merchant Custom JS Allowed by Default DB: merchants.custom_js_enabled · admin route PATCH /custom-js
Merchants can upload custom JavaScript files that are injected into their storefront. New merchants have custom_js_enabled = 1 by default. While admins can disable this per-merchant, customers of all merchants are exposed to merchant-controlled JS execution without explicit opt-in review.
Fix: Default custom_js_enabled to 0 (disabled) and require admin approval before enabling custom JS per merchant.
LOW Invite Token Not Hashed in Database DB: merchant_employees.invite_token
Employee invite tokens (64-hex, generated via crypto.randomBytes(32)) are stored as plaintext in the database. If the database is breached, all pending invite tokens are exposed and can be used to activate employee accounts.
Fix: Store a bcrypt or SHA-256 hash of the token; compare at activation time.
🛡️

Hardening Recommendations

1
Monitor and Alert on Auth Rate-Limit Exhaustion P2 High
Rate limiting is now implemented. Next step is to make abuse visible by tracking 429 spikes, endpoint concentration, and source IP patterns from reverse proxy logs and app metrics.
# Operational check (already implemented) GET /api/system/check # Inspect: checks[].key == "auth_rate_limit" # Suggested ops metric # Count HTTP 429 responses by route and source IP over 5m windows.
2
Audit JWT_SECRET Value Across All Deployment Environments P2 High
JWT startup guard is now implemented. Next step is operational: ensure every environment (staging, production, CI) has a unique, cryptographically random JWT_SECRET ≥ 64 characters stored as a secret — not a shared or guessable string.
# Generate a strong JWT_SECRET openssl rand -base64 64 # Startup guard (already implemented in src/server.js) if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { process.exit(1); // FATAL — server refuses to start }
3
Operationalize MooChedda TLS Monitoring P2 High
TLS hardening is complete. Next step is operational assurance: alert when MooChedda TLS check fails, and pin CA via MOOCHEDDA_CA_CERT where required by environment policy.
# Optional (private/self-signed CA only) MOOCHEDDA_CA_CERT=/absolute/path/to/moochedda-ca.pem # Health probe (already implemented) GET /api/system/check # Inspect checks[] for: # - moochedda_tls_mode # - moochedda_reachability
4
Add Helmet for Security Headers P2 High
Use the helmet package to automatically add HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and a tightened CSP — replacing the manual header code in server.js.
npm install helmet const helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'nonce-...'", "https://js.stripe.com"], // script-src 'unsafe-inline' already removed — per-request nonce in place // style-src still uses 'unsafe-inline' — audit inline style= attrs to remove } }, hsts: { maxAge: 63072000, includeSubDomains: true, preload: true } }));
5
Audit Future Shell Integrations for exec() Use P3 Medium
exec() has been removed from domain provisioning. As a preventive measure, add a lint rule or pre-commit hook to flag any future introduction of child_process.exec() in the codebase.
# Example: grep in CI to catch regressions grep -r "require.*child_process" src/ | grep -v execFile | grep -v spawn # Or add an ESLint rule: # "no-restricted-modules": ["error", { "name": "child_process", ... }] # (allow execFile/spawn only)
6
Rotate ENCRYPTION_KEY and Back Up Encrypted Records P3 Medium
AES-256-GCM encryption is implemented. Next operational step: establish a key-rotation runbook (re-encrypt all rows with the new key before removing the old one), and ensure ENCRYPTION_KEY is backed up in a secret manager separate from the database backup.
# One-time migration (already applied — idempotent, safe to re-run) node scripts/encrypt-existing-secrets.js # Key rotation runbook # 1. Run migration with OLD key to confirm all rows are encrypted # 2. Write a re-keying script: decrypt with OLD_ENCRYPTION_KEY, re-encrypt with NEW_ENCRYPTION_KEY # 3. UPDATE all rows, then swap the env var and restart # Generate a new key: openssl rand -hex 32 # Health probe (confirms ENCRYPTION_KEY guard passed at startup) GET /api/system/check
7
Monitor Correlation IDs in Server Logs P3 Medium
Error detail leakage is now closed. Next step is operational: set up log aggregation (e.g., PM2 log rotate, Loki, or CloudWatch) to correlate ERR-XXXXXXXX IDs reported by clients with the full server-side log entries for debugging without exposing internals.
# Server logs now emit: # [ERR-A1B2C3D4] POST /api/auth/login SequelizeConnectionError: ... # Clients receive: # { "error": "Internal server error", "id": "ERR-A1B2C3D4" } # interceptor location: src/server.js — res.json safe wrapper # error handler location: src/server.js — next(err) fallback
8
Replace speakeasy with a Maintained TOTP Library P3 Medium
speakeasy has been unmaintained since 2018. Replace it with @otplib/preset-default which is actively maintained and RFC 6238 compliant, with a compatible API.
npm install @otplib/preset-default npm uninstall speakeasy const { authenticator } = require('@otplib/preset-default'); const secret = authenticator.generateSecret(); const isValid = authenticator.verify({ token: code, secret });
9
Add CORS Configuration P4 Low
Configure the cors package to only allow same-origin requests and the platform domain. This prevents cross-origin request forgery from third-party sites.
npm install cors app.use(cors({ origin: process.env.APP_URL, credentials: true }));
10
Hash Invite Tokens Before Storage P4 Low
Store a SHA-256 hash of employee invite tokens in the database. On activation, hash the URL token and compare to the stored hash, so a DB breach does not expose live tokens.
const hash = crypto.createHash('sha256').update(rawToken).digest('hex'); // Store hash in DB; send rawToken in email URL // On activation: compare crypto.createHash('sha256').update(req.params.token).digest('hex') to DB value
📦

Dependency Analysis

10
Direct Deps
0
devDependencies
1
Unmaintained
0
Known CVEs
Package Version Spec Purpose Risk Level Notes
bcryptjs ^2.4.3 Password hashing for all user types LOW Pure-JS bcrypt. Stable. Cost factor 12 used throughout. Actively maintained.
dotenv ^16.4.5 Environment variable loading from .env LOW Dev convenience. Production deployments should use OS env vars directly. Latest stable branch.
express ^4.19.2 HTTP framework — routing, middleware, static serving LOW Express 4 LTS. Express 5 is available but breaking. No known CVEs in 4.19.x.
jsonwebtoken ^9.0.2 JWT signing and verification for all auth roles LOW Version 9 patched CVE-2022-23529. Using HS256 — RSA asymmetric (RS256) preferred for distributed systems.
multer ^1.4.5-lts.1 Multipart file upload handling (images, CSS, JS) LOW LTS version. File type filtering implemented. 8 MB limit for images, 2 MB for code files. MIME type validated.
mysql2 ^3.9.8 MariaDB/MySQL driver with prepared statements LOW Uses Promise API + execute() for parameterized queries preventing SQL injection. Connection pooling enabled.
nodemailer ^8.0.4 Transactional email sending (orders, invites, password resets) LOW Version 8 (major rewrite — ES modules). Per-merchant SMTP configuration. No platform SMTP relay configured.
qrcode ^1.5.4 TOTP QR code generation for 2FA setup flows LOW Generates SVG/data-URI QR codes for authenticator app enrollment. No network calls.
speakeasy ^2.0.0 TOTP/HOTP generation and verification (2FA) MEDIUM ⚠ Last release: 2018. Unmaintained. No security patches since then. Recommend replacing with @otplib/preset-default.
stripe ^20.4.1 Platform Stripe billing — subscriptions, checkout sessions, webhooks LOW v20 SDK. Webhook signature verified via stripe.webhooks.constructEvent() with raw body. Current checkout flow uses inline price_data; STRIPE_PRICE_ID is optional.
Recommended Additions
express-rate-limit
Auth rate limiting
helmet
Security headers
cors
CORS policy
@otplib/preset-default
Replace speakeasy
zod
Request body validation