Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @fileoverview Express auth middleware — populates req.auth.
|
||||
* @module middleware/authenticate
|
||||
*/
|
||||
|
||||
import { extractBearerToken } from '../lib/tokens.js';
|
||||
import { resolveAuthContext } from '../services/authService.js';
|
||||
import { asyncHandler } from '../errors.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('auth');
|
||||
|
||||
/**
|
||||
* Resolve Bearer or x-api-key into `req.auth`.
|
||||
*/
|
||||
export const authenticate = asyncHandler(async (req, res, next) => {
|
||||
const rawToken =
|
||||
extractBearerToken(req.headers.authorization) ?? req.headers['x-api-key'] ?? null;
|
||||
|
||||
req.auth = await resolveAuthContext(rawToken);
|
||||
log.debug('authenticated', {
|
||||
tenantId: req.auth.tenantId,
|
||||
method: req.auth.authMethod,
|
||||
plan: req.auth.tenant?.plan,
|
||||
});
|
||||
next();
|
||||
});
|
||||
56
packages/verae-zapier-middleware/src/middleware/rateLimit.js
Normal file
56
packages/verae-zapier-middleware/src/middleware/rateLimit.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @fileoverview Simple in-memory per-tenant rate limiter.
|
||||
* @module middleware/rateLimit
|
||||
*/
|
||||
|
||||
import { AppError, asyncHandler } from '../errors.js';
|
||||
import { getTenant, resolveLimits } from '../store/tenants.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('billing');
|
||||
|
||||
/** @type {Map<string, { windowStart: number, count: number }>} */
|
||||
const windows = new Map();
|
||||
|
||||
/**
|
||||
* Reset rate limit windows (tests).
|
||||
* @returns {void}
|
||||
*/
|
||||
export function resetRateLimitWindows() {
|
||||
windows.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce plan requestsPerMinute using a 60s sliding fixed window.
|
||||
*/
|
||||
export const rateLimit = asyncHandler(async (req, res, next) => {
|
||||
const tenantId = req.auth?.tenantId;
|
||||
if (!tenantId) return next();
|
||||
|
||||
const tenant = getTenant(tenantId);
|
||||
const limits = tenant
|
||||
? resolveLimits(tenant)
|
||||
: { requestsPerMinute: 30 };
|
||||
|
||||
const rpm = limits.requestsPerMinute ?? 30;
|
||||
const now = Date.now();
|
||||
const windowMs = 60_000;
|
||||
|
||||
let entry = windows.get(tenantId);
|
||||
if (!entry || now - entry.windowStart >= windowMs) {
|
||||
entry = { windowStart: now, count: 0 };
|
||||
windows.set(tenantId, entry);
|
||||
}
|
||||
|
||||
entry.count += 1;
|
||||
if (entry.count > rpm) {
|
||||
log.debug('rate limited', { tenantId, count: entry.count, rpm });
|
||||
throw new AppError('Rate limit exceeded', {
|
||||
status: 429,
|
||||
code: 'RATE_LIMITED',
|
||||
details: { requestsPerMinute: rpm },
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue