From 252c929428a9a589a48b2d191db55ef9e86ea90c Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Fri, 19 Jun 2026 19:45:59 +0800 Subject: [PATCH 01/17] docs: update all documentation to reflect $8/mo pricing and PayMongo --- README.md | 6 +- docs/API-REFERENCE.md | 36 ++++++------ docs/ARCHITECTURE-FLOW.md | 66 +++++++++++----------- docs/DATABASE.md | 8 +-- docs/DEPLOYMENT.md | 31 +++++----- docs/ENVIRONMENT-VARIABLES.md | 28 ++++----- docs/ONBOARDING.md | 18 +++--- docs/README.md | 8 +-- docs/SECURITY.md | 12 ++-- docs/TESTING.md | 2 +- tests/e2e/pricing-plan-limits.spec.ts | 10 ++-- tests/unit/components/pricing-data.test.ts | 2 +- 12 files changed, 113 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index b6b9509..2efe736 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ AgentFlow is built on a modern, production-grade stack: | Styling | Tailwind CSS 3.4 | | Database | Supabase (PostgreSQL) | | Authentication | Supabase Auth (Magic Link + Google OAuth) | -| Payments | Stripe | +| Payments | PayMongo | | Email | Resend | | Testing | Vitest (84 tests, 99%+ coverage) + Playwright (E2E) | | CI/CD | GitHub Actions (4-stage automated pipeline) | @@ -86,7 +86,7 @@ Comprehensive developer documentation lives in [`docs/`](./docs/README.md): - [**Components & Hooks**](./docs/COMPONENTS-AND-HOOKS.md) — UI primitive and hook catalog - [**Environment Variables**](./docs/ENVIRONMENT-VARIABLES.md) — full env matrix and per-runtime topology - [**Security**](./docs/SECURITY.md) — defense-in-depth, CSP, headers, captcha, secret rotation -- [**Deployment**](./docs/DEPLOYMENT.md) — CI/CD, Vercel, Supabase, Stripe, Resend, Sentry +- [**Deployment**](./docs/DEPLOYMENT.md) — CI/CD, Vercel, Supabase, PayMongo, Resend, Sentry - [**PWA**](./docs/PWA.md) — manifest, service worker, install prompt - [**Testing**](./docs/TESTING.md) — Vitest + Playwright patterns, auth fixture - [**Onboarding**](./docs/ONBOARDING.md) — first-time dev setup, common tasks, troubleshooting @@ -150,7 +150,7 @@ AgentFlow is built on top of open-source dependencies. Each dependency is govern | React | MIT | | Tailwind CSS | MIT | | Supabase JS Client | MIT | -| Stripe JS | Apache 2.0 | +| PayMongo JS | Apache 2.0 | | Resend | MIT | | Lucide React | ISC | | Vitest | MIT | diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 5e9482f..3fe4f14 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -187,13 +187,13 @@ optional): operations in `/pipeline` are not rate-limited at the database level. -## Stripe +## PayMongo -### `POST /api/stripe/checkout` +### `POST /api/paymongo/checkout` -**File:** `src/app/api/stripe/checkout/route.ts` +**File:** `src/app/api/paymongo/checkout/route.ts` -Create a Stripe Checkout Session for the AgentFlow Pro subscription. +Create a PayMongo Checkout Session for the AgentFlow Pro subscription. **Auth:** required. @@ -211,35 +211,35 @@ Create a Stripe Checkout Session for the AgentFlow Pro subscription. **Implementation:** 1. `supabase.auth.getUser()` → user. -2. Lazy-init the Stripe client (`getStripe()` from - `src/lib/stripe.ts`). -3. `getOrCreateStripeCustomer(user)` — looks up - `profiles.stripe_customer_id`; if missing, creates a Stripe +2. Lazy-init the PayMongo client (`getPayMongo()` from + `src/lib/paymongo.ts`). +3. `getOrCreatePayMongoCustomer(user)` — looks up + `profiles.paymongo_customer_id`; if missing, creates a PayMongo customer and updates the profile via service-role key. 4. `createCheckoutSession(customer, user.email, returnUrl)` — - mode: `subscription`, line_items: `STRIPE_CONFIG.price` (the - $5/mo Pro tier), success_url: `/settings/billing?upgraded=true`, + mode: `subscription`, line_items: `PAYMONGO_CONFIG.price` (the + $8/mo Pro tier), success_url: `/settings/billing?upgraded=true`, cancel_url: `/settings/billing`. 5. Return `{ url: session.url }`. The client (`/settings/billing/page.tsx`) does `window.location.href = url` to start the checkout flow. -### `POST /api/stripe/webhook` +### `POST /api/paymongo/webhook` -**File:** `src/app/api/stripe/webhook/route.ts` +**File:** `src/app/api/paymongo/webhook/route.ts` -Stripe webhook receiver. Signature-verified; the raw body is read +PayMongo webhook receiver. Signature-verified; the raw body is read via `request.text()` and passed to -`constructWebhookEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)`. +`verifyPayMongoSignature(rawBody, sig, PAYMONGO_WEBHOOK_SECRET)`. -**Auth:** Stripe signature verification (no Supabase auth). +**Auth:** PayMongo signature verification (no Supabase auth). **Handled events:** | Event | Handler | | --- | --- | -| `checkout.session.completed` | `handleCheckoutCompleted()` — updates `profiles.plan = 'pro'`, `stripe_customer_id`, `stripe_subscription_id`. | +| `checkout.session.completed` | `handleCheckoutCompleted()` — updates `profiles.plan = 'pro'`, `paymongo_customer_id`, `paymongo_subscription_id`. | | `customer.subscription.deleted` | `handleSubscriptionDeleted()` — sets `profiles.plan = 'free'`. | | `invoice.payment_failed` | `handlePaymentFailed()` — logs a warning and (optionally) downgrades the user. | @@ -312,7 +312,7 @@ monitoring. ```ts export const PLAN_LIMITS = { free: { maxActiveLeads: 10, maxPipelines: 10, price: 0 }, - pro: { maxActiveLeads: Infinity, maxPipelines: Infinity, price: 500 }, // $5/mo + pro: { maxActiveLeads: Infinity, maxPipelines: Infinity, price: 800 }, // $8/mo team: { maxActiveLeads: Infinity, maxPipelines: Infinity, price: 0 }, // internal placeholder } as const; @@ -363,7 +363,7 @@ with appropriate HTTP status codes: | Code | Meaning | | --- | --- | | `400` | Invalid request body (Zod parse failure) | -| `401` | No authenticated user (or bad Stripe signature) | +| `401` | No authenticated user (or bad PayMongo signature) | | `403` | Plan limit reached | | `404` | Resource not found or not owned by user | | `429` | Rate limit exceeded | diff --git a/docs/ARCHITECTURE-FLOW.md b/docs/ARCHITECTURE-FLOW.md index ae6205b..2e2dc56 100644 --- a/docs/ARCHITECTURE-FLOW.md +++ b/docs/ARCHITECTURE-FLOW.md @@ -15,15 +15,15 @@ flowchart LR User([User -- browser]) CF[Cloudflare Turnstile] Supabase[(Supabase
Postgres + Auth)] - Stripe[Stripe] + PayMongo[PayMongo] Resend[Resend Email] Vercel[Vercel
hosting + cron] User <-->|HTTPS| Vercel Vercel -->|API routes| CF Vercel <-->|cookies + RLS| Supabase - Vercel -->|Checkout Sessions| Stripe - Stripe -.->|webhook| Vercel + Vercel -->|Checkout Sessions| PayMongo + PayMongo -.->|webhook| Vercel Vercel -->|daily cron GET| Resend Resend -.->|digest email| User ``` @@ -201,46 +201,46 @@ sequenceDiagram --- -## 7. Stripe — Checkout + Webhook (Pro upgrade) +## 7. PayMongo — Checkout + Webhook (Pro upgrade) -The flow splits into a synchronous user-facing leg (create-checkout → Stripe-hosted page) and an async server leg (webhook → DB update). The webhook is the source of truth — never trust the redirect. +The flow splits into a synchronous user-facing leg (create-checkout → PayMongo-hosted page) and an async server leg (webhook → DB update). The webhook is the source of truth — never trust the redirect. ```mermaid sequenceDiagram autonumber actor U as User participant S as /settings/billing - participant API as /api/stripe/checkout - participant ST as Stripe - participant WH as /api/stripe/webhook + participant API as /api/paymongo/checkout + participant PM as PayMongo + participant WH as /api/paymongo/webhook participant DB as Supabase U->>S: click "Upgrade to Pro" - S->>API: POST /api/stripe/checkout + S->>API: POST /api/paymongo/checkout API->>API: getUser() -> must be logged in API->>DB: SELECT email, full_name FROM profiles - API->>ST: customers.create or reuse stripe_customer_id - ST-->>API: customer.id - API->>ST: checkout.sessions.create({ customer, success_url, cancel_url, metadata.user_id }) - ST-->>API: { url } + API->>PM: customers.create or reuse paymongo_customer_id + PM-->>API: customer.id + API->>PM: checkout.sessions.create({ customer, success_url, cancel_url, metadata.user_id }) + PM-->>API: { url } API-->>S: { url } - S-->>U: window.location -> Stripe-hosted checkout - - U->>ST: enter card, click Subscribe - ST->>ST: create subscription, charge - ST-->>U: 302 -> /settings?upgraded=true - ST->>WH: POST /api/stripe/webhook (event: checkout.session.completed) - WH->>WH: constructWebhookEvent (verify signature) - WH->>DB: UPDATE profiles SET plan='pro', stripe_subscription_id, subscription_status='active' - WH-->>ST: 200 { received: true } + S-->>U: window.location -> PayMongo-hosted checkout + + U->>PM: enter card, click Subscribe + PM->>PM: create subscription, charge + PM-->>U: 302 -> /settings?upgraded=true + PM->>WH: POST /api/paymongo/webhook (event: checkout.session.completed) + WH->>WH: verifyPayMongoSignature (verify signature) + WH->>DB: UPDATE profiles SET plan='pro', paymongo_subscription_id, subscription_status='active' + WH-->>PM: 200 { received: true } ``` -**Webhook handlers** (`src/lib/stripe.ts`): +**Webhook handlers** (`src/lib/paymongo.ts`): - `checkout.session.completed` → `handleCheckoutCompleted` → set plan to `pro` - `customer.subscription.deleted` → `handleSubscriptionDeleted` → set plan to `free` - `invoice.payment_failed` → `handlePaymentFailed` → set `subscription_status='past_due'` -**Idempotency:** Stripe retries webhooks on 5xx. The DB update is a single row-level UPDATE keyed on `id` (the user) — safe to re-apply. +**Idempotency:** PayMongo retries webhooks on 5xx. The DB update is a single row-level UPDATE keyed on `id` (the user) — safe to re-apply. --- @@ -317,8 +317,8 @@ flowchart TD Pipeline["(dashboard)/pipeline/page.tsx"] Billing["(dashboard)/settings/billing/page.tsx"] ApiLeads["/api/leads/route.ts"] - ApiStripe["/api/stripe/checkout"] - ApiWH["/api/stripe/webhook"] + ApiPayMongo["/api/paymongo/checkout"] + ApiWH["/api/paymongo/webhook"] ApiCron["/api/cron/daily-digest"] end @@ -326,7 +326,7 @@ flowchart TD Auth[auth.ts] Valid[validations.ts] RL[rate-limiter.ts] - StripeLib[stripe.ts] + PayMongoLib[paymongo.ts] ResendLib[resend.ts] Constants[constants.ts
PLAN_LIMITS] PlanLimit[plan-limit.ts] @@ -353,10 +353,10 @@ flowchart TD ApiLeads --> RL ApiLeads --> Constants - Billing --> ApiStripe - ApiStripe --> SBS - ApiStripe --> StripeLib - ApiWH --> StripeLib + Billing --> ApiPayMongo + ApiPayMongo --> SBS + ApiPayMongo --> PayMongoLib + ApiWH --> PayMongoLib ApiCron --> SBS ApiCron --> ResendLib @@ -381,8 +381,8 @@ erDiagram text email text full_name text plan "free|pro|team" - text stripe_customer_id - text stripe_subscription_id + text paymongo_customer_id + text paymongo_subscription_id text subscription_status timestamptz created_at timestamptz updated_at diff --git a/docs/DATABASE.md b/docs/DATABASE.md index f429e3e..4c4c5c2 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -20,8 +20,8 @@ migration 001; the trigger is recreated implicitly via | `id` | `uuid` | no | — | Primary key. Foreign key to `auth.users.id` (cascade on delete). | | `email` | `text` | no | — | Synced from `auth.users.email` on signup. | | `full_name` | `text` | yes | `null` | User-provided display name. | -| `plan` | `text` | no | `'free'` | One of `'free'`, `'pro'`, `'team'`. Set by Stripe webhook on subscription change. | -| `stripe_customer_id` | `text` | yes | `null` | Populated on first checkout. | +| `plan` | `text` | no | `'free'` | One of `'free'`, `'pro'`, `'team'`. Set by PayMongo webhook on subscription change. | +| `paymongo_customer_id` | `text` | yes | `null` | Populated on first checkout. | | `created_at` | `timestamptz` | no | `now()` | | | `updated_at` | `timestamptz` | no | `now()` | Updated by trigger on row update. | @@ -317,8 +317,8 @@ Hard deletes are not exposed. - **Modifying `profiles` from a client.** The RLS policy is `auth.uid() = id`, which works. But the `plan` column should - never be set from a client — it's owned by the Stripe webhook. - The `profiles` row is updated by `src/lib/stripe.ts` using the + never be set from a client — it's owned by the PayMongo webhook. + The `profiles` row is updated by `src/lib/paymongo.ts` using the service-role key, which bypasses RLS. - **Inserting actions with a `lead_id` that doesn't belong to the user.** RLS on `leads` will reject the corresponding read, diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index fd38dc9..c4e8472 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,6 +1,6 @@ # Deployment -AgentFlow deploys via Vercel, with Supabase / Stripe / Resend / +AgentFlow deploys via Vercel, with Supabase / PayMongo / Resend / Cloudflare Turnstile as managed services. CI is GitHub Actions; the production release is on a fast-forward merge to `main` plus a Sentry release upload. @@ -38,7 +38,7 @@ Live URL: **https://agent-flow.app**. See [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md#vercel-environment-topology) for the full per-env table. Summary: -- **Production env** (`agent-flow.app`): real Stripe + Resend +- **Production env** (`agent-flow.app`): real PayMongo + Resend keys, real Turnstile site key, real Sentry DSN, real Supabase URL. - **Preview env** (`*.vercel.app`): real services but @@ -135,33 +135,32 @@ To add a new migration: 5. Bump `types/supabase.ts` by re-running `supabase gen types typescript` and committing the diff. -## Stripe +## PayMongo ### Configuration -- **Dashboard:** https://dashboard.stripe.com -- **API version:** `2026-05-27.dahlia` (pinned in - `src/lib/stripe.ts`). -- **Webhook endpoint:** `https://agent-flow.app/api/stripe/webhook` - (Production). Preview deploys do not need a webhook — Stripe - test events go to a separate `stripe listen` CLI forwarding +- **Dashboard:** https://dashboard.paymongo.com +- **API version:** PayMongo V1 API. +- **Webhook endpoint:** `https://agent-flow.app/api/paymongo/webhook` + (Production). Preview deploys do not need a webhook — PayMongo + test events go to a separate `paymongo listen` CLI forwarding setup, not the preview URL. - **Handled events:** `checkout.session.completed`, `customer.subscription.deleted`, `invoice.payment_failed`. - See [API-REFERENCE.md](./API-REFERENCE.md#post-apistripewebhook). + See [API-REFERENCE.md](./API-REFERENCE.md#post-apipaymongowebhook). -### Local testing with `stripe listen` +### Local testing with `paymongo listen` -For local dev, install the Stripe CLI and forward events to +For local dev, install the PayMongo CLI and forward events to your dev server: ```bash -stripe login -stripe listen --forward-to localhost:3000/api/stripe/webhook -# Copy the `whsec_...` to .env.local as STRIPE_WEBHOOK_SECRET +paymongo login +paymongo listen --forward-to localhost:3000/api/paymongo/webhook +# Copy the `whsec_...` to .env.local as PAYMONGO_WEBHOOK_SECRET ``` -Use Stripe's test card numbers +Use PayMongo's test card numbers (e.g. `4242 4242 4242 4242`, any future expiry, any CVC) for test checkouts. diff --git a/docs/ENVIRONMENT-VARIABLES.md b/docs/ENVIRONMENT-VARIABLES.md index e1e9f11..5dec4ae 100644 --- a/docs/ENVIRONMENT-VARIABLES.md +++ b/docs/ENVIRONMENT-VARIABLES.md @@ -57,8 +57,8 @@ on the anon key being secret. ### `SUPABASE_SERVICE_ROLE_KEY` -**Where:** `src/lib/stripe.ts` (for updating `profiles.plan` and -`stripe_customer_id`), `src/lib/resend.ts` (for the daily digest +**Where:** `src/lib/paymongo.ts` (for updating `profiles.plan` and +`paymongo_customer_id`), `src/lib/resend.ts` (for the daily digest to query all users), `src/app/api/cron/daily-digest/route.ts`, `tests/e2e/fixtures/auth.ts` (test-only). @@ -116,11 +116,11 @@ The standard pattern is: > dev (they'd link to production). Keep `.env.local` pointing at > `localhost`. -## Stripe +## PayMongo -### `STRIPE_SECRET_KEY` +### `PAYMONGO_SECRET_KEY` -**Where:** `src/lib/stripe.ts` (lazy-init). Used for Checkout +**Where:** `src/lib/paymongo.ts` (lazy-init). Used for Checkout Session creation, customer lookup, webhook signature verification. **Required:** yes (production). The lazy init means the absence @@ -129,24 +129,24 @@ build failures. Format: `sk_live_...` in production, `sk_test_...` in dev. -### `STRIPE_WEBHOOK_SECRET` +### `PAYMONGO_WEBHOOK_SECRET` -**Where:** `src/app/api/stripe/webhook/route.ts`. Used by -`constructWebhookEvent` to verify the signature on incoming +**Where:** `src/app/api/paymongo/webhook/route.ts`. Used by +`verifyPayMongoSignature` to verify the signature on incoming webhooks. **Required:** yes (production). Get this from -https://dashboard.stripe.com/webhooks after creating a webhook -endpoint that points to `/api/stripe/webhook`. +https://dashboard.paymongo.com/webhooks after creating a webhook +endpoint that points to `/api/paymongo/webhook`. -### `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` +### `NEXT_PUBLIC_PAYMONGO_PUBLISHABLE_KEY` **Where:** Currently not used in app code (the app is server-driven for checkout). Listed for future client-side use -(Stripe Elements, etc.) and for Stripe.js preflight detection. +(PayMongo Elements, etc.) and for PayMongo.js preflight detection. **Required:** not currently. Recommended to set in production -to silence Stripe domain-detection warnings. +to silence PayMongo domain-detection warnings. ## Email (Resend) @@ -369,7 +369,7 @@ infrastructure vars. | SDK | Behavior on missing var | | --- | --- | -| Stripe (`src/lib/stripe.ts`) | Lazy. First call throws. Build succeeds. | +| PayMongo (`src/lib/paymongo.ts`) | Lazy. First call throws. Build succeeds. | | Resend (`src/lib/resend.ts`) | Lazy. First call throws. Build succeeds. | | Supabase browser client | Returns `{}` cast to `SupabaseClient` (safe on SSR). | | Supabase server client | **Throws on module load.** | diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 7e3d5bb..bf17905 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -12,7 +12,7 @@ server, the Supabase database connected, and tests passing. - **Git.** - A Supabase project (the repo includes the schema; the host is your choice — cloud or local Docker). -- A Stripe test account. +- A PayMongo test account. - A Resend account (optional for local dev — the daily-digest cron will silently no-op without it). - A Cloudflare Turnstile site key (optional for local dev — @@ -53,10 +53,10 @@ NEXT_PUBLIC_TURNSTILE_TEST_BYPASS=true Optional (for full feature parity): ```bash -# Stripe -STRIPE_SECRET_KEY=sk_test_... -STRIPE_WEBHOOK_SECRET=whsec_... -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... +# PayMongo +PAYMONGO_SECRET_KEY=sk_test_... +PAYMONGO_WEBHOOK_SECRET=whsec_... +NEXT_PUBLIC_PAYMONGO_PUBLISHABLE_KEY=pk_test_... # Resend RESEND_API_KEY=re_... @@ -226,16 +226,16 @@ Either: - Add `127.0.0.1` too — Cloudflare distinguishes between the two. -### "Stripe webhook returns 400" +### "PayMongo webhook returns 400" -Locally, you need to forward Stripe events to your dev server: +Locally, you need to forward PayMongo events to your dev server: ```bash -stripe listen --forward-to localhost:3000/api/stripe/webhook +paymongo listen --forward-to localhost:3000/api/paymongo/webhook ``` This prints a `whsec_...` signing secret. Set it as -`STRIPE_WEBHOOK_SECRET` in `.env.local`. Do **not** use the +`PAYMONGO_WEBHOOK_SECRET` in `.env.local`. Do **not** use the production secret locally. ### "Vercel build fails with 'SUPABASE_ACCESS_TOKEN' is empty" diff --git a/docs/README.md b/docs/README.md index bb509e2..11e2206 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,9 +19,9 @@ codebase-level reference for the project. | [DATABASE.md](./DATABASE.md) | Tables, RLS, plan-limit trigger, types/supabase.ts consumption, migration history, regenerating schema | | [API-REFERENCE.md](./API-REFERENCE.md) | Every route handler with method, path, request/response shape, auth, rate limit, error semantics | | [COMPONENTS-AND-HOOKS.md](./COMPONENTS-AND-HOOKS.md) | Catalog of every component and hook with props, signature, purpose, and dependencies | -| [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) | Full env-var matrix (Supabase, Stripe, Resend, Turnstile, Sentry, Cron, Captcha, App URL) with where each is consumed | +| [ENVIRONMENT-VARIABLES.md](./ENVIRONMENT-VARIABLES.md) | Full env-var matrix (Supabase, PayMongo, Resend, Turnstile, Sentry, Cron, Captcha, App URL) with where each is consumed | | [SECURITY.md](./SECURITY.md) | CSP, RLS, captcha, rate limiting, security headers, secret management, kill switches, Sentry DSN handling | -| [DEPLOYMENT.md](./DEPLOYMENT.md) | CI/CD pipelines, Vercel + Supabase + Stripe + Resend + Cloudflare Turnstile + Sentry configuration, branch / env topology | +| [DEPLOYMENT.md](./DEPLOYMENT.md) | CI/CD pipelines, Vercel + Supabase + PayMongo + Resend + Cloudflare Turnstile + Sentry configuration, branch / env topology | | [PWA.md](./PWA.md) | `manifest.json`, service worker, install prompt, icon assets, off-screen Turnstile iframe | | [TESTING.md](./TESTING.md) | Vitest unit + Playwright e2e patterns, project setup, auth fixture, captcha bypass, Lighthouse CI | | [ONBOARDING.md](./ONBOARDING.md) | New-developer setup: install, env, dev workflow, common tasks, troubleshooting | @@ -70,8 +70,8 @@ the rest easier to read. `supabase/migrations/002_update_free_tier_limit_to_10.sql`. See [DATABASE.md](./DATABASE.md#plan-tier-enforcement). -- **Lazy module pattern.** Stripe and Resend are lazy-initialized in - `src/lib/stripe.ts` and `src/lib/resend.ts` so a missing API key +- **Lazy module pattern.** PayMongo and Resend are lazy-initialized in + `src/lib/paymongo.ts` and `src/lib/resend.ts` so a missing API key doesn't crash `next build`. The Turnstile widget itself is `React.lazy` (in `src/components/turnstile-widget.tsx`) to keep the Cloudflare script out of the main bundle. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 035ef19..2899cc7 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -16,7 +16,7 @@ the kill switches for emergencies. | Zod input validation | Invalid request bodies are rejected before reaching Supabase | `src/lib/validations.ts` | | Rate limiting | In-memory per-user rate limit on `GET /api/leads` | `src/lib/rate-limiter.ts` | | Security headers | CSP, HSTS, COOP, CORP, X-Frame-Options, X-Content-Type-Options, Permissions-Policy | `next.config.mjs` | -| Stripe webhook signature | Prevents forged webhook events | `src/app/api/stripe/webhook/route.ts` | +| PayMongo webhook signature | Prevents forged webhook events | `src/app/api/paymongo/webhook/route.ts` | | Service-role key isolation | Only used server-side; never inlined in client bundle | Env var, no `NEXT_PUBLIC_` prefix | | Sentry lazy load | Sentry SDK only loaded on actual errors (in `global-error.tsx`) | `src/app/global-error.tsx` | @@ -129,7 +129,7 @@ the full Turnstile flow. The relevant security points: - `POST /api/leads` — currently unprotected. Add a 5 req / 60s limit to slow brute-force inserts. -- `POST /api/stripe/checkout` — currently unprotected. Add a +- `POST /api/paymongo/checkout` — currently unprotected. Add a 3 req / 60s limit to slow webhook-induced loop attacks. ## Input validation @@ -171,8 +171,8 @@ for `profiles`). The service-role key bypasses RLS. It's used by: -- `src/lib/stripe.ts` — update `profiles.plan` and - `stripe_customer_id` on subscription events. +- `src/lib/paymongo.ts` — update `profiles.plan` and + `paymongo_customer_id` on subscription events. - `src/lib/resend.ts` and `src/app/api/cron/daily-digest/route.ts` — query all users for the daily digest. - `tests/e2e/fixtures/auth.ts` — test-only admin operations. @@ -200,8 +200,8 @@ Supabase, not in this repo) is what actually verifies tokens. | Secret | How to rotate | | --- | --- | -| Stripe secret | Stripe dashboard → API keys → roll. Update Vercel. | -| Stripe webhook secret | Stripe dashboard → webhooks → endpoint → roll. Update Vercel. | +| PayMongo secret | PayMongo dashboard → API keys → roll. Update Vercel. | +| PayMongo webhook secret | PayMongo dashboard → webhooks → endpoint → roll. Update Vercel. | | Resend API key | Resend dashboard → API keys → revoke + re-create. Update Vercel. | | Supabase service role | Supabase dashboard → Settings → API → service_role → reset. Update Vercel + all CI secrets. | | Vercel token | Vercel dashboard → account tokens → revoke. Re-create and update GitHub Actions. | diff --git a/docs/TESTING.md b/docs/TESTING.md index c58d5cb..3cad672 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -68,7 +68,7 @@ tests/ │ │ ├── auth.test.ts │ │ ├── plan-limit.test.ts │ │ ├── rate-limiter.test.ts -│ │ ├── stripe.test.ts +│ │ ├── paymongo.test.ts │ │ ├── resend.test.ts │ │ ├── supabase-middleware.test.ts │ │ ├── utils.test.ts diff --git a/tests/e2e/pricing-plan-limits.spec.ts b/tests/e2e/pricing-plan-limits.spec.ts index 66f69e8..aa42980 100644 --- a/tests/e2e/pricing-plan-limits.spec.ts +++ b/tests/e2e/pricing-plan-limits.spec.ts @@ -1,12 +1,12 @@ import { test, expect } from "@playwright/test"; test.describe("Pricing Display", () => { - test("landing page shows $5/mo for Pro plan", async ({ page }) => { + test("landing page shows $8/mo for Pro plan", async ({ page }) => { await page.goto("/"); await page.waitForLoadState("networkidle"); - // Pro plan price should be $5 - const proPrice = page.locator("text=$5").first(); + // Pro plan price should be $8 + const proPrice = page.locator("text=$8").first(); await expect(proPrice).toBeVisible(); // Free plan should show $0 @@ -104,12 +104,12 @@ test.describe("API Endpoints", () => { }); test.describe("Pricing Consistency", () => { - test("all pages reference $5 not $19", async ({ page }) => { + test("all pages reference $8 not $19", async ({ page }) => { // Check landing page - use innerText to avoid React RSC serialized references await page.goto("/"); await page.waitForLoadState("networkidle"); const text = await page.locator("body").innerText(); expect(text).not.toContain("$19"); - expect(text).toContain("$5"); + expect(text).toContain("$8"); }); }); diff --git a/tests/unit/components/pricing-data.test.ts b/tests/unit/components/pricing-data.test.ts index ea73f74..e2ab453 100644 --- a/tests/unit/components/pricing-data.test.ts +++ b/tests/unit/components/pricing-data.test.ts @@ -70,7 +70,7 @@ describe("pricingPlans", () => { it("Pro annual price saves 2 months vs monthly", async () => { const { pricingPlans } = await import("@/lib/pricing-data"); const pro = pricingPlans.find((p) => p.name === "Pro"); - // 10 months * $5 = $50 annual (saves 2 months) + // 10 months * $8 = $80 annual (saves 2 months) expect(pro?.annualPrice).toBe(pro!.monthlyPrice * 10); }); }); From e7a95c9c20f6485eefb2b4b555f2f94f4de7ede4 Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Fri, 19 Jun 2026 19:46:28 +0800 Subject: [PATCH 02/17] docs: add GLM 5.2 security and capability analysis prompt --- GLM5-SECURITY-PROMPT.md | 184 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 GLM5-SECURITY-PROMPT.md diff --git a/GLM5-SECURITY-PROMPT.md b/GLM5-SECURITY-PROMPT.md new file mode 100644 index 0000000..690e631 --- /dev/null +++ b/GLM5-SECURITY-PROMPT.md @@ -0,0 +1,184 @@ +# GLM 5.2 — AgentFlow Security & Capability Analysis Prompt + +## Context + +You are analyzing **AgentFlow**, a production SaaS CRM for solo real estate agents. +It is live at https://agent-flow.app with real users. + +**Stack:** +- Next.js 14.2.35 (App Router, TypeScript strict) +- Supabase Cloud (PostgreSQL + GoTrue + RLS) +- PayMongo (payments, webhooks, subscription management) +- Cloudflare Turnstile (CAPTCHA on auth pages) +- Resend (transactional email) +- Vercel (hosting, edge middleware) +- Sentry (error tracking) + +**Architecture:** +- Server-side auth via Supabase middleware (Edge runtime) +- Row Level Security (RLS) on all tables +- API routes use service-role key for DB access (with `auth.uid()` checks) +- Client-side data fetching via custom hooks +- PayMongo webhooks for subscription lifecycle + +--- + +## Task 1: Security Audit + +Perform a comprehensive security audit of the AgentFlow codebase. Focus on these areas in priority order: + +### Critical (must fix before any release) +1. **Authentication & Authorization** + - Are there any routes that can be accessed without authentication? + - Can a user access another user's data (IDOR)? + - Is the Supabase service-role key ever exposed to the client? + - Are RLS policies correctly enforced, or are there bypass vectors? + +2. **Input Validation & Injection** + - Is every API endpoint validated with Zod? + - Are there SQL injection vectors via Supabase client? + - Is user input sanitized before rendering (XSS)? + - Are file uploads (CSV import) validated for size, content type, and malicious payloads? + +3. **Webhook Security** + - Are PayMongo webhooks verified with signature validation? + - Can an attacker forge webhook events to upgrade their plan? + - Is the webhook secret stored securely (not in client bundle)? + +4. **Secrets & Configuration** + - Are any API keys, tokens, or secrets exposed in the client bundle (`NEXT_PUBLIC_*`)? + - Is `SUPABASE_SERVICE_ROLE_KEY` properly guarded? + - Are environment variables properly scoped (production vs preview vs development)? + +### High +5. **Rate Limiting** + - Is rate limiting applied to all sensitive endpoints (auth, API, webhooks)? + - Is the rate limiter distributed (survives serverless cold starts)? + - Can an attacker bypass rate limiting? + +6. **Session & Cookie Security** + - Are cookies set with `httpOnly`, `secure`, `sameSite`? + - Is there session fixation or session hijacking risk? + - How long do sessions persist? + +7. **API Security** + - Are CORS headers configured correctly? + - Is there CSRF protection on state-changing endpoints? + - Are error messages safe (no stack traces or internal paths leaked)? + +### Medium +8. **Dependency Security** + - Are there known vulnerabilities in `npm audit`? + - Are dependencies pinned or floating? + - Are there any Supply Chain risks? + +9. **Content Security Policy** + - Is CSP configured in production? + - Are there `unsafe-inline` or `unsafe-eval` directives? + - Is the CSP sufficient to prevent XSS? + +10. **Data Exposure** + - Is PII (email, phone, name) exposed in any public endpoint? + - Are database error messages safe? + - Is logging sanitizing sensitive data? + +--- + +## Task 2: Capability Analysis + +Evaluate AgentFlow against these capability dimensions: + +### Core CRM +- Lead management (CRUD, import, search, filter) +- Pipeline management (stages, drag-and-drop, bulk actions) +- Follow-up tracking (daily digest, overdue alerts) +- Contact management (phone, email, SMS integration points) + +### Authentication & Multi-tenancy +- Magic link authentication +- Google OAuth +- Cloudflare Turnstile CAPTCHA +- Row Level Security (multi-tenant isolation) + +### Payments & Subscriptions +- PayMongo checkout (subscription creation) +- Webhook-driven plan enforcement +- Free tier limits (10 leads, 10 pipelines) +- Pro tier (unlimited) + +### Developer Experience +- TypeScript strict mode +- CI/CD (GitHub Actions — 5 workflows) +- Unit tests (Vitest, 254 tests) +- E2E tests (Playwright) +- Load tests + +### Deployment & Infrastructure +- Vercel edge middleware +- Supabase Cloud (managed Postgres) +- Custom domain (`agent-flow.app`) +- PWA capabilities (manifest, service worker) + +--- + +## Task 3: Gap Analysis + +Compare AgentFlow against these competitor capabilities: + +| Feature | Follow Up Boss ($69/mo) | kvCORE ($100/mo) | AgentFlow ($8/mo) | +|---------|------------------------|-------------------|-------------------| +| Unlimited leads | ✅ | ✅ | ✅ (Pro) | +| Pipeline view | ✅ | ✅ | ✅ | +| Mobile app | ✅ | ✅ | PWA | +| Email integration | ✅ | ✅ | Via Resend (digest) | +| SMS integration | ✅ | ✅ | ❌ (planned) | +| Calling | ✅ | ✅ | ✅ (one-tap) | +| Drip campaigns | ✅ | ✅ | ❌ (planned) | +| Team features | ✅ | ✅ | ❌ (solo only) | +| IDX integration | ✅ | ✅ | ❌ | +| Custom branding | ✅ | ✅ | ✅ (Pro) | + +Identify: +1. **Must-have gaps** — features that prevent adoption by solo agents +2. **Nice-to-have gaps** — features that would increase retention +3. **Security gaps** — areas where AgentFlow is weaker than competitors +4. **Pricing gaps** — where the $8 price point creates expectations that aren't met + +--- + +## Output Format + +For each finding, use this format: + +``` +### [SEVERITY] Finding Title + +**Category:** Authentication / Authorization / Injection / etc. +**File(s):** `src/path/to/file.ts:line` +**Description:** What the vulnerability is and how it could be exploited. +**Impact:** What an attacker could achieve. +**Recommendation:** How to fix it, with code example if applicable. +**Effort:** S / M / L (hours to fix) +``` + +For capability analysis, use: + +``` +### Capability: [Name] + +**Current state:** What exists today. +**Gap level:** None / Minor / Significant / Critical. +**Competitor comparison:** How competitors handle this. +**Recommendation:** What to build or improve. +**Priority:** P0 / P1 / P2 / P3. +``` + +--- + +## Constraints + +- Do NOT modify any code. This is an analysis-only task. +- Do NOT expose any actual secrets, API keys, or tokens in your output. +- Use placeholder values (e.g., `sk_xxx`, `NEXT_PUBLIC_SUPABASE_URL`) when referencing secrets. +- Focus on actionable findings, not theoretical risks. +- Prioritize findings by real-world exploitability. From e578a37bfdb985dc68bf8a5b09322bdc1941859c Mon Sep 17 00:00:00 2001 From: Ryan Gabriel Date: Fri, 19 Jun 2026 19:50:37 +0800 Subject: [PATCH 03/17] docs: comprehensive GLM 5.2 security audit and capability analysis prompt --- GLM5-SECURITY-PROMPT.md | 713 +++++++++++++++++++++++++++++++--------- 1 file changed, 563 insertions(+), 150 deletions(-) diff --git a/GLM5-SECURITY-PROMPT.md b/GLM5-SECURITY-PROMPT.md index 690e631..ed88b65 100644 --- a/GLM5-SECURITY-PROMPT.md +++ b/GLM5-SECURITY-PROMPT.md @@ -1,184 +1,597 @@ -# GLM 5.2 — AgentFlow Security & Capability Analysis Prompt +# GLM 5.2 — AgentFlow Comprehensive Security Audit & Capability Analysis + +## Project Context + +**AgentFlow** is a production SaaS CRM for solo real estate agents, live at `https://agent-flow.app` with real paying users ($8/mo Pro tier via Stripe). + +### Tech Stack +| Layer | Technology | Version | +|-------|-----------|---------| +| Framework | Next.js (App Router, TypeScript strict) | 14.2.35 | +| Database | Supabase Cloud (PostgreSQL + GoTrue + RLS) | — | +| Payments | Stripe (checkout sessions + webhooks) | stripe 16.x | +| CAPTCHA | Cloudflare Turnstile | @marsidev/react-turnstile 1.5 | +| Email | Resend | 3.5.x | +| Hosting | Vercel (Edge middleware) | — | +| Error tracking | Sentry | @sentry/nextjs 10 | +| Validation | Zod | 4.x | +| Testing | Vitest (unit) + Playwright (e2e + load) | — | +| CI/CD | GitHub Actions (5 workflows) | — | + +### Architecture Overview +- **Auth**: Supabase GoTrue (magic link + Google OAuth), middleware at `src/middleware.ts` protects `/dashboard`, `/pipeline`, `/leads`, `/follow-ups`, `/settings`, `/api/leads`, `/api/pipeline` +- **Database**: PostgreSQL with Row Level Security (RLS) on all tables. Service-role key used server-side only for webhook handlers and cron jobs. +- **Payments**: Stripe checkout sessions (subscription mode, $8/mo). Webhook at `/api/stripe/webhook` verifies signature. Handlers use `createServiceClient()` (bypasses RLS). +- **API**: Next.js Route Handlers under `src/app/api/`. Each handler authenticates via `supabase.auth.getUser()`, applies rate limiting, validates input with Zod. +- **Client**: React components fetch data via custom hooks (`useLeads`, `useProfile`, `useActions`). Data flows: component → hook → Supabase client (with user JWT) → database. -## Context +--- + +## PART 1: SECURITY AUDIT + +You must analyze every file listed below. For each file, identify specific vulnerabilities with line numbers. Do not generalize — cite the exact code. + +### Files to Analyze + +#### Authentication & Session Management +| File | What to check | +|------|--------------| +| `src/middleware.ts` | Route protection completeness, bypass vectors, matcher regex | +| `src/lib/supabase/middleware.ts` | Session handling, cookie security, fail-closed behavior, redirect validation | +| `src/lib/supabase/server.ts` | Client creation, cookie handling, error handling | +| `src/lib/supabase/client.ts` | Browser client, token storage, session persistence | +| `src/lib/supabase/service.ts` | Service-role key usage, import guards, RLS bypass scope | +| `src/app/auth/callback/route.ts` | OAuth code exchange, redirect validation (open redirect), error handling | +| `src/lib/auth.ts` | Origin detection, callback URL construction | +| `src/app/(auth)/login/page.tsx` | CAPTCHA integration, error handling, rate limiting on client | +| `src/app/(auth)/signup/page.tsx` | Same as login | + +#### API Routes (Attack Surface) +| File | What to check | +|------|--------------| +| `src/app/api/leads/route.ts` | GET/POST auth, rate limiting, plan limit enforcement, input validation, pagination DoS | +| `src/app/api/leads/[id]/route.ts` | GET/PUT/DELETE auth, IDOR prevention, input validation, protected columns | +| `src/app/api/stripe/checkout/route.ts` | Auth, price tampering, customer ID reuse | +| `src/app/api/stripe/webhook/route.ts` | Signature verification, event replay, handler errors | +| `src/app/api/cron/daily-digest/route.ts` | Bearer token auth, service-role usage, email enumeration | +| `src/app/api/health/route.ts` | Info leakage, authentication | + +#### Data Layer +| File | What to check | +|------|--------------| +| `src/lib/validations.ts` | Zod schema completeness, edge cases, type coercion attacks | +| `src/lib/constants.ts` | Plan limits, type safety | +| `src/lib/plan-limit.ts` | Limit enforcement, bypass vectors | +| `src/lib/rate-limiter.ts` | In-memory store, serverless bypass, key collision, race conditions | +| `src/lib/stripe.ts` | Price hardcoded vs configurable, webhook secret handling, service-role usage | +| `src/lib/resend.ts` | API key handling, email injection | +| `src/lib/feature-flags.ts` | Env var injection, fail-open vs fail-closed | +| `src/types/index.ts` | Type safety, data exposure | + +#### Client-Side +| File | What to check | +|------|--------------| +| `src/hooks/useLeads.ts` | Data fetching, error handling, cache invalidation | +| `src/hooks/useProfile.ts` | Profile data exposure | +| `src/hooks/useActions.ts` | Action data handling | +| `src/components/turnstile-widget.tsx` | CAPTCHA bypass, token handling, error states | +| `src/components/auth/captcha-status-pill.tsx` | State machine correctness | +| `src/app/(dashboard)/leads/import/page.tsx` | CSV parsing, file size limits, formula injection, binary content, XSS via cell data | +| `src/app/(dashboard)/leads/[id]/edit/page.tsx` | IDOR, protected field updates | +| `src/app/(dashboard)/leads/[id]/page.tsx` | Data exposure, access control | + +#### Infrastructure +| File | What to check | +|------|--------------| +| `next.config.mjs` | CSP headers, HSTS, COOP/CORP, cache headers, Sentry config | +| `src/sentry.client.config.ts` | DSN exposure, trace rates | +| `src/app/global-error.tsx` | Error boundary, Sentry integration | +| `src/app/layout.tsx` | Meta tags, font loading, preconnect hints | +| `public/sw.js` | Service worker caching strategy, stale data, cache poisoning | +| `package.json` | Dependency versions, known CVEs | + +--- + +### 1.1 Authentication & Authorization (CRITICAL) + +#### 1.1.1 Middleware Bypass Analysis +The middleware at `src/middleware.ts` uses a regex matcher: +``` +/((?!_next/static|_next/image|favicon.ico|manifest.json|sw\\.js|auth/callback|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)/ +``` + +**Test these bypass vectors:** +- Does `/../dashboard` bypass the matcher? +- Does `/%2e%2e/dashboard` bypass? +- Does `/dashboard%00` (null byte) bypass? +- Does `/DASHBOARD` (case variation) bypass? +- Does `/dashboard?foo=/login` (query param injection) bypass? +- Does the middleware correctly handle URL-encoded paths? +- Does `/api/leads` with POST bypass rate limiting (matcher applies to all methods)? +- Can an attacker hit `/api/leads` without the middleware (e.g., via a direct fetch from a different origin)? + +#### 1.1.2 Session Security +In `src/lib/supabase/middleware.ts`: +- Are cookies set with `httpOnly: true`, `secure: true`, `sameSite: "lax"`? +- What is the cookie domain scope? +- Can session cookies be exfiltrated via a subdomain? +- Is there session fixation risk during the OAuth flow? +- What happens when `supabase.auth.getUser()` throws? (line 76-78 — it catches and continues) +- **CRITICAL**: When the catch block runs (line 76-78), the middleware continues to `return supabaseResponse` on line 80. This means a Supabase client error silently passes the request through without auth checking. An attacker who can trigger Supabase errors (e.g., malformed cookies) bypasses authentication. + +#### 1.1.3 IDOR Prevention +In `src/app/api/leads/[id]/route.ts`: +- Every query includes `.eq("user_id", user.id)` — verify this is present on ALL operations (GET, PUT, DELETE) +- The PUT handler uses `leadUpdateSchema` (partial) — can an attacker set `user_id` to a different user? Check if `user_id` is in the update schema or excluded. +- What happens if `id` is not a UUID? Does Supabase return an error or does the query silently return nothing? +- Can an attacker use SQL injection via the `id` parameter? (Supabase client should prevent this, but verify) + +#### 1.1.4 Service-Role Key Exposure +In `src/lib/supabase/service.ts`: +- Is `SUPABASE_SERVICE_ROLE_KEY` ever referenced in any `NEXT_PUBLIC_*` variable? +- Is it imported in any client-side component (any file without `"use server"` or in `src/app/(dashboard)`)? +- Search for `SUPABASE_SERVICE_ROLE_KEY` across all files — is it only in `service.ts` and env config? +- The `createServiceClient()` bypasses RLS — verify it's only used in: + - `src/app/api/stripe/webhook/route.ts` (via `lib/stripe.ts`) + - `src/app/api/cron/daily-digest/route.ts` + - Nowhere else in client-facing code + +#### 1.1.5 Protected Route Completeness +The middleware protects: `/dashboard`, `/pipeline`, `/leads`, `/follow-ups`, `/settings`, `/api/leads`, `/api/pipeline` + +**Analyze what's NOT protected:** +- `/api/health` — is this intentional? Does it leak anything? +- `/changelog`, `/contact`, `/privacy`, `/terms` — public pages (intentional) +- `/demo` — is this a public demo page? Does it expose any data? +- Are there any other API routes that should be protected but aren't? + +--- + +### 1.2 Input Validation & Injection (CRITICAL) + +#### 1.2.1 Zod Schema Analysis +In `src/lib/validations.ts`: +```typescript +export const leadSchema = z.object({ + full_name: z.string().min(1).max(100), + email: z.string().email().optional().nullable(), + phone: z.string().max(20).optional().nullable(), + source: z.enum([...]).default("manual"), + pipeline_stage: z.enum([...]).default("new_lead"), + notes: z.string().max(1000).optional().nullable(), + next_action: z.string().max(200).optional().nullable(), + next_action_date: z.string().optional().nullable(), + is_active: z.boolean().default(true), +}); +``` + +**Check these attack vectors:** +- `next_action_date` is `z.string()` — is it validated as a date format? Can an attacker pass arbitrary strings? +- `full_name` allows 100 characters — is this sufficient? Could a very long name cause UI rendering issues (CSS injection, layout breaking)? +- `notes` allows 1000 characters — is there XSS risk if this is rendered as HTML anywhere? +- `leadUpdateSchema = leadSchema.partial()` — can an attacker update `is_active` to `false` to "hide" leads? Is `is_active` supposed to be user-controllable? +- Does the API route strip `user_id` from the request body before inserting? (It should — the user should not be able to set their own `user_id`) + +#### 1.2.2 CSV Import Security +In `src/app/(dashboard)/leads/import/page.tsx`: +- File size limit: what is it? (Previously 5MB, check current) +- Row limit: what is it? (Previously 1000, check if removed) +- Is the CSV parsed client-side or server-side? +- If client-side: is the parsed data sent to the API for insertion? Is the API validated? +- **Formula injection**: Does the CSV parser sanitize cells starting with `=`, `+`, `-`, `@`? (Excel interprets these as formulas) +- **Binary content**: Is there detection for null bytes or binary content in CSV cells? +- **XSS via cell data**: If a cell contains `` and is rendered in the UI, is it escaped? +- **Prototype pollution**: Does the CSV parser use `Object.assign` or spread with untrusted data? + +#### 1.2.3 XSS Analysis +Check every place where user-provided data is rendered: +- `full_name` — rendered in lead cards, dashboard greeting, settings +- `email` — rendered in lead details +- `phone` — rendered in lead details +- `notes` — rendered in lead details +- `next_action` — rendered in follow-ups +- `source` — rendered as a badge/label +- `pipeline_stage` — rendered as a badge/label + +For each: Is it rendered via `{variable}` (safe, React escapes) or via `dangerouslySetInnerHTML` (unsafe)? + +#### 1.2.4 SQL Injection +Supabase client uses parameterized queries by default. But check: +- Are there any raw SQL queries (`.rpc()`, `.sql()`, `.raw()`)? +- Is there any string concatenation in queries? +- Can an attacker inject via the `.eq()` filter with crafted values? + +--- + +### 1.3 Payment Security (CRITICAL) + +#### 1.1.1 Stripe Webhook Verification +In `src/app/api/stripe/webhook/route.ts`: +- Signature verification uses `stripe.webhooks.constructEvent()` — this is correct +- But verify: is the `STRIPE_WEBHOOK_SECRET` the same for test and production? +- What happens if `STRIPE_WEBHOOK_SECRET` is not set? (Line 11 — returns 500, which is correct) +- Can an attacker replay old webhook events? (Stripe events have timestamps — does the SDK check this?) +- What if the webhook handler throws? (Line 42-44 — returns 500, Stripe will retry. Is there idempotency handling?) + +#### 1.1.2 Price Tampering +In `src/lib/stripe.ts`: +```typescript +export const STRIPE_CONFIG = { + price: 800, // $8.00 + currency: "usd", + ... +}; +``` +- The price is hardcoded server-side — this is correct +- But the checkout session is created with `price_data` (not a Stripe Price ID) — verify this means the price cannot be changed client-side +- Can an attacker create a checkout session with a different price by modifying the request? +- Is there any validation that the webhook's `amount_paid` matches `STRIPE_CONFIG.price`? + +#### 1.1.3 Plan Upgrade Flow +In `handleCheckoutCompleted`: +- The webhook handler updates `profiles.plan` to `"pro"` based on `session.metadata.user_id` +- Can an attacker set `metadata.user_id` to another user's ID when creating a checkout session? +- The checkout session is created in `createCheckoutSession(customerId, userId)` — the `userId` comes from `supabase.auth.getUser()` — verify this is safe +- What if `session.metadata.user_id` is missing? (Line 100-101 — returns early, which is correct) + +#### 1.1.4 Subscription Downgrade +In `handleSubscriptionDeleted`: +- Finds profile by `stripe_subscription_id` — correct +- Sets `plan: "free"` — correct +- But what if the subscription is cancelled but the user still has active leads > 10? Is there enforcement? -You are analyzing **AgentFlow**, a production SaaS CRM for solo real estate agents. -It is live at https://agent-flow.app with real users. +--- -**Stack:** -- Next.js 14.2.35 (App Router, TypeScript strict) -- Supabase Cloud (PostgreSQL + GoTrue + RLS) -- PayMongo (payments, webhooks, subscription management) -- Cloudflare Turnstile (CAPTCHA on auth pages) -- Resend (transactional email) -- Vercel (hosting, edge middleware) -- Sentry (error tracking) +### 1.4 Rate Limiting (HIGH) -**Architecture:** -- Server-side auth via Supabase middleware (Edge runtime) -- Row Level Security (RLS) on all tables -- API routes use service-role key for DB access (with `auth.uid()` checks) -- Client-side data fetching via custom hooks -- PayMongo webhooks for subscription lifecycle +#### 1.4.1 In-Memory Store Bypass +In `src/lib/rate-limiter.ts`: +```typescript +const store = new Map(); +``` +- This is an in-memory `Map` — it does NOT survive serverless cold starts +- On Vercel, each request may hit a different instance — the rate limit counter resets per instance +- An attacker can bypass rate limiting by sending requests fast enough to trigger new instances +- **Impact**: Rate limiting is effectively decorative against a determined attacker +- **Question**: Is this acceptable for the current threat model? Should Redis/Upstash be used? + +#### 1.4.2 Rate Limit Scope +Check all rate-limited endpoints: +- `leads:get` — 100 requests per 60s per user +- `leads:create` — 30 requests per 60s per user +- Are there rate limits on: + - `/api/stripe/checkout` (POST) — should be very restrictive (e.g., 5 per hour) + - `/api/stripe/webhook` (POST) — Stripe handles this, but should we add our own? + - `/api/cron/daily-digest` — protected by Bearer token, but should have rate limit + - `/login` (auth) — Supabase handles this, but should we add our own? + - `/signup` (auth) — same question + +#### 1.4.3 Key Collision +- Rate limit keys are `leads:get:${user.id}` — can an attacker manipulate `user.id`? +- What if `user.id` contains special characters? +- Is there a global rate limit (per IP) in addition to per-user? --- -## Task 1: Security Audit - -Perform a comprehensive security audit of the AgentFlow codebase. Focus on these areas in priority order: - -### Critical (must fix before any release) -1. **Authentication & Authorization** - - Are there any routes that can be accessed without authentication? - - Can a user access another user's data (IDOR)? - - Is the Supabase service-role key ever exposed to the client? - - Are RLS policies correctly enforced, or are there bypass vectors? - -2. **Input Validation & Injection** - - Is every API endpoint validated with Zod? - - Are there SQL injection vectors via Supabase client? - - Is user input sanitized before rendering (XSS)? - - Are file uploads (CSV import) validated for size, content type, and malicious payloads? - -3. **Webhook Security** - - Are PayMongo webhooks verified with signature validation? - - Can an attacker forge webhook events to upgrade their plan? - - Is the webhook secret stored securely (not in client bundle)? - -4. **Secrets & Configuration** - - Are any API keys, tokens, or secrets exposed in the client bundle (`NEXT_PUBLIC_*`)? - - Is `SUPABASE_SERVICE_ROLE_KEY` properly guarded? - - Are environment variables properly scoped (production vs preview vs development)? - -### High -5. **Rate Limiting** - - Is rate limiting applied to all sensitive endpoints (auth, API, webhooks)? - - Is the rate limiter distributed (survives serverless cold starts)? - - Can an attacker bypass rate limiting? - -6. **Session & Cookie Security** - - Are cookies set with `httpOnly`, `secure`, `sameSite`? - - Is there session fixation or session hijacking risk? - - How long do sessions persist? - -7. **API Security** - - Are CORS headers configured correctly? - - Is there CSRF protection on state-changing endpoints? - - Are error messages safe (no stack traces or internal paths leaked)? - -### Medium -8. **Dependency Security** - - Are there known vulnerabilities in `npm audit`? - - Are dependencies pinned or floating? - - Are there any Supply Chain risks? - -9. **Content Security Policy** - - Is CSP configured in production? - - Are there `unsafe-inline` or `unsafe-eval` directives? - - Is the CSP sufficient to prevent XSS? - -10. **Data Exposure** - - Is PII (email, phone, name) exposed in any public endpoint? - - Are database error messages safe? - - Is logging sanitizing sensitive data? +### 1.5 Content Security Policy (MEDIUM) + +In `next.config.mjs`: +```javascript +"script-src 'self' 'unsafe-inline'" + (isDev ? " 'unsafe-eval'" : "") +``` + +**Analyze:** +- `'unsafe-inline'` in `script-src` — this allows inline `` and is rendered in the UI, is it escaped? -- **Prototype pollution**: Does the CSV parser use `Object.assign` or spread with untrusted data? - -#### 1.2.3 XSS Analysis -Check every place where user-provided data is rendered: -- `full_name` — rendered in lead cards, dashboard greeting, settings -- `email` — rendered in lead details -- `phone` — rendered in lead details -- `notes` — rendered in lead details -- `next_action` — rendered in follow-ups -- `source` — rendered as a badge/label -- `pipeline_stage` — rendered as a badge/label - -For each: Is it rendered via `{variable}` (safe, React escapes) or via `dangerouslySetInnerHTML` (unsafe)? - -#### 1.2.4 SQL Injection -Supabase client uses parameterized queries by default. But check: -- Are there any raw SQL queries (`.rpc()`, `.sql()`, `.raw()`)? -- Is there any string concatenation in queries? -- Can an attacker inject via the `.eq()` filter with crafted values? - ---- - -### 1.3 Payment Security (CRITICAL) - -#### 1.1.1 Stripe Webhook Verification -In `src/app/api/stripe/webhook/route.ts`: -- Signature verification uses `stripe.webhooks.constructEvent()` — this is correct -- But verify: is the `STRIPE_WEBHOOK_SECRET` the same for test and production? -- What happens if `STRIPE_WEBHOOK_SECRET` is not set? (Line 11 — returns 500, which is correct) -- Can an attacker replay old webhook events? (Stripe events have timestamps — does the SDK check this?) -- What if the webhook handler throws? (Line 42-44 — returns 500, Stripe will retry. Is there idempotency handling?) - -#### 1.1.2 Price Tampering -In `src/lib/stripe.ts`: -```typescript -export const STRIPE_CONFIG = { - price: 800, // $8.00 - currency: "usd", - ... -}; -``` -- The price is hardcoded server-side — this is correct -- But the checkout session is created with `price_data` (not a Stripe Price ID) — verify this means the price cannot be changed client-side -- Can an attacker create a checkout session with a different price by modifying the request? -- Is there any validation that the webhook's `amount_paid` matches `STRIPE_CONFIG.price`? - -#### 1.1.3 Plan Upgrade Flow -In `handleCheckoutCompleted`: -- The webhook handler updates `profiles.plan` to `"pro"` based on `session.metadata.user_id` -- Can an attacker set `metadata.user_id` to another user's ID when creating a checkout session? -- The checkout session is created in `createCheckoutSession(customerId, userId)` — the `userId` comes from `supabase.auth.getUser()` — verify this is safe -- What if `session.metadata.user_id` is missing? (Line 100-101 — returns early, which is correct) - -#### 1.1.4 Subscription Downgrade -In `handleSubscriptionDeleted`: -- Finds profile by `stripe_subscription_id` — correct -- Sets `plan: "free"` — correct -- But what if the subscription is cancelled but the user still has active leads > 10? Is there enforcement? - ---- - -### 1.4 Rate Limiting (HIGH) - -#### 1.4.1 In-Memory Store Bypass -In `src/lib/rate-limiter.ts`: -```typescript -const store = new Map(); -``` -- This is an in-memory `Map` — it does NOT survive serverless cold starts -- On Vercel, each request may hit a different instance — the rate limit counter resets per instance -- An attacker can bypass rate limiting by sending requests fast enough to trigger new instances -- **Impact**: Rate limiting is effectively decorative against a determined attacker -- **Question**: Is this acceptable for the current threat model? Should Redis/Upstash be used? - -#### 1.4.2 Rate Limit Scope -Check all rate-limited endpoints: -- `leads:get` — 100 requests per 60s per user -- `leads:create` — 30 requests per 60s per user -- Are there rate limits on: - - `/api/stripe/checkout` (POST) — should be very restrictive (e.g., 5 per hour) - - `/api/stripe/webhook` (POST) — Stripe handles this, but should we add our own? - - `/api/cron/daily-digest` — protected by Bearer token, but should have rate limit - - `/login` (auth) — Supabase handles this, but should we add our own? - - `/signup` (auth) — same question - -#### 1.4.3 Key Collision -- Rate limit keys are `leads:get:${user.id}` — can an attacker manipulate `user.id`? -- What if `user.id` contains special characters? -- Is there a global rate limit (per IP) in addition to per-user? - ---- - -### 1.5 Content Security Policy (MEDIUM) - -In `next.config.mjs`: -```javascript -"script-src 'self' 'unsafe-inline'" + (isDev ? " 'unsafe-eval'" : "") -``` - -**Analyze:** -- `'unsafe-inline'` in `script-src` — this allows inline `