docs: add missing OpenAPI annotations for signup/verify, billing/success, billing/webhook
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 16m15s

This commit is contained in:
OpenClaw 2026-02-27 16:04:55 +00:00
parent 427ec8e894
commit 8b31d11e74
15 changed files with 2167 additions and 128 deletions

69
dist/services/db.js vendored
View file

@ -1,20 +1,7 @@
import pg from "pg";
import logger from "./logger.js";
import { isTransientError } from "../utils/errors.js";
const { Pool } = pg;
// Transient error codes from PgBouncer / PostgreSQL that warrant retry
const TRANSIENT_ERRORS = new Set([
"ECONNRESET",
"ECONNREFUSED",
"EPIPE",
"ETIMEDOUT",
"CONNECTION_LOST",
"57P01", // admin_shutdown
"57P02", // crash_shutdown
"57P03", // cannot_connect_now
"08006", // connection_failure
"08003", // connection_does_not_exist
"08001", // sqlclient_unable_to_establish_sqlconnection
]);
const pool = new Pool({
host: process.env.DATABASE_HOST || "172.17.0.1",
port: parseInt(process.env.DATABASE_PORT || "5432", 10),
@ -33,28 +20,7 @@ const pool = new Pool({
pool.on("error", (err, client) => {
logger.error({ err }, "Unexpected error on idle PostgreSQL client — evicted from pool");
});
/**
* Determine if an error is transient (PgBouncer failover, network blip)
*/
export function isTransientError(err) {
if (!err)
return false;
const code = err.code || "";
const msg = (err.message || "").toLowerCase();
if (TRANSIENT_ERRORS.has(code))
return true;
if (msg.includes("no available server"))
return true; // PgBouncer specific
if (msg.includes("connection terminated"))
return true;
if (msg.includes("connection refused"))
return true;
if (msg.includes("server closed the connection"))
return true;
if (msg.includes("timeout expired"))
return true;
return false;
}
export { isTransientError } from "../utils/errors.js";
/**
* Execute a query with automatic retry on transient errors.
*
@ -180,5 +146,36 @@ export async function initDatabase() {
client.release();
}
}
/**
* Clean up stale database entries:
* - Expired pending verifications
* - Unverified free-tier API keys (never completed verification)
* - Orphaned usage rows (key no longer exists)
*/
export async function cleanupStaleData() {
const results = { expiredVerifications: 0, staleKeys: 0, orphanedUsage: 0 };
// 1. Delete expired pending verifications
const pv = await queryWithRetry("DELETE FROM pending_verifications WHERE expires_at < NOW() RETURNING email");
results.expiredVerifications = pv.rowCount || 0;
// 2. Delete unverified free-tier keys (email not in verified verifications)
const sk = await queryWithRetry(`
DELETE FROM api_keys
WHERE tier = 'free'
AND email NOT IN (
SELECT DISTINCT email FROM verifications WHERE verified_at IS NOT NULL
)
RETURNING key
`);
results.staleKeys = sk.rowCount || 0;
// 3. Delete orphaned usage rows
const ou = await queryWithRetry(`
DELETE FROM usage
WHERE key NOT IN (SELECT key FROM api_keys)
RETURNING key
`);
results.orphanedUsage = ou.rowCount || 0;
logger.info({ ...results }, `Database cleanup complete: ${results.expiredVerifications} expired verifications, ${results.staleKeys} stale keys, ${results.orphanedUsage} orphaned usage rows removed`);
return results;
}
export { pool };
export default pool;