@buenojs/bueno 0.8.0
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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Primitives
|
|
3
|
+
*
|
|
4
|
+
* Provides password hashing, JWT handling, CSRF protection,
|
|
5
|
+
* and authentication middleware using Bun's built-in capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Context } from "../context";
|
|
9
|
+
import type { Middleware } from "../middleware";
|
|
10
|
+
|
|
11
|
+
// ============= Password Utilities =============
|
|
12
|
+
|
|
13
|
+
export interface PasswordOptions {
|
|
14
|
+
algorithm?: "argon2id" | "bcrypt";
|
|
15
|
+
cost?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Password = {
|
|
19
|
+
/**
|
|
20
|
+
* Hash a password using Bun's built-in password hashing
|
|
21
|
+
*/
|
|
22
|
+
async hash(password: string, options?: PasswordOptions): Promise<string> {
|
|
23
|
+
const algorithm = options?.algorithm ?? "argon2id";
|
|
24
|
+
// Bun.password.hash returns a string hash
|
|
25
|
+
return Bun.password.hash(password, {
|
|
26
|
+
algorithm,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Verify a password against a hash
|
|
32
|
+
*/
|
|
33
|
+
async verify(password: string, hash: string): Promise<boolean> {
|
|
34
|
+
return Bun.password.verify(password, hash);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a hash needs rehashing (e.g., algorithm upgrade)
|
|
39
|
+
*/
|
|
40
|
+
needsRehash(hash: string, options?: PasswordOptions): boolean {
|
|
41
|
+
const algorithm = options?.algorithm ?? "argon2id";
|
|
42
|
+
// Check if needsRehash exists on Bun.password (it may not in all versions)
|
|
43
|
+
if (typeof (Bun.password as unknown as Record<string, unknown>).needsRehash === 'function') {
|
|
44
|
+
const needsRehashFn = (Bun.password as unknown as Record<string, unknown>).needsRehash as (hash: string, options: { algorithm: string }) => boolean;
|
|
45
|
+
return !needsRehashFn(hash, { algorithm });
|
|
46
|
+
}
|
|
47
|
+
// Fallback: check if hash starts with the expected algorithm prefix
|
|
48
|
+
if (algorithm === 'argon2id' && !hash.startsWith('$argon2id')) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (algorithm === 'bcrypt' && !hash.startsWith('$2')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ============= JWT Utilities =============
|
|
59
|
+
|
|
60
|
+
export interface JWTOptions {
|
|
61
|
+
expiresIn?: number | string; // seconds or string like '1h', '7d'
|
|
62
|
+
issuer?: string;
|
|
63
|
+
audience?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface JWTPayload {
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
iat?: number;
|
|
69
|
+
exp?: number;
|
|
70
|
+
iss?: string;
|
|
71
|
+
aud?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class JWT {
|
|
75
|
+
private secret: string;
|
|
76
|
+
private options: JWTOptions;
|
|
77
|
+
|
|
78
|
+
constructor(secret: string, options: JWTOptions = {}) {
|
|
79
|
+
this.secret = secret;
|
|
80
|
+
this.options = options;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sign a payload and create a JWT
|
|
85
|
+
*/
|
|
86
|
+
async sign(payload: JWTPayload): Promise<string> {
|
|
87
|
+
const now = Math.floor(Date.now() / 1000);
|
|
88
|
+
|
|
89
|
+
const fullPayload: JWTPayload = {
|
|
90
|
+
...payload,
|
|
91
|
+
iat: now,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Add expiry
|
|
95
|
+
if (this.options.expiresIn) {
|
|
96
|
+
if (typeof this.options.expiresIn === "number") {
|
|
97
|
+
fullPayload.exp = now + this.options.expiresIn;
|
|
98
|
+
} else {
|
|
99
|
+
// Parse string like '1h', '7d'
|
|
100
|
+
fullPayload.exp = now + this.parseExpiry(this.options.expiresIn);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add issuer
|
|
105
|
+
if (this.options.issuer) {
|
|
106
|
+
fullPayload.iss = this.options.issuer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add audience
|
|
110
|
+
if (this.options.audience) {
|
|
111
|
+
fullPayload.aud = this.options.audience;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create JWT using Bun's JWT support or fallback to manual creation
|
|
115
|
+
return this.createToken(fullPayload);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Verify and decode a JWT
|
|
120
|
+
*/
|
|
121
|
+
async verify(token: string): Promise<JWTPayload | null> {
|
|
122
|
+
try {
|
|
123
|
+
const payload = await this.decodeToken(token);
|
|
124
|
+
|
|
125
|
+
if (!payload) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check expiry
|
|
130
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return payload;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decode token without verification (for debugging)
|
|
142
|
+
*/
|
|
143
|
+
decode(token: string): JWTPayload | null {
|
|
144
|
+
try {
|
|
145
|
+
return this.decodeTokenUnsafe(token);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse expiry string to seconds
|
|
153
|
+
*/
|
|
154
|
+
private parseExpiry(expiresIn: string): number {
|
|
155
|
+
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
|
156
|
+
if (!match) {
|
|
157
|
+
throw new Error(`Invalid expiresIn format: ${expiresIn}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const value = Number.parseInt(match[1], 10);
|
|
161
|
+
const unit = match[2];
|
|
162
|
+
|
|
163
|
+
switch (unit) {
|
|
164
|
+
case "s":
|
|
165
|
+
return value;
|
|
166
|
+
case "m":
|
|
167
|
+
return value * 60;
|
|
168
|
+
case "h":
|
|
169
|
+
return value * 60 * 60;
|
|
170
|
+
case "d":
|
|
171
|
+
return value * 60 * 60 * 24;
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown time unit: ${unit}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create JWT token manually
|
|
179
|
+
*/
|
|
180
|
+
private async createToken(payload: JWTPayload): Promise<string> {
|
|
181
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
182
|
+
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
const headerB64 = this.base64UrlEncode(
|
|
185
|
+
encoder.encode(JSON.stringify(header)),
|
|
186
|
+
);
|
|
187
|
+
const payloadB64 = this.base64UrlEncode(
|
|
188
|
+
encoder.encode(JSON.stringify(payload)),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
192
|
+
const signature = await this.signData(data);
|
|
193
|
+
|
|
194
|
+
return `${data}.${signature}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Decode and verify token
|
|
199
|
+
*/
|
|
200
|
+
private async decodeToken(token: string): Promise<JWTPayload | null> {
|
|
201
|
+
const parts = token.split(".");
|
|
202
|
+
if (parts.length !== 3) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
207
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
208
|
+
|
|
209
|
+
// Verify signature
|
|
210
|
+
const expectedSignature = await this.signData(data);
|
|
211
|
+
if (signature !== expectedSignature) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Decode payload
|
|
216
|
+
return this.base64UrlDecode<JWTPayload>(payloadB64);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Decode token without verification
|
|
221
|
+
*/
|
|
222
|
+
private decodeTokenUnsafe(token: string): JWTPayload | null {
|
|
223
|
+
const parts = token.split(".");
|
|
224
|
+
if (parts.length !== 3) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return this.base64UrlDecode<JWTPayload>(parts[1]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sign data using HMAC-SHA256
|
|
232
|
+
*/
|
|
233
|
+
private async signData(data: string): Promise<string> {
|
|
234
|
+
const encoder = new TextEncoder();
|
|
235
|
+
const key = await crypto.subtle.importKey(
|
|
236
|
+
"raw",
|
|
237
|
+
encoder.encode(this.secret),
|
|
238
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
239
|
+
false,
|
|
240
|
+
["sign"],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const signature = await crypto.subtle.sign(
|
|
244
|
+
"HMAC",
|
|
245
|
+
key,
|
|
246
|
+
encoder.encode(data),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return this.base64UrlEncode(new Uint8Array(signature));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Base64URL encode
|
|
254
|
+
*/
|
|
255
|
+
private base64UrlEncode(data: Uint8Array): string {
|
|
256
|
+
const base64 = btoa(String.fromCharCode(...data));
|
|
257
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Base64URL decode
|
|
262
|
+
*/
|
|
263
|
+
private base64UrlDecode<T>(data: string): T {
|
|
264
|
+
const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
265
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
|
|
266
|
+
const decoded = atob(padded);
|
|
267
|
+
return JSON.parse(decoded);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ============= CSRF Protection =============
|
|
272
|
+
|
|
273
|
+
export const CSRF = {
|
|
274
|
+
/**
|
|
275
|
+
* Generate a CSRF token
|
|
276
|
+
*/
|
|
277
|
+
generate(): string {
|
|
278
|
+
return crypto.randomUUID();
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get the secret for a token (for cookie-based validation)
|
|
283
|
+
*/
|
|
284
|
+
getSecret(): string {
|
|
285
|
+
return crypto.randomUUID();
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Verify a CSRF token against a secret
|
|
290
|
+
*/
|
|
291
|
+
verify(token: string, secret: string): boolean {
|
|
292
|
+
// Simple comparison - in production, use constant-time comparison
|
|
293
|
+
return token === secret;
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Create CSRF middleware
|
|
298
|
+
*/
|
|
299
|
+
middleware(): Middleware {
|
|
300
|
+
return async (context: Context, next: () => Promise<Response>) => {
|
|
301
|
+
// Skip for safe methods
|
|
302
|
+
if (["GET", "HEAD", "OPTIONS"].includes(context.method)) {
|
|
303
|
+
return next();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const token = context.getHeader("x-csrf-token");
|
|
307
|
+
const cookieToken = context.getCookie("csrf-token");
|
|
308
|
+
|
|
309
|
+
if (!token || !cookieToken || token !== cookieToken) {
|
|
310
|
+
return context.status(403).json({ error: "Invalid CSRF token" });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return next();
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ============= Authentication Middleware =============
|
|
319
|
+
|
|
320
|
+
export interface AuthMiddlewareOptions {
|
|
321
|
+
jwt: JWT;
|
|
322
|
+
header?: string;
|
|
323
|
+
prefix?: string;
|
|
324
|
+
skipPaths?: string[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create authentication middleware
|
|
329
|
+
*/
|
|
330
|
+
export function createAuthMiddleware(
|
|
331
|
+
options: AuthMiddlewareOptions,
|
|
332
|
+
): Middleware {
|
|
333
|
+
const {
|
|
334
|
+
jwt,
|
|
335
|
+
header = "authorization",
|
|
336
|
+
prefix = "Bearer ",
|
|
337
|
+
skipPaths = [],
|
|
338
|
+
} = options;
|
|
339
|
+
|
|
340
|
+
return async (context: Context, next: () => Promise<Response>) => {
|
|
341
|
+
// Skip certain paths
|
|
342
|
+
if (skipPaths.some((path) => context.path.startsWith(path))) {
|
|
343
|
+
return next();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const authHeader = context.getHeader(header);
|
|
347
|
+
|
|
348
|
+
if (!authHeader) {
|
|
349
|
+
return context
|
|
350
|
+
.status(401)
|
|
351
|
+
.json({ error: "Missing authorization header" });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!authHeader.startsWith(prefix)) {
|
|
355
|
+
return context
|
|
356
|
+
.status(401)
|
|
357
|
+
.json({ error: "Invalid authorization format" });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const token = authHeader.slice(prefix.length);
|
|
361
|
+
const payload = await jwt.verify(token);
|
|
362
|
+
|
|
363
|
+
if (!payload) {
|
|
364
|
+
return context.status(401).json({ error: "Invalid or expired token" });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Store user in context
|
|
368
|
+
context.set("user", payload);
|
|
369
|
+
|
|
370
|
+
return next();
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============= Role-Based Access Control =============
|
|
375
|
+
|
|
376
|
+
export interface RBACOptions {
|
|
377
|
+
getUserRoles: (userId: string | number) => Promise<string[]>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Create RBAC middleware
|
|
382
|
+
*/
|
|
383
|
+
export function createRBACMiddleware(
|
|
384
|
+
options: RBACOptions,
|
|
385
|
+
): (roles: string[]) => Middleware {
|
|
386
|
+
const { getUserRoles } = options;
|
|
387
|
+
|
|
388
|
+
return (allowedRoles: string[]): Middleware => {
|
|
389
|
+
return async (context: Context, next: () => Promise<Response>) => {
|
|
390
|
+
const user = context.get("user") as { userId?: string | number } | undefined;
|
|
391
|
+
|
|
392
|
+
if (!user?.userId) {
|
|
393
|
+
return context.status(401).json({ error: "Unauthorized" });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const userRoles = await getUserRoles(user.userId);
|
|
397
|
+
const hasRole = allowedRoles.some((role) => userRoles.includes(role));
|
|
398
|
+
|
|
399
|
+
if (!hasRole) {
|
|
400
|
+
return context.status(403).json({ error: "Forbidden" });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return next();
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ============= API Key Authentication =============
|
|
409
|
+
|
|
410
|
+
export interface APIKeyOptions {
|
|
411
|
+
validateKey: (apiKey: string) => Promise<boolean>;
|
|
412
|
+
header?: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create API key authentication middleware
|
|
417
|
+
*/
|
|
418
|
+
export function createAPIKeyMiddleware(options: APIKeyOptions): Middleware {
|
|
419
|
+
const { validateKey, header = "x-api-key" } = options;
|
|
420
|
+
|
|
421
|
+
return async (context: Context, next: () => Promise<Response>) => {
|
|
422
|
+
const apiKey = context.getHeader(header);
|
|
423
|
+
|
|
424
|
+
if (!apiKey) {
|
|
425
|
+
return context.status(401).json({ error: "Missing API key" });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const isValid = await validateKey(apiKey);
|
|
429
|
+
|
|
430
|
+
if (!isValid) {
|
|
431
|
+
return context.status(401).json({ error: "Invalid API key" });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return next();
|
|
435
|
+
};
|
|
436
|
+
}
|