diff --git a/package-lock.json b/package-lock.json index 37ab73e..aef8f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "dependencies": { "yaml": "^2.8.3" }, + "bin": { + "relay": "dist/cli/main.js" + }, "devDependencies": { "@biomejs/biome": "^2.3.5", "@mariozechner/pi-agent-core": "^0.69.0", @@ -886,9 +889,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -906,9 +906,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -926,9 +923,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -946,9 +940,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -1555,9 +1546,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1575,9 +1563,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1595,9 +1580,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1615,9 +1597,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1635,9 +1614,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1972,9 +1948,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1989,9 +1962,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2006,9 +1976,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2023,9 +1990,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2040,9 +2004,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2057,9 +2018,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2074,9 +2032,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2091,9 +2046,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2108,9 +2060,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2125,9 +2074,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2142,9 +2088,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2159,9 +2102,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2176,9 +2116,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/utils/rate-limit.ts b/src/utils/rate-limit.ts new file mode 100644 index 0000000..38f8322 --- /dev/null +++ b/src/utils/rate-limit.ts @@ -0,0 +1,76 @@ +export interface RateLimitConfig { + maxRequests: number; + windowMs: number; +} + +export class RateLimiter { + private timestamps: number[] = []; + private config: RateLimitConfig; + + constructor(config: Partial = {}) { + this.config = { + maxRequests: config.maxRequests ?? 10, + windowMs: config.windowMs ?? 60000, + }; + } + + tryAcquire(): boolean { + const now = Date.now(); + this.timestamps = this.timestamps.filter((t) => now - t < this.config.windowMs); + + if (this.timestamps.length < this.config.maxRequests) { + this.timestamps.push(now); + return true; + } + return false; + } + + remainingRequests(): number { + const now = Date.now(); + const active = this.timestamps.filter((t) => now - t < this.config.windowMs); + return this.config.maxRequests - active.length; + } + + async waitForSlot(): Promise { + while (!this.tryAcquire()) { + const oldest = this.timestamps[0]!; + const waitMs = this.config.windowMs - (Date.now() - oldest); + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + } + + reset(windowMs?: number) { + this.timestamps = []; + if (windowMs) { + this.config.windowMs = windowMs; + } + } +} + +export function parseRateLimit(header: string): RateLimitConfig | null { + const parts = header.split(","); + let maxRequests: number | null = null; + let windowMs: number | null = null; + + for (const part of parts) { + const eqIdx = part.indexOf("="); + if (eqIdx === -1) continue; + const key = part.substring(0, eqIdx).trim(); + const val = part.substring(eqIdx + 1); + switch (key) { + case "limit": { + const parsed = parseInt(val, 10); + if (!Number.isNaN(parsed)) maxRequests = parsed; + break; + } + case "window": { + const parsed = parseInt(val, 10); + if (!Number.isNaN(parsed)) windowMs = parsed * 1000; + break; + } + } + } + + if (maxRequests !== null && windowMs !== null) return { maxRequests, windowMs }; + return null; +}