fix: cancelled tier, remove key logging, add billing rate limits
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m13s

- Add 'cancelled' tier (0 req/month) for downgraded subscriptions
- Remove full API key from recovery endpoint logs (security)
- Add IP-based rate limiting (10/15min) to billing endpoints
- Bump version to 0.7.0
- 4 new tests (338 total)
This commit is contained in:
Hoid 2026-03-04 09:06:16 +01:00
parent f3a363fb17
commit 9575d312fe
5 changed files with 68 additions and 7 deletions

View file

@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getTierLimit, getKeyByEmail, getCustomerIdByEmail } from '../keys.js'
import { getTierLimit, getKeyByEmail, getCustomerIdByEmail, downgradeByCustomer } from '../keys.js'
// Mock the db module
vi.mock('../db.js', () => ({
@ -25,6 +25,10 @@ describe('getTierLimit', () => {
expect(getTierLimit('business')).toBe(25000)
})
it('should return 0 for cancelled tier', () => {
expect(getTierLimit('cancelled')).toBe(0)
})
it('should return 100 for unknown tier', () => {
expect(getTierLimit('enterprise')).toBe(100)
})
@ -124,3 +128,20 @@ describe('getCustomerIdByEmail', () => {
expect(result).toBeUndefined()
})
})
describe('downgradeByCustomer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should set tier to cancelled instead of free', async () => {
vi.mocked(queryWithRetry).mockResolvedValue({ rows: [] })
await downgradeByCustomer('cus_test_123')
expect(queryWithRetry).toHaveBeenCalledWith(
expect.stringContaining("'cancelled'"),
['cus_test_123']
)
})
})

View file

@ -4,7 +4,7 @@ import { queryWithRetry } from "./db.js";
export interface ApiKey {
key: string;
tier: "free" | "starter" | "pro" | "business";
tier: "free" | "cancelled" | "starter" | "pro" | "business";
email: string;
createdAt: string;
stripeCustomerId?: string;
@ -130,6 +130,7 @@ export function getAllKeys(): ApiKey[] {
export function getTierLimit(tier: string): number {
switch (tier) {
case "free": return 100;
case "cancelled": return 0;
case "starter": return 1000;
case "pro": return 5000;
case "business": return 25000;
@ -184,16 +185,16 @@ export async function createPaidKey(email: string, tier: "starter" | "pro" | "bu
export async function downgradeByCustomer(customerId: string): Promise<void> {
await queryWithRetry(
"UPDATE api_keys SET tier = 'free', stripe_customer_id = NULL WHERE stripe_customer_id = $1",
"UPDATE api_keys SET tier = 'cancelled', stripe_customer_id = NULL WHERE stripe_customer_id = $1",
[customerId]
);
for (const k of keysCache) {
if (k.stripeCustomerId === customerId) {
k.tier = "free";
k.tier = "cancelled";
k.stripeCustomerId = undefined;
}
}
logger.info({ customerId }, "Downgraded customer to free");
logger.info({ customerId }, "Downgraded customer to cancelled");
}
export async function updateEmailByCustomer(customerId: string, newEmail: string): Promise<void> {