Initial import of verae-middleware from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 14:59:27 -04:00
commit 0c807706f3
67 changed files with 6630 additions and 0 deletions

View file

@ -0,0 +1,160 @@
/**
* @fileoverview Plan quotas and usage recording.
* @module services/entitlementService
*/
import { config } from '../config.js';
import { AppError } from '../errors.js';
import { getTenant, resolveLimits } from '../store/tenants.js';
import { getUsage, incrementUsage } from '../store/usage.js';
import { createDebugger } from '../debug/logger.js';
const log = createDebugger('billing');
function isUnlimited(value) {
return value === null || value === undefined;
}
function quotaExceeded({ limit, used, allowOverage }) {
if (isUnlimited(limit)) return false;
if (used < limit) return false;
return !allowOverage;
}
/**
* Ensure tenant may perform an action under plan limits.
*
* @param {string} tenantId
* @param {'timestamp'|'verify'|'batch_timestamp'} action
* @param {{ amount?: number }} [options]
* @returns {{ tenant: object, limits: object, usage: object }}
*/
export function checkEntitlement(tenantId, action, { amount = 1 } = {}) {
const tenant = getTenant(tenantId);
if (!tenant) {
throw new AppError('Unknown tenant', { status: 401, code: 'UNAUTHORIZED' });
}
const limits = resolveLimits(tenant);
const usage = getUsage(tenantId);
log.debug('checkEntitlement', {
tenantId,
action,
plan: tenant.plan,
amount,
timestampsUsed: usage.timestamps,
timestampsLimit: limits.timestamps,
});
if (action === 'batch_timestamp') {
if (!limits.batch) {
throw new AppError('Batch timestamps require a paid plan', {
status: 403,
code: 'PLAN_UPGRADE_REQUIRED',
details: { upgradeUrl: config.upgradeUrl },
});
}
if (!isUnlimited(limits.batchMaxItems) && amount > limits.batchMaxItems) {
throw new AppError(`Batch size exceeds plan limit of ${limits.batchMaxItems}`, {
status: 403,
code: 'BATCH_LIMIT_EXCEEDED',
details: { upgradeUrl: config.upgradeUrl, limit: limits.batchMaxItems },
});
}
}
if (action === 'timestamp') {
if (
quotaExceeded({
limit: limits.timestamps,
used: usage.timestamps,
allowOverage: limits.allowOverage,
})
) {
const err = new AppError('Monthly timestamp quota exceeded', {
status: 402,
code: 'QUOTA_EXCEEDED',
details: {
limit: limits.timestamps,
used: usage.timestamps,
period: 'monthly',
upgradeUrl: config.upgradeUrl,
},
});
throw err;
}
}
if (action === 'verify') {
if (
quotaExceeded({
limit: limits.verifications,
used: usage.verifications,
allowOverage: limits.allowOverage,
})
) {
throw new AppError('Monthly verification quota exceeded', {
status: 402,
code: 'QUOTA_EXCEEDED',
details: {
limit: limits.verifications,
used: usage.verifications,
period: 'monthly',
upgradeUrl: config.upgradeUrl,
},
});
}
}
return { tenant, limits, usage };
}
/**
* Record usage after a successful action.
*
* @param {string} tenantId
* @param {string} action
* @param {{ amount?: number }} [options]
* @returns {void}
*/
export function recordUsage(tenantId, action, { amount = 1 } = {}) {
const tenant = getTenant(tenantId);
if (!tenant) return;
const limits = resolveLimits(tenant);
const usage = getUsage(tenantId);
if (action === 'timestamp') {
if (
!isUnlimited(limits.timestamps) &&
usage.timestamps >= limits.timestamps &&
limits.allowOverage
) {
incrementUsage(tenantId, 'overage.timestamps', amount);
}
incrementUsage(tenantId, 'timestamps', amount);
}
if (action === 'verify') {
if (
!isUnlimited(limits.verifications) &&
usage.verifications >= limits.verifications &&
limits.allowOverage
) {
incrementUsage(tenantId, 'overage.verifications', amount);
}
incrementUsage(tenantId, 'verifications', amount);
}
if (action === 'status') {
incrementUsage(tenantId, 'statusChecks', amount);
}
if (action === 'batch_timestamp') {
incrementUsage(tenantId, 'batchTimestamps', amount);
incrementUsage(tenantId, 'timestamps', amount);
}
log.debug('recordUsage', { tenantId, action, amount });
}