@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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. 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
+ }