Spaces:
Running
Running
File size: 3,276 Bytes
6c3af4e 765119d 6c3af4e 765119d 6c3af4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | /**
* Aggregate counters for the requests that reached token verification.
*
* The rate limiter keys on the client IP, so that is the one thing these must
* not keep: no address, no token, no query, and no timestamp per request. What
* is left says whether the instance is being abused, whether a client is
* broken, and how the shared request budget is spent across the endpoints that
* share it, none of which needs to know who asked.
*/
import {
getRejectedTokenCacheHits,
getRejectedTokenCount,
} from "./rejectedTokens.ts";
import {
RATE_LIMIT_DURATION_SECONDS,
RATE_LIMIT_POINTS,
type RejectionReason,
THUMBNAIL_RATE_LIMIT_DURATION_SECONDS,
THUMBNAIL_RATE_LIMIT_POINTS,
} from "./verifyTokenAndRateLimit.ts";
/**
* Which endpoint family a request was aimed at. `other` is what keeps the
* totals closed when a new endpoint starts using the same verification funnel
* and nobody extends this list.
*/
export type AuthorizationSurface =
| "search"
| "pageContent"
| "thumbnail"
| "inference"
| "other";
interface SurfaceCounts {
authorized: number;
rejected: number;
}
let authorized = 0;
const reasons: Record<RejectionReason, number> = {
rateLimited: 0,
missingToken: 0,
invalidToken: 0,
};
const bySurface: Record<AuthorizationSurface, SurfaceCounts> = {
search: { authorized: 0, rejected: 0 },
pageContent: { authorized: 0, rejected: 0 },
thumbnail: { authorized: 0, rejected: 0 },
inference: { authorized: 0, rejected: 0 },
other: { authorized: 0, rejected: 0 },
};
export function recordAuthorizedRequest(surface: AuthorizationSurface): void {
authorized++;
bySurface[surface].authorized++;
}
export function recordRejectedRequest(
surface: AuthorizationSurface,
reason: RejectionReason,
): void {
reasons[reason]++;
bySurface[surface].rejected++;
}
/**
* `authorized` plus every entry of `reasons` sums to `requests`, and each half
* of `bySurface` sums to its side of that, so a request that goes uncounted
* shows up as a gap rather than being lost silently.
*
* The limiter's own settings ride along because the counts cannot be read
* without them: 40 rate-limited requests means something different at 10
* points per 10 seconds than it would at 100.
*/
export function getAuthorizationStats() {
const rejected = Object.values(reasons).reduce(
(total, count) => total + count,
0,
);
const requests = authorized + rejected;
return {
requests,
authorized,
rejectedRate: Number(((rejected / requests) * 100 || 0).toFixed(1)),
rejectedTokenCacheHits: getRejectedTokenCacheHits(),
distinctRejectedTokens: getRejectedTokenCount(),
reasons: { ...reasons },
// Deep copy, so a caller holding a snapshot for comparison does not watch
// it change under them as later requests arrive.
bySurface: Object.fromEntries(
Object.entries(bySurface).map(([surface, counts]) => [
surface,
{ ...counts },
]),
) as Record<AuthorizationSurface, SurfaceCounts>,
limiter: {
points: RATE_LIMIT_POINTS,
durationSeconds: RATE_LIMIT_DURATION_SECONDS,
thumbnail: {
points: THUMBNAIL_RATE_LIMIT_POINTS,
durationSeconds: THUMBNAIL_RATE_LIMIT_DURATION_SECONDS,
},
},
};
}
|