docfast/dist/services/keys.js
DocFast Bot 45b5be248c
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m58s
Promote to Production / Deploy to Production (push) Successful in 2m21s
docs: remove free tier, update rate limits and auth for demo+pro model
- Remove free tier from rate limits, add Demo (5/hour, watermarked)
- Update auth section: remove free-tier key mention, link to docfast.dev
- Update getting started: demo → upgrade to Pro → use API key
- Add deprecated: true to /v1/signup/free swagger annotation
- Regenerate openapi.json
2026-02-20 19:10:25 +00:00

137 lines
5.4 KiB
JavaScript

import { randomBytes } from "crypto";
import logger from "./logger.js";
import { queryWithRetry } from "./db.js";
// In-memory cache for fast lookups, synced with PostgreSQL
let keysCache = [];
export async function loadKeys() {
try {
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys");
keysCache = result.rows.map((r) => ({
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,
}));
}
catch (err) {
logger.error({ err }, "Failed to load keys from PostgreSQL");
keysCache = [];
}
// Also load seed keys from env
const envKeys = process.env.API_KEYS?.split(",").map((k) => k.trim()).filter(Boolean) || [];
for (const k of envKeys) {
if (!keysCache.find((e) => e.key === k)) {
const entry = { key: k, tier: "pro", email: "seed@docfast.dev", createdAt: new Date().toISOString() };
keysCache.push(entry);
// Upsert into DB
await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4)
ON CONFLICT (key) DO NOTHING`, [k, "pro", "seed@docfast.dev", new Date().toISOString()]).catch(() => { });
}
}
}
export function isValidKey(key) {
return keysCache.some((k) => k.key === key);
}
export function getKeyInfo(key) {
return keysCache.find((k) => k.key === key);
}
export function isProKey(key) {
const info = getKeyInfo(key);
return info?.tier === "pro";
}
function generateKey(prefix) {
return `${prefix}_${randomBytes(24).toString("hex")}`;
}
export async function createFreeKey(email) {
if (email) {
const existing = keysCache.find((k) => k.email === email && k.tier === "free");
if (existing)
return existing;
}
const entry = {
key: generateKey("df_free"),
tier: "free",
email: email || "",
createdAt: new Date().toISOString(),
};
await queryWithRetry("INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4)", [entry.key, entry.tier, entry.email, entry.createdAt]);
keysCache.push(entry);
return entry;
}
export async function createProKey(email, stripeCustomerId) {
// Check in-memory cache first (fast path)
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
if (existing) {
existing.tier = "pro";
await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
return existing;
}
// UPSERT: handles duplicate webhooks across pods via DB unique index
const newKey = generateKey("df_pro");
const now = new Date().toISOString();
const result = await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL
DO UPDATE SET tier = 'pro'
RETURNING key, tier, email, created_at, stripe_customer_id`, [newKey, "pro", email, now, stripeCustomerId]);
const row = result.rows[0];
const entry = {
key: row.key,
tier: row.tier,
email: row.email,
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
stripeCustomerId: row.stripe_customer_id || undefined,
};
// Refresh in-memory cache
const cacheIdx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
if (cacheIdx >= 0) {
keysCache[cacheIdx] = entry;
}
else {
keysCache.push(entry);
}
return entry;
}
export async function downgradeByCustomer(stripeCustomerId) {
const entry = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
if (entry) {
entry.tier = "free";
await queryWithRetry("UPDATE api_keys SET tier = 'free' WHERE stripe_customer_id = $1", [stripeCustomerId]);
return true;
}
return false;
}
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,
};
}
export function getAllKeys() {
return [...keysCache];
}
export async function updateKeyEmail(apiKey, newEmail) {
const entry = keysCache.find((k) => k.key === apiKey);
if (!entry)
return false;
entry.email = newEmail;
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
return true;
}
export async function updateEmailByCustomer(stripeCustomerId, newEmail) {
const entry = keysCache.find(k => k.stripeCustomerId === stripeCustomerId);
if (!entry)
return false;
entry.email = newEmail;
await queryWithRetry("UPDATE api_keys SET email = $1 WHERE stripe_customer_id = $2", [newEmail, stripeCustomerId]);
return true;
}