feat: Add built dist files with EU compliance routes
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s

- Include compiled TypeScript with new /impressum, /privacy, /terms routes
- Temporary commit of dist files for Docker deployment
This commit is contained in:
openclawd 2026-02-16 13:09:25 +00:00
parent 5ef8f34133
commit 1ef8f5743c
21 changed files with 2179 additions and 0 deletions

23
dist/middleware/auth.js vendored Normal file
View file

@ -0,0 +1,23 @@
import { isValidKey, getKeyInfo } from "../services/keys.js";
export function authMiddleware(req, res, next) {
const header = req.headers.authorization;
const xApiKey = req.headers["x-api-key"];
let key;
if (header?.startsWith("Bearer ")) {
key = header.slice(7);
}
else if (xApiKey) {
key = xApiKey;
}
if (!key) {
res.status(401).json({ error: "Missing API key. Use: Authorization: Bearer <key> or X-API-Key: <key>" });
return;
}
if (!isValidKey(key)) {
res.status(403).json({ error: "Invalid API key" });
return;
}
// Attach key info to request for downstream use
req.apiKeyInfo = getKeyInfo(key);
next();
}

91
dist/middleware/pdfRateLimit.js vendored Normal file
View file

@ -0,0 +1,91 @@
import { isProKey } from "../services/keys.js";
// Per-key rate limits (requests per minute)
const FREE_RATE_LIMIT = 10;
const PRO_RATE_LIMIT = 30;
const RATE_WINDOW_MS = 60_000; // 1 minute
// Concurrency limits
const MAX_CONCURRENT_PDFS = 3;
const MAX_QUEUE_SIZE = 10;
const rateLimitStore = new Map();
let activePdfCount = 0;
const pdfQueue = [];
function cleanupExpiredEntries() {
const now = Date.now();
for (const [key, entry] of rateLimitStore.entries()) {
if (now >= entry.resetTime) {
rateLimitStore.delete(key);
}
}
}
function getRateLimit(apiKey) {
return isProKey(apiKey) ? PRO_RATE_LIMIT : FREE_RATE_LIMIT;
}
function checkRateLimit(apiKey) {
cleanupExpiredEntries();
const now = Date.now();
const limit = getRateLimit(apiKey);
const entry = rateLimitStore.get(apiKey);
if (!entry || now >= entry.resetTime) {
// Create new window
rateLimitStore.set(apiKey, {
count: 1,
resetTime: now + RATE_WINDOW_MS
});
return true;
}
if (entry.count >= limit) {
return false;
}
entry.count++;
return true;
}
async function acquireConcurrencySlot() {
if (activePdfCount < MAX_CONCURRENT_PDFS) {
activePdfCount++;
return;
}
if (pdfQueue.length >= MAX_QUEUE_SIZE) {
throw new Error("QUEUE_FULL");
}
return new Promise((resolve, reject) => {
pdfQueue.push({ resolve, reject });
});
}
function releaseConcurrencySlot() {
activePdfCount--;
const waiter = pdfQueue.shift();
if (waiter) {
activePdfCount++;
waiter.resolve();
}
}
export function pdfRateLimitMiddleware(req, res, next) {
const keyInfo = req.apiKeyInfo;
const apiKey = keyInfo?.key || "unknown";
// Check rate limit first
if (!checkRateLimit(apiKey)) {
const limit = getRateLimit(apiKey);
const tier = isProKey(apiKey) ? "pro" : "free";
res.status(429).json({
error: "Rate limit exceeded",
limit: `${limit} PDFs per minute`,
tier,
retryAfter: "60 seconds"
});
return;
}
// Add concurrency control to the request
req.acquirePdfSlot = acquireConcurrencySlot;
req.releasePdfSlot = releaseConcurrencySlot;
next();
}
export function getConcurrencyStats() {
return {
activePdfCount,
queueSize: pdfQueue.length,
maxConcurrent: MAX_CONCURRENT_PDFS,
maxQueue: MAX_QUEUE_SIZE
};
}
// Proactive cleanup every 60s
setInterval(cleanupExpiredEntries, 60_000);

75
dist/middleware/usage.js vendored Normal file
View file

@ -0,0 +1,75 @@
import { isProKey } from "../services/keys.js";
import logger from "../services/logger.js";
import pool from "../services/db.js";
const FREE_TIER_LIMIT = 100;
// In-memory cache, periodically synced to PostgreSQL
let usage = new Map();
function getMonthKey() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
export async function loadUsageData() {
try {
const result = await pool.query("SELECT key, count, month_key FROM usage");
usage = new Map();
for (const row of result.rows) {
usage.set(row.key, { count: row.count, monthKey: row.month_key });
}
logger.info(`Loaded usage data for ${usage.size} keys from PostgreSQL`);
}
catch (error) {
logger.info("No existing usage data found, starting fresh");
usage = new Map();
}
}
async function saveUsageEntry(key, record) {
try {
await pool.query(`INSERT INTO usage (key, count, month_key) VALUES ($1, $2, $3)
ON CONFLICT (key) DO UPDATE SET count = $2, month_key = $3`, [key, record.count, record.monthKey]);
}
catch (error) {
logger.error({ err: error }, "Failed to save usage data");
}
}
export function usageMiddleware(req, res, next) {
const keyInfo = req.apiKeyInfo;
const key = keyInfo?.key || "unknown";
const monthKey = getMonthKey();
if (isProKey(key)) {
trackUsage(key, monthKey);
next();
return;
}
const record = usage.get(key);
if (record && record.monthKey === monthKey && record.count >= FREE_TIER_LIMIT) {
res.status(429).json({
error: "Free tier limit reached",
limit: FREE_TIER_LIMIT,
used: record.count,
upgrade: "Upgrade to Pro for unlimited conversions: https://docfast.dev/pricing",
});
return;
}
trackUsage(key, monthKey);
next();
}
function trackUsage(key, monthKey) {
const record = usage.get(key);
if (!record || record.monthKey !== monthKey) {
const newRecord = { count: 1, monthKey };
usage.set(key, newRecord);
saveUsageEntry(key, newRecord).catch((err) => logger.error({ err }, "Failed to save usage entry"));
}
else {
record.count++;
saveUsageEntry(key, record).catch((err) => logger.error({ err }, "Failed to save usage entry"));
}
}
export function getUsageStats() {
const stats = {};
for (const [key, record] of usage) {
const masked = key.slice(0, 8) + "...";
stats[masked] = { count: record.count, month: record.monthKey };
}
return stats;
}