The comment named endpoints and described prior behaviour, which is more
detail than a public repo needs. The assertions are unchanged and still
enforce the same guarantee.
The remaining comments in this area state forward-looking rules ("never
return this to a client") rather than describing what used to happen,
which is what a contributor actually needs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user schema had no projection on `password` and no select: false, so
every read of a user document loaded the bcrypt hash. Three paths then
sent it to the client:
- GET /auth/who-am-i returns req.user, which the auth guard populates
with an unprojected usersService.findOne. Every session check.
- The login response returns the whole user document.
- The register response returns the freshly created document.
The hash is bcrypt, so it is not directly usable, and this is not a
plaintext leak. It still matters: it hands an attacker unlimited offline
guessing with no rate limit, no lockout and no audit trail, against
whatever password strength the user chose. It also lands anywhere a
response goes, which for this app includes Microsoft Clarity session
recording, plus devtools, browser extensions, error reporting and any
XSS. Password reuse makes a cracked hash a problem beyond this app.
Fixed in two layers. The schema marks the hash select: false, so nothing
loads it by default and any future read is safe by construction. The two
flows that genuinely need it, login and change-password, ask for it
explicitly via usersService.findOneWithPassword.
select: false does not cover a document held in memory, so the login and
register responses also strip it through withoutPassword before
returning. Those were the two paths that had it loaded on purpose or had
just written it.
The schema guard was confirmed to fail against the pre-change schema
before being trusted.
Verified: 61 API tests pass (from 53). Typecheck introduces no new
errors (2 pre-existing before this change, 1 after).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Message history had no way to find an old message. The endpoint only accepted
page, limit and type, so a client-side search box could only ever filter the
20 rows already loaded, which is more misleading than having no search.
getMessages takes an optional `search` and matches it against the message
body, recipient and sender. It composes with the existing type filter.
The term is escaped before it reaches RegExp. This is not theoretical: an
unescaped "(" throws a SyntaxError and fails the request, "." would match any
character instead of a dot, and a nested quantifier is a ReDoS vector. The
escaping is a separate, directly tested unit.
Note on performance: the existing { device, type, receivedAt } index does not
serve a regex $or, so a search is a scan within one device's messages. That is
fine at current per-device volumes; a text index is the follow-up if it stops
being fine.
Frontend: useDeviceMessages takes `search`, joins it to the query key so each
term caches separately, and builds the query with URLSearchParams so terms
containing & or = are encoded rather than corrupting the URL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard account card showed base plan limits and ignored the
per-user custom limits admins can set, so a user whose limits were
raised saw stale numbers. Surface the effective values and make the
overrides legible.
- api: include the effective bulkSendLimit in the current-subscription
usage payload (the only effective limit it was missing)
- subscription card: render the effective limit with the standard plan
value struck through, an amber "Custom" chip and tooltip, a "Custom
limits" header badge, a new Devices tile, and daily/monthly usage
meters (green under 80%, amber approaching, red at the limit; "0 left"
when over the limit in grace)
- add a rolling-window info note: daily resets at 00:00 UTC, monthly is
a rolling 30 days, separate from the subscription start/renewal dates
- compact the card and lay the four limits out as a 2x2 grid
- bulk send: cap CSV rows by the effective bulk limit so a raised
override is honored instead of the base plan limit
- remove the unused account-settings.tsx duplicate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Polar's subscription.canceled signals a scheduled cancellation (access
continues until the period end), while subscription.revoked is the event
that should actually remove access. The webhook handler was downgrading
immediately on cancel and never handled revoke, so a Pro user dropped to
Free the moment they canceled instead of at their real period end.
- cancelSubscription now records cancelAtPeriodEnd + currentPeriodEnd and
keeps the subscription active instead of deactivating it
- add revokeSubscription for the real downgrade, wired to the
subscription.revoked event in the Polar webhook handler
- add controller + service specs covering event dispatch and the
record-vs-downgrade DB semantics
- add jest moduleNameMapper so specs can resolve src/* path aliases
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2.8 Kotlin client stopped sending `enabled` on registration, so the
backend created devices with the schema default `enabled: false` ("Disabled"),
which users could not activate. Default new registrations to enabled on the
server (so existing 2.8 clients are fixed without an app update), still gated
by the device-limit check, and send `enabled = true` from onboarding.
Also make the gateway toggle always give feedback: show a success toast on
enable/disable and surface the server's reason (e.g. device-limit 429) on
failure instead of silently snapping back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A paid subscriber upgrading (pro -> scale) or downgrading (scale -> pro)
previously got a brand-new Polar checkout, ending up with two live Polar
subscriptions and double billing. Now an active paid subscription is
updated in place via Polar's subscription update API.
- store polarSubscriptionId/polarCustomerId/cancelAtPeriodEnd on
subscriptions (recovered via externalCustomerId for legacy records)
- POST /billing/checkout returns a planChange preview for paid users;
new POST /billing/change-plan executes it (uncancels a scheduled
cancellation first, org-default proration, idempotent with webhooks)
- allow monthly<->yearly interval switches; keep ALREADY_ON_PLAN only
for same plan + same interval; block custom plans (CONTACT_BILLING)
- map Polar 402/403/409 errors to actionable messages; run plan-change
detection before cached checkout-session reuse
- checkout page shows a confirmation screen before applying the change;
account page shows a success toast
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- switchPlan now resolves plan strictly by polarProductId (no name fallback when product ID is provided); throws on unknown product ID
- cancelSubscription: new method that deactivates only the subscription matching the cancelled product, instead of wiping all active subscriptions
- Webhook handler: subscription.created/active/updated no longer hardcodes newPlanName='pro'; subscription.cancelled/canceled use cancelSubscription; subscription.revoked commented out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Scale plan ($29.99/mo, 25k SMS/mo, 15 devices) between Pro and
Custom in the subscription priority chain. Update dashboard upgrade
prompts to surface Scale for Pro users approaching their monthly limit,
and expose Scale upgrade links in subscription-info and account-settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add deviceLimit to plans (default -1 = unlimited) with per-subscription
customDeviceLimit override, resolved in getEffectiveLimits and exposed
via the usage object. Gateway blocks device creation and disabled to
enabled transitions with 429 once the enabled-device count reaches the
limit; already-enabled devices are never affected and the check fails
open on lookup errors. Send a throttled device_limit_reached email
notification and show approaching/reached banners with an upgrade CTA
in the dashboard device list.
Also replace the isYearly checkout field with billingInterval
('monthly' | 'yearly') across DTO, service, and checkout page (legacy
?billing= param still accepted until the marketing site redeploys).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Order the polar products array from the isYearly flag so the chosen
interval is preselected at checkout, forward the ?billing= param from
the checkout page as isYearly, and only reuse cached checkout sessions
that match the requested plan and billing interval and are neither
completed nor abandoned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm v10 is now the npm "latest" tag but generates a different lockfile
format than v9.0 used in this repo, causing --frozen-lockfile to fail.
Replace corepack pnpm@latest with npm install -g pnpm@9 in api and web
Dockerfiles. Also bump docker/* actions to v3/v6 to resolve Node.js 20
deprecation warnings ahead of the June 2026 forced migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fourth_reminder to AbandonedEmailType and schema enum
- Replace expiry-based timing with createdAt-based timing so the
schedule is independent of Stripe session expiry windows
- Register all 6 emails in emailSchedule with correct delays:
10 min, 1 hr, 24 hr, 3 d, 7 d, 14 d after session creation
- Add isCompleted filter to query so paid users are never emailed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete rewrite of all 6 abandoned checkout templates for better
conversion and deliverability:
- Remove social media icons from all templates (promotions tab signal)
- Remove fake discounts, fabricated testimonials, and unverifiable
claims (money-back guarantee, Calendly link)
- Give each email a distinct purpose: recovery nudge (10min), feature
comparison table (1hr), cost objection handling (24hr), personal
founder message (3d), honest comparison + low pressure (7d),
graceful farewell + feedback ask (14d)
- Add opt-out notice to every email footer (required for marketing emails)
- Fix spam trigger subject '⏰ Your textbee pro upgrade is waiting!'
to 'Your TextBee checkout is still open'
- Standardise year to 2026 and brand to on-brand orange throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added `lastEnabledAt` property to track when a user re-enables a previously disabled webhook, preventing immediate auto-disable due to historical failures.
- Updated `WebhookService` to set `lastEnabledAt` when a webhook is re-enabled and adjusted the auto-disable logic to respect a grace period based on this new property.
- Updated device query in GatewayService to cast the filter object, addressing a type collision with the reserved `model` field in Mongoose 9.6. This change maintains the runtime behavior while ensuring type compatibility.
Mongoose 9 updateOne expects an ObjectId for _id; webhookSubscriptionId
was inferred as ObjectId | WebhookSubscription in CI, causing TS2769.
Use webhookSubscription._id after findById instead.
Made-with: Cursor
- Added a new method in AuthService to find active API keys using a masked match and fallback to regex.
- Updated OptionalAuthGuard and AuthGuard to utilize the new method for improved API key validation.
- Introduced an index on the apiKey field in the ApiKey schema for optimized query performance.
- Added a query parameter to filter API keys by status (active, revoked, all) in the getApiKey endpoint.
- Updated the AuthService to handle status filtering logic for API key retrieval.
- Modified the frontend to support status-based API key listing and added a button to view revoked keys.
- Implemented a check for the existence of the insertMany method in the SMS model to enhance flexibility.
- Added a fallback mechanism for models that do not support insertMany, allowing for individual document creation.
- Improved SMS document insertion process by maintaining performance while ensuring compatibility with various model types.
- Introduced batching for SMS document insertion to improve performance.
- Added metadata tracking for SMS to FCM message mapping.
- Implemented error handling for mismatched SMS records and queue payloads.
- Updated SMS queue service to support dynamic batch sizes and immediate queue delays.
- Refactored SMS status updates in SmsQueueProcessor to batch updates for failed and dispatched SMS records.
- Improved error handling by collecting failed SMS details and updating their status in a single operation.
- Updated the SMS queue registration to use asynchronous configuration with dynamic limits from the ConfigService.
- Updated getFcmErrorCode function to remove 'messaging/' prefix from error codes.
- Introduced getFcmErrorMessage function to provide actionable feedback for invalid device tokens.
- Enhanced error messages in SMS processing to utilize the new getFcmErrorMessage function for better clarity.
- Added `dispatchedAt` property to SMS schema and updated status options to include 'dispatched'.
- Implemented logic in SmsQueueProcessor to mark SMS as 'dispatched' upon successful FCM push.
- Enhanced error handling for SMS failures, including specific error codes for FCM delivery issues.
- Updated SmsStatusUpdateTask to handle both 'pending' and 'dispatched' statuses for timeout updates.