bazis 0.98.8 → 0.98.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -61,7 +61,11 @@ export class PayloadTooLargeError extends HttpError {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export class TooManyRequestsError extends HttpError {
|
|
64
|
-
constructor(
|
|
64
|
+
constructor(
|
|
65
|
+
readonly retryAfterSeconds: number,
|
|
66
|
+
/** Extra response headers, for example `RateLimit-*` from {@link rateLimit}. */
|
|
67
|
+
readonly headers?: Readonly<Record<string, string>>,
|
|
68
|
+
) {
|
|
65
69
|
super(429, "Too Many Requests");
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -112,6 +112,10 @@ export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware
|
|
|
112
112
|
if (challenge !== undefined) {
|
|
113
113
|
extra.push(["www-authenticate", challenge]);
|
|
114
114
|
}
|
|
115
|
+
const headers = (error as { headers?: Readonly<Record<string, string>> }).headers;
|
|
116
|
+
if (headers !== undefined) {
|
|
117
|
+
extra.push(...Object.entries(headers));
|
|
118
|
+
}
|
|
115
119
|
ctx.response = safeJsonResponse(
|
|
116
120
|
error.status,
|
|
117
121
|
error.details === undefined ? { error: error.message } : { error: error.message, details: error.details },
|
|
@@ -13,12 +13,19 @@ export interface RateLimitOptions {
|
|
|
13
13
|
* bucket when unavailable). Plug your own: user id, API key, etc.
|
|
14
14
|
*/
|
|
15
15
|
keyOf?: (ctx: HttpContext) => string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Read the client address from `X-Forwarded-For` behind trusted proxies.
|
|
18
|
+
* `true` — one proxy: the last address, the one the proxy saw; a number —
|
|
19
|
+
* that many proxies: the address that many hops from the end. Earlier
|
|
20
|
+
* addresses are written by the client and are never used. Off by default.
|
|
21
|
+
*/
|
|
22
|
+
trustProxy?: boolean | number;
|
|
23
|
+
/** Forwarded client-address header used when `trustProxy` is set. */
|
|
19
24
|
proxyHeader?: string;
|
|
20
25
|
/** Hard cap for in-process buckets. New keys receive 429 when full. Default: 10,000. */
|
|
21
26
|
maxBuckets?: number;
|
|
27
|
+
/** Add `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` to responses. Default: true. */
|
|
28
|
+
headers?: boolean;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
interface Bucket {
|
|
@@ -30,7 +37,8 @@ interface Bucket {
|
|
|
30
37
|
* Fixed-window in-memory rate limiter. Per-process state (no external
|
|
31
38
|
* store) — suitable for a single binary; swap `keyOf` or wrap your own
|
|
32
39
|
* middleware for distributed setups. Exceeding the limit throws 429 with
|
|
33
|
-
* Retry-After
|
|
40
|
+
* Retry-After; every response carries `RateLimit-Limit`, `RateLimit-Remaining`
|
|
41
|
+
* and `RateLimit-Reset` unless `headers: false`. At capacity, new keys are rejected until a bucket expires;
|
|
34
42
|
* existing keys retain their quota and live buckets are never evicted.
|
|
35
43
|
*/
|
|
36
44
|
export function rateLimit(options: RateLimitOptions): HttpMiddleware {
|
|
@@ -39,15 +47,22 @@ export function rateLimit(options: RateLimitOptions): HttpMiddleware {
|
|
|
39
47
|
const maxBuckets = Math.floor(options.maxBuckets ?? 10_000);
|
|
40
48
|
let nextExpiry = Number.POSITIVE_INFINITY;
|
|
41
49
|
const proxyHeader = options.proxyHeader ?? "x-forwarded-for";
|
|
50
|
+
const trustedHops = options.trustProxy === true ? 1 : options.trustProxy || 0;
|
|
51
|
+
const withHeaders = options.headers !== false;
|
|
42
52
|
const keyOf = options.keyOf ?? ((ctx: HttpContext) => {
|
|
43
|
-
if (
|
|
44
|
-
const forwarded = ctx.header(proxyHeader)
|
|
53
|
+
if (trustedHops > 0) {
|
|
54
|
+
const forwarded = forwardedClient(ctx.header(proxyHeader), trustedHops);
|
|
45
55
|
if (forwarded) {
|
|
46
56
|
return forwarded;
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
return ctx.clientIp ?? "*";
|
|
50
60
|
});
|
|
61
|
+
const limitHeaders = (remaining: number, resetSeconds: number): Record<string, string> => ({
|
|
62
|
+
"ratelimit-limit": String(options.max),
|
|
63
|
+
"ratelimit-remaining": String(remaining),
|
|
64
|
+
"ratelimit-reset": String(resetSeconds),
|
|
65
|
+
});
|
|
51
66
|
|
|
52
67
|
return async (ctx, next) => {
|
|
53
68
|
const now = Date.now();
|
|
@@ -65,20 +80,45 @@ export function rateLimit(options: RateLimitOptions): HttpMiddleware {
|
|
|
65
80
|
nextExpiry = purgeExpired(buckets, now);
|
|
66
81
|
}
|
|
67
82
|
if (buckets.size >= maxBuckets) {
|
|
68
|
-
|
|
83
|
+
const retryAfter = Math.ceil((nextExpiry - now) / 1000);
|
|
84
|
+
throw new TooManyRequestsError(retryAfter, withHeaders ? limitHeaders(0, retryAfter) : undefined);
|
|
69
85
|
}
|
|
70
86
|
bucket = { count: 0, resetAt: now + options.windowMs };
|
|
71
87
|
buckets.set(key, bucket);
|
|
72
88
|
nextExpiry = Math.min(nextExpiry, bucket.resetAt);
|
|
73
89
|
}
|
|
90
|
+
const resetSeconds = Math.ceil((bucket.resetAt - now) / 1000);
|
|
74
91
|
if (bucket.count >= options.max) {
|
|
75
|
-
throw new TooManyRequestsError(
|
|
92
|
+
throw new TooManyRequestsError(resetSeconds, withHeaders ? limitHeaders(0, resetSeconds) : undefined);
|
|
76
93
|
}
|
|
77
94
|
bucket.count += 1;
|
|
78
95
|
await next();
|
|
96
|
+
if (withHeaders && ctx.response) {
|
|
97
|
+
setIfAbsent(ctx.response, limitHeaders(options.max - bucket.count, resetSeconds));
|
|
98
|
+
}
|
|
79
99
|
};
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The client address `hops` entries from the end of `X-Forwarded-For`: each
|
|
104
|
+
* trusted proxy appends the address it saw, so entries before them are
|
|
105
|
+
* whatever the client sent.
|
|
106
|
+
*/
|
|
107
|
+
function forwardedClient(header: string | undefined, hops: number): string | undefined {
|
|
108
|
+
const addresses = header?.split(",").map((address) => address.trim()).filter((address) => address.length > 0) ?? [];
|
|
109
|
+
return addresses[Math.max(0, addresses.length - hops)];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function setIfAbsent(response: Response, headers: Record<string, string>): void {
|
|
113
|
+
try {
|
|
114
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
115
|
+
if (!response.headers.has(name)) response.headers.set(name, value);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// A raw Response may have immutable headers; the limit itself still applies.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
82
122
|
function assertRateLimitOptions(options: RateLimitOptions): void {
|
|
83
123
|
if (!Number.isFinite(options.windowMs) || options.windowMs <= 0) {
|
|
84
124
|
throw new HttpSetupError("rateLimit windowMs must be a positive finite number.");
|
|
@@ -89,6 +129,9 @@ function assertRateLimitOptions(options: RateLimitOptions): void {
|
|
|
89
129
|
if (options.maxBuckets !== undefined && (!Number.isSafeInteger(options.maxBuckets) || options.maxBuckets <= 0)) {
|
|
90
130
|
throw new HttpSetupError("rateLimit maxBuckets must be a positive integer.");
|
|
91
131
|
}
|
|
132
|
+
if (typeof options.trustProxy === "number" && (!Number.isSafeInteger(options.trustProxy) || options.trustProxy <= 0)) {
|
|
133
|
+
throw new HttpSetupError("rateLimit trustProxy must be true, false or the number of trusted proxies (a positive integer).");
|
|
134
|
+
}
|
|
92
135
|
}
|
|
93
136
|
|
|
94
137
|
function purgeExpired(buckets: Map<string, Bucket>, now: number): number {
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.9" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
package/package.json
CHANGED