Document rate limit headers in OpenAPI spec
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
- Add reusable header components (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After) - Reference headers in 200 responses on all conversion and demo endpoints - Add Retry-After header to 429 responses - Update Rate Limits section in API description to mention response headers - Add comprehensive tests for header documentation (21 new tests) - All 809 tests passing
This commit is contained in:
parent
a3bba8f0d5
commit
70eb6908e3
18 changed files with 801 additions and 821 deletions
77
dist/services/keys.js
vendored
77
dist/services/keys.js
vendored
|
|
@ -3,6 +3,20 @@ import logger from "./logger.js";
|
|||
import { queryWithRetry } from "./db.js";
|
||||
// In-memory cache for fast lookups, synced with PostgreSQL
|
||||
let keysCache = [];
|
||||
/** Look up a key row in the DB by a given column. Returns null if not found. */
|
||||
export async function findKeyInCacheOrDb(column, value) {
|
||||
const result = await queryWithRetry(`SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE ${column} = $1 LIMIT 1`, [value]);
|
||||
if (result.rows.length === 0)
|
||||
return null;
|
||||
const r = result.rows[0];
|
||||
return {
|
||||
key: r.key,
|
||||
tier: r.tier,
|
||||
email: r.email,
|
||||
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
||||
stripeCustomerId: r.stripe_customer_id || undefined,
|
||||
};
|
||||
}
|
||||
export async function loadKeys() {
|
||||
try {
|
||||
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys");
|
||||
|
|
@ -102,55 +116,60 @@ export async function downgradeByCustomer(stripeCustomerId) {
|
|||
}
|
||||
// DB fallback: key may exist on another pod's cache or after a restart
|
||||
logger.info({ stripeCustomerId }, "downgradeByCustomer: cache miss, falling back to DB");
|
||||
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE stripe_customer_id = $1 LIMIT 1", [stripeCustomerId]);
|
||||
if (result.rows.length === 0) {
|
||||
const dbKey = await findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId);
|
||||
if (!dbKey) {
|
||||
logger.warn({ stripeCustomerId }, "downgradeByCustomer: customer not found in cache or DB");
|
||||
return false;
|
||||
}
|
||||
const row = result.rows[0];
|
||||
await queryWithRetry("UPDATE api_keys SET tier = 'free' WHERE stripe_customer_id = $1", [stripeCustomerId]);
|
||||
// Add to local cache so subsequent lookups on this pod work
|
||||
const cached = {
|
||||
key: row.key,
|
||||
tier: "free",
|
||||
email: row.email,
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
||||
stripeCustomerId: row.stripe_customer_id || undefined,
|
||||
};
|
||||
keysCache.push(cached);
|
||||
logger.info({ stripeCustomerId, key: row.key }, "downgradeByCustomer: downgraded via DB fallback");
|
||||
dbKey.tier = "free";
|
||||
keysCache.push(dbKey);
|
||||
logger.info({ stripeCustomerId, key: dbKey.key }, "downgradeByCustomer: downgraded via DB fallback");
|
||||
return true;
|
||||
}
|
||||
export async function findKeyByCustomerId(stripeCustomerId) {
|
||||
// Check DB directly — survives pod restarts unlike in-memory cache
|
||||
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE stripe_customer_id = $1 LIMIT 1", [stripeCustomerId]);
|
||||
if (result.rows.length === 0)
|
||||
return null;
|
||||
const r = result.rows[0];
|
||||
return {
|
||||
key: r.key,
|
||||
tier: r.tier,
|
||||
email: r.email,
|
||||
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
||||
stripeCustomerId: r.stripe_customer_id || undefined,
|
||||
};
|
||||
return findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId);
|
||||
}
|
||||
export function getAllKeys() {
|
||||
return [...keysCache];
|
||||
}
|
||||
export async function updateKeyEmail(apiKey, newEmail) {
|
||||
const entry = keysCache.find((k) => k.key === apiKey);
|
||||
if (!entry)
|
||||
if (entry) {
|
||||
entry.email = newEmail;
|
||||
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
|
||||
return true;
|
||||
}
|
||||
// DB fallback: key may exist on another pod's cache or after a restart
|
||||
logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: cache miss, falling back to DB");
|
||||
const dbKey = await findKeyInCacheOrDb("key", apiKey);
|
||||
if (!dbKey) {
|
||||
logger.warn({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: key not found in cache or DB");
|
||||
return false;
|
||||
entry.email = newEmail;
|
||||
}
|
||||
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
|
||||
dbKey.email = newEmail;
|
||||
keysCache.push(dbKey);
|
||||
logger.info({ apiKey: apiKey.slice(0, 10) + "..." }, "updateKeyEmail: updated via DB fallback");
|
||||
return true;
|
||||
}
|
||||
export async function updateEmailByCustomer(stripeCustomerId, newEmail) {
|
||||
const entry = keysCache.find(k => k.stripeCustomerId === stripeCustomerId);
|
||||
if (!entry)
|
||||
if (entry) {
|
||||
entry.email = newEmail;
|
||||
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]);
|
||||
return true;
|
||||
}
|
||||
// DB fallback: key may exist on another pod's cache or after a restart
|
||||
logger.info({ stripeCustomerId }, "updateEmailByCustomer: cache miss, falling back to DB");
|
||||
const dbKey = await findKeyInCacheOrDb("stripe_customer_id", stripeCustomerId);
|
||||
if (!dbKey) {
|
||||
logger.warn({ stripeCustomerId }, "updateEmailByCustomer: customer not found in cache or DB");
|
||||
return false;
|
||||
entry.email = newEmail;
|
||||
}
|
||||
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]);
|
||||
dbKey.email = newEmail;
|
||||
keysCache.push(dbKey);
|
||||
logger.info({ stripeCustomerId, key: dbKey.key }, "updateEmailByCustomer: updated via DB fallback");
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue