@rebasepro/server-core 0.2.3 → 0.2.5
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.
- package/dist/common/src/collections/default-collections.d.ts +9 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2309 -370
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2298 -355
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +5 -60
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +36 -100
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +35 -73
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +56 -8
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +62 -164
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -29
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/types/src/users/roles.d.ts +0 -22
- package/src/bootstrappers/index.ts +0 -1
- package/src/singleton.test.ts +0 -28
package/src/auth/jwt.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface AccessTokenPayload {
|
|
|
11
11
|
userId: string;
|
|
12
12
|
roles: string[];
|
|
13
13
|
uid?: string;
|
|
14
|
+
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
15
|
+
aal?: "aal1" | "aal2";
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
let jwtConfig: JwtConfig = {
|
|
@@ -70,13 +72,22 @@ export function configureJwt(config: JwtConfig): void {
|
|
|
70
72
|
/**
|
|
71
73
|
* Generate an access token (short-lived, 1 hour by default)
|
|
72
74
|
*/
|
|
73
|
-
export function generateAccessToken(
|
|
75
|
+
export function generateAccessToken(
|
|
76
|
+
userId: string,
|
|
77
|
+
roles: string[],
|
|
78
|
+
aal: "aal1" | "aal2" = "aal1",
|
|
79
|
+
customClaims?: Record<string, unknown>
|
|
80
|
+
): string {
|
|
74
81
|
if (!jwtConfig.secret) {
|
|
75
82
|
throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
const payload:
|
|
79
|
-
|
|
85
|
+
const payload: Record<string, unknown> = {
|
|
86
|
+
userId,
|
|
87
|
+
roles,
|
|
88
|
+
aal,
|
|
89
|
+
...customClaims
|
|
90
|
+
};
|
|
80
91
|
|
|
81
92
|
return jwt.sign(payload, jwtConfig.secret, {
|
|
82
93
|
expiresIn: jwtConfig.accessExpiresIn as jwt.SignOptions["expiresIn"],
|
|
@@ -124,16 +135,19 @@ export function verifyAccessToken(token: string): AccessTokenPayload | null {
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
try {
|
|
127
|
-
const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[] };
|
|
138
|
+
const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[]; aal?: string };
|
|
128
139
|
const id = decoded.userId || decoded.uid || decoded.sub;
|
|
129
140
|
if (!id) {
|
|
130
141
|
console.error("[JWT] Verification failed: missing id in payload", decoded);
|
|
131
142
|
return null;
|
|
132
143
|
}
|
|
133
144
|
|
|
145
|
+
const aal = (decoded.aal === "aal1" || decoded.aal === "aal2") ? decoded.aal : "aal1";
|
|
146
|
+
|
|
134
147
|
return {
|
|
135
148
|
userId: id,
|
|
136
|
-
roles: decoded.roles || []
|
|
149
|
+
roles: decoded.roles || [],
|
|
150
|
+
aal
|
|
137
151
|
};
|
|
138
152
|
} catch (error) {
|
|
139
153
|
console.error("[JWT] Verification failed:", error, "Token start:", token.substring(0, 15));
|
package/src/auth/mfa.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP (Time-based One-Time Password) implementation.
|
|
3
|
+
*
|
|
4
|
+
* Pure Node.js crypto implementation — no external dependencies required.
|
|
5
|
+
* Implements RFC 6238 (TOTP) and RFC 4226 (HOTP).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHmac, randomBytes, createHash } from "crypto";
|
|
9
|
+
|
|
10
|
+
// ─── Base32 Encoding/Decoding ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Encode a Buffer to Base32 string (RFC 4648)
|
|
16
|
+
*/
|
|
17
|
+
export function base32Encode(buffer: Buffer): string {
|
|
18
|
+
let bits = 0;
|
|
19
|
+
let value = 0;
|
|
20
|
+
let output = "";
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
23
|
+
value = (value << 8) | buffer[i];
|
|
24
|
+
bits += 8;
|
|
25
|
+
|
|
26
|
+
while (bits >= 5) {
|
|
27
|
+
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
28
|
+
bits -= 5;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (bits > 0) {
|
|
33
|
+
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decode a Base32 string to Buffer
|
|
41
|
+
*/
|
|
42
|
+
export function base32Decode(encoded: string): Buffer {
|
|
43
|
+
const cleanInput = encoded.replace(/=+$/, "").toUpperCase();
|
|
44
|
+
const bytes: number[] = [];
|
|
45
|
+
let bits = 0;
|
|
46
|
+
let value = 0;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < cleanInput.length; i++) {
|
|
49
|
+
const index = BASE32_ALPHABET.indexOf(cleanInput[i]);
|
|
50
|
+
if (index === -1) continue;
|
|
51
|
+
|
|
52
|
+
value = (value << 5) | index;
|
|
53
|
+
bits += 5;
|
|
54
|
+
|
|
55
|
+
if (bits >= 8) {
|
|
56
|
+
bytes.push((value >>> (bits - 8)) & 255);
|
|
57
|
+
bits -= 8;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Buffer.from(bytes);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── HOTP/TOTP ───────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate an HOTP value per RFC 4226
|
|
68
|
+
*/
|
|
69
|
+
function generateHotp(secret: Buffer, counter: bigint): string {
|
|
70
|
+
const hmac = createHmac("sha1", secret);
|
|
71
|
+
const counterBuffer = Buffer.alloc(8);
|
|
72
|
+
counterBuffer.writeBigInt64BE(counter);
|
|
73
|
+
hmac.update(counterBuffer);
|
|
74
|
+
const hash = hmac.digest();
|
|
75
|
+
|
|
76
|
+
const offset = hash[hash.length - 1] & 0x0f;
|
|
77
|
+
const code =
|
|
78
|
+
(((hash[offset] & 0x7f) << 24) |
|
|
79
|
+
(hash[offset + 1] << 16) |
|
|
80
|
+
(hash[offset + 2] << 8) |
|
|
81
|
+
hash[offset + 3]) %
|
|
82
|
+
1000000;
|
|
83
|
+
|
|
84
|
+
return code.toString().padStart(6, "0");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate a TOTP value for the current time step
|
|
89
|
+
*/
|
|
90
|
+
export function generateTotp(secret: Buffer, timeStep = 30): string {
|
|
91
|
+
const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
|
|
92
|
+
return generateHotp(secret, counter);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verify a TOTP token with a configurable time window
|
|
97
|
+
*
|
|
98
|
+
* @param secret - The shared secret as a Buffer
|
|
99
|
+
* @param token - The 6-digit TOTP code to verify
|
|
100
|
+
* @param window - Number of time steps to check on each side (default: 1)
|
|
101
|
+
* @returns true if the token is valid within the window
|
|
102
|
+
*/
|
|
103
|
+
export function verifyTotp(secret: Buffer, token: string, window = 1): boolean {
|
|
104
|
+
const timeStep = 30;
|
|
105
|
+
const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
|
|
106
|
+
for (let i = -window; i <= window; i++) {
|
|
107
|
+
if (generateHotp(secret, counter + BigInt(i)) === token) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Secret Generation ───────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate a new TOTP secret and return the setup information
|
|
118
|
+
*
|
|
119
|
+
* @param issuer - The issuer name (app name) for the QR code
|
|
120
|
+
* @param accountName - The account identifier (usually email)
|
|
121
|
+
* @returns Object with base32 secret and otpauth URI
|
|
122
|
+
*/
|
|
123
|
+
export function generateTotpSecret(issuer: string, accountName: string): {
|
|
124
|
+
secret: string;
|
|
125
|
+
uri: string;
|
|
126
|
+
} {
|
|
127
|
+
// Generate a 20-byte random secret (160 bits, recommended by RFC 4226)
|
|
128
|
+
const secretBuffer = randomBytes(20);
|
|
129
|
+
const secret = base32Encode(secretBuffer);
|
|
130
|
+
|
|
131
|
+
// Build otpauth:// URI for QR code generation
|
|
132
|
+
const encodedIssuer = encodeURIComponent(issuer);
|
|
133
|
+
const encodedAccount = encodeURIComponent(accountName);
|
|
134
|
+
const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
135
|
+
|
|
136
|
+
return { secret, uri };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Recovery Codes ──────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate a set of one-time recovery codes
|
|
143
|
+
*
|
|
144
|
+
* @param count - Number of recovery codes to generate (default: 10)
|
|
145
|
+
* @returns Array of formatted recovery code strings (e.g. "A1B2C-D3E4F")
|
|
146
|
+
*/
|
|
147
|
+
export function generateRecoveryCodes(count = 10): string[] {
|
|
148
|
+
return Array.from({ length: count }, () => {
|
|
149
|
+
const raw = randomBytes(5).toString("hex").toUpperCase();
|
|
150
|
+
const parts = raw.match(/.{1,5}/g);
|
|
151
|
+
return parts ? parts.join("-") : raw;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Hash a recovery code for storage
|
|
157
|
+
*/
|
|
158
|
+
export function hashRecoveryCode(code: string): string {
|
|
159
|
+
return createHash("sha256").update(code.replace(/-/g, "").toUpperCase()).digest("hex");
|
|
160
|
+
}
|
package/src/auth/middleware.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { verifyAccessToken, AccessTokenPayload } from "./jwt";
|
|
|
4
4
|
import { HonoEnv } from "../api/types";
|
|
5
5
|
import { scopeDataDriver } from "./rls-scope";
|
|
6
6
|
import { safeCompare } from "./crypto-utils";
|
|
7
|
+
import { isApiKeyToken, validateApiKey } from "./api-keys/api-key-middleware";
|
|
8
|
+
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Result from a custom auth validator.
|
|
@@ -44,6 +46,12 @@ export interface AuthMiddlewareOptions {
|
|
|
44
46
|
* timing attacks. The key must be at least 32 characters.
|
|
45
47
|
*/
|
|
46
48
|
serviceKey?: string;
|
|
49
|
+
/**
|
|
50
|
+
* API key store for authenticating `rk_` prefixed tokens.
|
|
51
|
+
* When set, tokens starting with `rk_` are validated against the
|
|
52
|
+
* database instead of being treated as JWTs.
|
|
53
|
+
*/
|
|
54
|
+
apiKeyStore?: ApiKeyStore;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -223,7 +231,7 @@ export function extractUserFromToken(token: string): AccessTokenPayload | null {
|
|
|
223
231
|
*/
|
|
224
232
|
|
|
225
233
|
export function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
226
|
-
const { driver, requireAuth: enforceAuth = true, validator, serviceKey } = options;
|
|
234
|
+
const { driver, requireAuth: enforceAuth = true, validator, serviceKey, apiKeyStore } = options;
|
|
227
235
|
|
|
228
236
|
return async (c, next) => {
|
|
229
237
|
if (validator) {
|
|
@@ -291,6 +299,17 @@ code: "UNAUTHORIZED" } }, 401);
|
|
|
291
299
|
return c.json({ error: { message: "Internal authentication error",
|
|
292
300
|
code: "INTERNAL_ERROR" } }, 500);
|
|
293
301
|
}
|
|
302
|
+
} else if (apiKeyStore && isApiKeyToken(token)) {
|
|
303
|
+
// ── API Key verification ──────────────────────────────
|
|
304
|
+
// Tokens starting with `rk_` are validated against the
|
|
305
|
+
// api_keys table instead of JWT verification.
|
|
306
|
+
const result = await validateApiKey(c, token, {
|
|
307
|
+
store: apiKeyStore,
|
|
308
|
+
driver,
|
|
309
|
+
});
|
|
310
|
+
if (result !== true) {
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
294
313
|
} else {
|
|
295
314
|
// ── JWT verification ───────────────────────────────────
|
|
296
315
|
const payload = extractUserFromToken(token);
|
package/src/auth/rate-limiter.ts
CHANGED
|
@@ -131,3 +131,95 @@ export const strictAuthLimiter = createRateLimiter({
|
|
|
131
131
|
limit: 50,
|
|
132
132
|
message: "Too many requests to this sensitive endpoint, please try again later."
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Key generator for API-key-based rate limiting.
|
|
137
|
+
*
|
|
138
|
+
* Uses the API key ID (from `c.get("apiKey")`) as the rate limit key.
|
|
139
|
+
* Falls back to IP-based keying when the request is not authenticated
|
|
140
|
+
* via an API key.
|
|
141
|
+
*/
|
|
142
|
+
export function apiKeyKeyGenerator(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): string {
|
|
143
|
+
const apiKey = c.get("apiKey") as { id: string } | undefined;
|
|
144
|
+
if (apiKey) {
|
|
145
|
+
return `api-key:${apiKey.id}`;
|
|
146
|
+
}
|
|
147
|
+
return defaultKeyGenerator(c);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a rate limiter specifically for API key requests.
|
|
152
|
+
*
|
|
153
|
+
* When a request is authenticated via an API key that has a `rate_limit`
|
|
154
|
+
* configured, this limiter enforces per-key limits using the key's ID
|
|
155
|
+
* as the rate limit bucket.
|
|
156
|
+
*
|
|
157
|
+
* @param defaultLimit - Fallback limit when the key has no `rate_limit` set.
|
|
158
|
+
* @param windowMs - Time window in milliseconds (default: 15 minutes).
|
|
159
|
+
*/
|
|
160
|
+
export function createApiKeyRateLimiter(
|
|
161
|
+
defaultLimit = 1000,
|
|
162
|
+
windowMs = 15 * 60 * 1000,
|
|
163
|
+
): MiddlewareHandler<HonoEnv> {
|
|
164
|
+
// We maintain a single shared store keyed by API key ID.
|
|
165
|
+
// The actual limit is resolved per-request from the key's metadata.
|
|
166
|
+
const store = new Map<string, { timestamps: number[] }>();
|
|
167
|
+
|
|
168
|
+
const cleanupInterval = setInterval(() => {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
for (const [key, entry] of store.entries()) {
|
|
171
|
+
entry.timestamps = entry.timestamps.filter(t => now - t < windowMs);
|
|
172
|
+
if (entry.timestamps.length === 0) {
|
|
173
|
+
store.delete(key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, windowMs);
|
|
177
|
+
|
|
178
|
+
if (cleanupInterval.unref) {
|
|
179
|
+
cleanupInterval.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return async (c, next) => {
|
|
183
|
+
const apiKey = c.get("apiKey") as { id: string; rate_limit: number | null } | undefined;
|
|
184
|
+
if (!apiKey) {
|
|
185
|
+
// Not an API key request — skip this limiter
|
|
186
|
+
return next();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const limit = apiKey.rate_limit ?? defaultLimit;
|
|
190
|
+
const key = `api-key:${apiKey.id}`;
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
|
|
193
|
+
let entry = store.get(key);
|
|
194
|
+
if (!entry) {
|
|
195
|
+
entry = { timestamps: [] };
|
|
196
|
+
store.set(key, entry);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
entry.timestamps = entry.timestamps.filter(t => now - t < windowMs);
|
|
200
|
+
|
|
201
|
+
if (entry.timestamps.length >= limit) {
|
|
202
|
+
const retryAfterMs = entry.timestamps[0] + windowMs - now;
|
|
203
|
+
const retryAfterSec = Math.ceil(retryAfterMs / 1000);
|
|
204
|
+
|
|
205
|
+
c.header("Retry-After", String(retryAfterSec));
|
|
206
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
207
|
+
c.header("X-RateLimit-Remaining", "0");
|
|
208
|
+
c.header("X-RateLimit-Reset", String(Math.ceil((now + retryAfterMs) / 1000)));
|
|
209
|
+
|
|
210
|
+
return c.json({
|
|
211
|
+
error: {
|
|
212
|
+
message: "API key rate limit exceeded, please try again later.",
|
|
213
|
+
code: "RATE_LIMITED"
|
|
214
|
+
}
|
|
215
|
+
}, 429);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
entry.timestamps.push(now);
|
|
219
|
+
|
|
220
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
221
|
+
c.header("X-RateLimit-Remaining", String(limit - entry.timestamps.length));
|
|
222
|
+
|
|
223
|
+
return next();
|
|
224
|
+
};
|
|
225
|
+
}
|