ethercalc
Version:
Multi-User Spreadsheet Server — TypeScript rewrite (Cloudflare fullstack)
102 lines (91 loc) • 2.71 kB
text/typescript
/**
* Optional per-IP rate limit on room-creation endpoints (SH-3). Gated by
* `ETHERCALC_ROOM_CREATE_LIMIT` (default off). Complements the general
* `ETHERCALC_RATELIMIT` bucket — creation paths get a stricter default.
*/
import {
createRateLimitStore,
type RateLimitConfig,
type RateLimitStore,
} from './rate-limit.ts';
export interface RoomCreateLimitEnv {
readonly ETHERCALC_ROOM_CREATE_LIMIT?: string | null;
}
/** Six new rooms per minute per source (burst 6). */
const DEFAULT_CAPACITY = 6;
const DEFAULT_REFILL_PER_SEC = 6 / 60;
function limitDisabled(raw: string): boolean {
switch (raw.trim().toLowerCase()) {
case '':
case '0':
case 'false':
case 'no':
case 'off':
return true;
default:
return false;
}
}
/**
* Parse `ETHERCALC_ROOM_CREATE_LIMIT` — same forms as `ETHERCALC_RATELIMIT`
* but bare `1`/`true`/`on` defaults to 6 creations per 60 seconds.
*/
export function parseRoomCreateLimitConfig(
raw: string | null | undefined,
): RateLimitConfig | null {
if (raw == null) return null;
const trimmed = raw.trim();
if (limitDisabled(trimmed)) return null;
switch (trimmed.toLowerCase()) {
case '1':
case 'true':
case 'yes':
case 'on':
return {
capacity: DEFAULT_CAPACITY,
refillPerSec: DEFAULT_REFILL_PER_SEC,
};
}
const colon = trimmed.indexOf(':');
if (colon >= 0) {
const windowSec = Number(trimmed.slice(0, colon).trim());
const maxRequests = Number(trimmed.slice(colon + 1).trim());
if (
!Number.isFinite(windowSec) ||
!Number.isFinite(maxRequests) ||
windowSec <= 0 ||
maxRequests <= 0
) {
return null;
}
return {
capacity: maxRequests,
refillPerSec: maxRequests / windowSec,
};
}
const rps = Number(trimmed);
if (!Number.isFinite(rps) || rps <= 0) return null;
return {
capacity: Math.max(rps * 3, rps),
refillPerSec: rps,
};
}
export function roomCreateLimitFromEnv(
env: RoomCreateLimitEnv,
): RateLimitConfig | null {
return parseRoomCreateLimitConfig(env.ETHERCALC_ROOM_CREATE_LIMIT);
}
/** Routes that mint or seed a new room DO. */
export function isRoomCreationRoute(method: string, pathname: string): boolean {
if (method === 'POST' && pathname === '/_') return true;
if (method === 'GET' && (pathname === '/_new' || pathname === '/=_new')) {
return true;
}
if (method === 'GET' && pathname.startsWith('/_from/')) return true;
if (method === 'PUT' && pathname.startsWith('/_/')) {
const rest = pathname.slice(3);
return rest.length > 0 && !rest.includes('/');
}
return false;
}
export { createRateLimitStore, type RateLimitStore };