@iskra-bun/web-kit 0.1.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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +31 -0
  3. package/dist/chunk-POXNRNTC.js +51 -0
  4. package/dist/chunk-POXNRNTC.js.map +1 -0
  5. package/dist/index.d.ts +966 -0
  6. package/dist/index.js +2824 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/mailgun-Z46GZJNI.js +83 -0
  9. package/dist/mailgun-Z46GZJNI.js.map +1 -0
  10. package/dist/s3-7IG4ESFW.js +171 -0
  11. package/dist/s3-7IG4ESFW.js.map +1 -0
  12. package/dist/sendgrid-UK2GSBEF.js +43 -0
  13. package/dist/sendgrid-UK2GSBEF.js.map +1 -0
  14. package/dist/smtp-WJDLYKD5.js +50 -0
  15. package/dist/smtp-WJDLYKD5.js.map +1 -0
  16. package/package.json +74 -0
  17. package/src/driver.ts +55 -0
  18. package/src/errors.ts +66 -0
  19. package/src/features/api-key.ts +243 -0
  20. package/src/features/auth/better-auth-config.ts +160 -0
  21. package/src/features/auth/index.ts +229 -0
  22. package/src/features/auth/schema.ts +174 -0
  23. package/src/features/auth/types.ts +114 -0
  24. package/src/features/cache.ts +144 -0
  25. package/src/features/cors.ts +33 -0
  26. package/src/features/csrf.ts +94 -0
  27. package/src/features/db.ts +90 -0
  28. package/src/features/email/index.ts +103 -0
  29. package/src/features/email/providers/mailgun.ts +99 -0
  30. package/src/features/email/providers/sendgrid.ts +42 -0
  31. package/src/features/email/providers/smtp.ts +51 -0
  32. package/src/features/error-handler.ts +147 -0
  33. package/src/features/health.ts +94 -0
  34. package/src/features/json-schema-validation.ts +186 -0
  35. package/src/features/logger.ts +70 -0
  36. package/src/features/openapi.ts +107 -0
  37. package/src/features/permissions.ts +128 -0
  38. package/src/features/rate-limit.ts +173 -0
  39. package/src/features/request-id.ts +45 -0
  40. package/src/features/session.ts +322 -0
  41. package/src/features/storage/adapters/local.ts +133 -0
  42. package/src/features/storage/adapters/s3.ts +193 -0
  43. package/src/features/storage/base.ts +112 -0
  44. package/src/features/storage/index.ts +53 -0
  45. package/src/features/tracing.ts +49 -0
  46. package/src/features/upload/helper.ts +85 -0
  47. package/src/features/upload/index.ts +140 -0
  48. package/src/features/validation.ts +105 -0
  49. package/src/index.ts +29 -0
  50. package/src/kernel.ts +257 -0
  51. package/src/responses.ts +37 -0
  52. package/src/router.ts +31 -0
  53. package/src/server.ts +135 -0
  54. package/src/types.ts +272 -0
package/dist/index.js ADDED
@@ -0,0 +1,2824 @@
1
+ import {
2
+ BaseStorageAdapter
3
+ } from "./chunk-POXNRNTC.js";
4
+
5
+ // src/kernel.ts
6
+ import { Hono } from "hono";
7
+ import { HTTPException } from "hono/http-exception";
8
+ var Kernel = class {
9
+ app;
10
+ config;
11
+ features = /* @__PURE__ */ new Map();
12
+ initialized = false;
13
+ constructor(config = {}) {
14
+ this.config = {
15
+ port: 8e3,
16
+ hostname: "localhost",
17
+ ...config
18
+ };
19
+ this.app = new Hono();
20
+ this.app.onError((err, c) => {
21
+ if (err instanceof HTTPException) {
22
+ return c.json(
23
+ { message: err.message },
24
+ err.status
25
+ );
26
+ }
27
+ console.error("Unhandled error:", err);
28
+ return c.json(
29
+ { message: "Internal Server Error" },
30
+ 500
31
+ );
32
+ });
33
+ }
34
+ async initialize() {
35
+ if (this.initialized) {
36
+ throw new Error("Kernel already initialized");
37
+ }
38
+ console.log("\u{1F680} Initializing Web-Kit Kernel...");
39
+ this.validateFeatureDependencies();
40
+ await this.validatePeerDependencies();
41
+ this.applySecurityHeaders();
42
+ const orderedFeatures = this.sortFeaturesByDependencies();
43
+ for (const feature of orderedFeatures) {
44
+ await this.initializeFeature(feature);
45
+ }
46
+ this.initialized = true;
47
+ console.log("\u2705 Web-Kit Kernel initialized");
48
+ }
49
+ applySecurityHeaders() {
50
+ if (!this.config.securityHeaders) {
51
+ this.config.securityHeaders = {
52
+ xFrameOptions: "SAMEORIGIN",
53
+ xContentTypeOptions: true,
54
+ xXssProtection: true,
55
+ referrerPolicy: "strict-origin-when-cross-origin"
56
+ };
57
+ }
58
+ const headers = this.config.securityHeaders;
59
+ this.app.use("*", async (c, next) => {
60
+ await next();
61
+ if (headers.xFrameOptions) {
62
+ c.res.headers.set("X-Frame-Options", headers.xFrameOptions);
63
+ }
64
+ if (headers.xContentTypeOptions) {
65
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
66
+ }
67
+ if (headers.xXssProtection) {
68
+ c.res.headers.set("X-XSS-Protection", "1; mode=block");
69
+ }
70
+ if (headers.referrerPolicy) {
71
+ c.res.headers.set("Referrer-Policy", headers.referrerPolicy);
72
+ }
73
+ if (headers.strictTransportSecurity) {
74
+ const hsts = headers.strictTransportSecurity;
75
+ let hstsValue = `max-age=${hsts.maxAge || 31536e3}`;
76
+ if (hsts.includeSubDomains) hstsValue += "; includeSubDomains";
77
+ if (hsts.preload) hstsValue += "; preload";
78
+ c.res.headers.set("Strict-Transport-Security", hstsValue);
79
+ }
80
+ if (headers.contentSecurityPolicy) {
81
+ if (typeof headers.contentSecurityPolicy === "string") {
82
+ c.res.headers.set(
83
+ "Content-Security-Policy",
84
+ headers.contentSecurityPolicy
85
+ );
86
+ } else if (headers.contentSecurityPolicy.directives) {
87
+ const directives = Object.entries(
88
+ headers.contentSecurityPolicy.directives
89
+ ).map(([key, value]) => {
90
+ const values = Array.isArray(value) ? value.join(" ") : value;
91
+ return `${key} ${values}`;
92
+ }).join("; ");
93
+ c.res.headers.set("Content-Security-Policy", directives);
94
+ }
95
+ }
96
+ if (headers.permissionsPolicy) {
97
+ const policy = Object.entries(headers.permissionsPolicy).map(([key, value]) => `${key}=(${value.join(" ")})`).join(", ");
98
+ c.res.headers.set("Permissions-Policy", policy);
99
+ }
100
+ });
101
+ }
102
+ registerFeature(feature) {
103
+ if (this.initialized) {
104
+ throw new Error("Cannot register features after initialization");
105
+ }
106
+ if (feature.peerDependencies) {
107
+ for (const dep of feature.peerDependencies) {
108
+ try {
109
+ import(dep);
110
+ } catch {
111
+ console.warn(
112
+ `\u26A0\uFE0F Warning: Feature '${feature.name}' requires peer dependency: ${dep}`
113
+ );
114
+ }
115
+ }
116
+ }
117
+ this.features.set(feature.name, feature);
118
+ console.log(`\u{1F4E6} Registered feature: ${feature.name}`);
119
+ }
120
+ validateFeatureDependencies() {
121
+ for (const [name, feature] of this.features) {
122
+ if (feature.dependencies) {
123
+ for (const dep of feature.dependencies) {
124
+ if (!this.features.has(dep)) {
125
+ throw new Error(
126
+ `Feature '${name}' requires feature '${dep}' which is not registered`
127
+ );
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ async validatePeerDependencies() {
134
+ for (const [featureName, feature] of this.features) {
135
+ if (feature.peerDependencies) {
136
+ for (const dep of feature.peerDependencies) {
137
+ }
138
+ }
139
+ }
140
+ }
141
+ sortFeaturesByDependencies() {
142
+ const sorted = [];
143
+ const visited = /* @__PURE__ */ new Set();
144
+ const visiting = /* @__PURE__ */ new Set();
145
+ const visit = (name) => {
146
+ if (visited.has(name)) return;
147
+ if (visiting.has(name)) {
148
+ throw new Error(`Circular dependency detected for feature: ${name}`);
149
+ }
150
+ visiting.add(name);
151
+ const feature = this.features.get(name);
152
+ if (feature.dependencies) {
153
+ for (const dep of feature.dependencies) {
154
+ visit(dep);
155
+ }
156
+ }
157
+ visiting.delete(name);
158
+ visited.add(name);
159
+ sorted.push(feature);
160
+ };
161
+ for (const name of this.features.keys()) {
162
+ visit(name);
163
+ }
164
+ return sorted;
165
+ }
166
+ async initializeFeature(feature) {
167
+ console.log(`\u2699\uFE0F Initializing feature: ${feature.name}`);
168
+ await feature.initialize(this);
169
+ if (feature.routes) {
170
+ feature.routes(this.app);
171
+ }
172
+ }
173
+ getApp() {
174
+ return this.app;
175
+ }
176
+ getFeature(name) {
177
+ return this.features.get(name);
178
+ }
179
+ server;
180
+ async start() {
181
+ if (!this.initialized) {
182
+ await this.initialize();
183
+ }
184
+ console.log(
185
+ `\u{1F310} Server running at http://${this.config.hostname}:${this.config.port}`
186
+ );
187
+ if (typeof Bun !== "undefined") {
188
+ this.server = Bun.serve({
189
+ port: this.config.port,
190
+ hostname: this.config.hostname,
191
+ fetch: this.app.fetch
192
+ });
193
+ } else {
194
+ console.warn("Not running in Bun, start() might strictly need an adapter.");
195
+ }
196
+ }
197
+ async shutdown() {
198
+ console.log("\u{1F6D1} Shutting down...");
199
+ if (this.server) {
200
+ this.server.stop();
201
+ this.server = null;
202
+ }
203
+ const features = Array.from(this.features.values()).reverse();
204
+ for (const feature of features) {
205
+ if (feature.shutdown) {
206
+ await feature.shutdown();
207
+ }
208
+ }
209
+ console.log("\u{1F44B} Server shut down gracefully");
210
+ }
211
+ };
212
+
213
+ // src/driver.ts
214
+ var WebPlugin = class {
215
+ name = "WebPlugin";
216
+ app = null;
217
+ kernel;
218
+ runningServer;
219
+ router;
220
+ constructor(config = {}) {
221
+ const { router, features, ...kernelConfig } = config;
222
+ this.kernel = new Kernel(kernelConfig);
223
+ this.router = router;
224
+ if (features) {
225
+ for (const feature of features) {
226
+ this.kernel.registerFeature(feature);
227
+ }
228
+ }
229
+ }
230
+ async init(app) {
231
+ this.app = app;
232
+ await this.kernel.initialize();
233
+ if (this.router) {
234
+ this.kernel.getApp().route("/", this.router);
235
+ }
236
+ }
237
+ getHonoApp() {
238
+ return this.kernel.getApp();
239
+ }
240
+ async start() {
241
+ this.app?.logger.info(`Starting WebPlugin...`);
242
+ await this.kernel.start();
243
+ this.runningServer = true;
244
+ }
245
+ async stop() {
246
+ await this.kernel.shutdown();
247
+ this.app?.logger.info("WebPlugin stopped");
248
+ }
249
+ };
250
+
251
+ // src/server.ts
252
+ import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
253
+ import { z } from "zod";
254
+ var WebDriver = class {
255
+ name = "WebDriver";
256
+ app = null;
257
+ server;
258
+ options;
259
+ runningServer;
260
+ constructor(options = {}) {
261
+ this.options = options;
262
+ this.server = new OpenAPIHono();
263
+ }
264
+ init(app) {
265
+ this.app = app;
266
+ this.setupRoutes();
267
+ this.setupOpenApi();
268
+ }
269
+ setupOpenApi() {
270
+ if (this.options.openApi) {
271
+ this.server.doc(this.options.openApi.path, {
272
+ openapi: "3.0.0",
273
+ info: {
274
+ version: this.options.openApi.version,
275
+ title: this.options.openApi.title
276
+ }
277
+ });
278
+ }
279
+ }
280
+ setupRoutes() {
281
+ if (!this.options.routes) return;
282
+ for (const route of this.options.routes) {
283
+ const routeConfig = {
284
+ method: route.method.toLowerCase(),
285
+ path: route.path,
286
+ tags: route.doc?.tags,
287
+ summary: route.doc?.summary,
288
+ description: route.doc?.description,
289
+ request: {},
290
+ responses: {
291
+ 200: {
292
+ description: "Successful response",
293
+ content: {
294
+ "application/json": {
295
+ schema: z.any()
296
+ // We don't enforce response schema yet
297
+ }
298
+ }
299
+ },
300
+ 500: {
301
+ description: "Internal Server Error"
302
+ }
303
+ }
304
+ };
305
+ if (route.schema?.body) {
306
+ routeConfig.request.body = {
307
+ content: {
308
+ "application/json": {
309
+ schema: route.schema.body
310
+ }
311
+ }
312
+ };
313
+ }
314
+ if (route.schema?.query) {
315
+ routeConfig.request.query = route.schema.query;
316
+ }
317
+ if (route.schema?.params) {
318
+ routeConfig.request.params = route.schema.params;
319
+ }
320
+ const openApiRoute = createRoute(routeConfig);
321
+ this.server.openapi(openApiRoute, async (c) => {
322
+ if (!this.app) throw new Error("App not initialized");
323
+ const webCtx = {
324
+ raw: c,
325
+ // Hono/zod-openapi puts validated data in c.req.valid('json') etc similar to validator middleware
326
+ body: route.schema?.body ? c.req.valid("json") : void 0,
327
+ query: route.schema?.query ? c.req.valid("query") : void 0,
328
+ params: c.req.param(),
329
+ app: this.app
330
+ };
331
+ try {
332
+ const result = await route.handler(webCtx);
333
+ if (result instanceof Response) return result;
334
+ return c.json(result);
335
+ } catch (err) {
336
+ this.app.logger.error(err);
337
+ return c.json({ error: err.message }, 500);
338
+ }
339
+ });
340
+ }
341
+ }
342
+ start() {
343
+ const port = this.options.port || 3e3;
344
+ this.app?.logger.info(`Starting WebServer on port ${port}...`);
345
+ this.runningServer = Bun.serve({
346
+ fetch: this.server.fetch,
347
+ port
348
+ });
349
+ }
350
+ stop() {
351
+ if (this.runningServer) {
352
+ this.runningServer.stop();
353
+ }
354
+ this.app?.logger.info("WebServer stopped");
355
+ }
356
+ };
357
+
358
+ // src/router.ts
359
+ var createRouter = (routes) => routes;
360
+
361
+ // src/features/cors.ts
362
+ import { cors } from "hono/cors";
363
+ var CorsFeature = class {
364
+ constructor(config = {}) {
365
+ this.config = config;
366
+ if (!this.config.origin) {
367
+ this.config.origin = "*";
368
+ }
369
+ if (this.config.credentials === void 0) {
370
+ this.config.credentials = false;
371
+ }
372
+ }
373
+ config;
374
+ name = "cors";
375
+ async initialize(kernel) {
376
+ const app = kernel.getApp();
377
+ const honoConfig = { ...this.config };
378
+ if (typeof this.config.origin === "function") {
379
+ honoConfig.origin = (origin) => {
380
+ const result = this.config.origin(origin);
381
+ return result ? origin : null;
382
+ };
383
+ }
384
+ app.use("*", cors(honoConfig));
385
+ console.log("\u2705 CORS feature initialized");
386
+ }
387
+ };
388
+
389
+ // src/features/error-handler.ts
390
+ import { HTTPException as HTTPException3 } from "hono/http-exception";
391
+ import { IskraError as IskraError2 } from "@iskra-bun/core";
392
+
393
+ // src/errors.ts
394
+ import { IskraError, ErrorCodes } from "@iskra-bun/core";
395
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
396
+ var HttpError = class extends IskraError {
397
+ status;
398
+ constructor(status, message, options) {
399
+ super(message, { code: options?.code ?? ErrorCodes.INTERNAL_ERROR, cause: options?.cause, context: options?.context });
400
+ this.name = "HttpError";
401
+ this.status = status;
402
+ }
403
+ toHTTPException() {
404
+ return new HTTPException2(this.status, { message: this.message, cause: this });
405
+ }
406
+ };
407
+ var ValidationError = class extends HttpError {
408
+ details;
409
+ constructor(message, details, options) {
410
+ super(400, message, { code: ErrorCodes.VALIDATION_ERROR, ...options });
411
+ this.name = "ValidationError";
412
+ this.details = details;
413
+ }
414
+ };
415
+ var AuthError = class extends HttpError {
416
+ constructor(message = "Unauthorized", options) {
417
+ super(401, message, { code: ErrorCodes.UNAUTHORIZED, ...options });
418
+ this.name = "AuthError";
419
+ }
420
+ };
421
+ var ForbiddenError = class extends HttpError {
422
+ constructor(message = "Forbidden", options) {
423
+ super(403, message, { code: ErrorCodes.FORBIDDEN, ...options });
424
+ this.name = "ForbiddenError";
425
+ }
426
+ };
427
+ var NotFoundError = class extends HttpError {
428
+ constructor(message = "Not Found", options) {
429
+ super(404, message, { code: ErrorCodes.NOT_FOUND, ...options });
430
+ this.name = "NotFoundError";
431
+ }
432
+ };
433
+ var ConflictError = class extends HttpError {
434
+ constructor(message = "Conflict", options) {
435
+ super(409, message, { code: ErrorCodes.CONFLICT, ...options });
436
+ this.name = "ConflictError";
437
+ }
438
+ };
439
+
440
+ // src/features/error-handler.ts
441
+ var ErrorHandlerFeature = class {
442
+ name = "error-handler";
443
+ config;
444
+ constructor(config = {}) {
445
+ this.config = {
446
+ includeStack: config.includeStack !== void 0 ? config.includeStack : process.env.NODE_ENV === "development",
447
+ customHandlers: config.customHandlers,
448
+ logger: config.logger
449
+ };
450
+ }
451
+ async initialize(kernel) {
452
+ const app = kernel.getApp();
453
+ app.onError((err, c) => {
454
+ return this.handleError(err, c);
455
+ });
456
+ console.log("\u2705 Error handler feature initialized");
457
+ }
458
+ handleError(err, c) {
459
+ if (this.config.logger) {
460
+ this.config.logger(err, c);
461
+ } else {
462
+ console.error("Error:", err);
463
+ }
464
+ if (err instanceof HttpError) {
465
+ const status2 = err.status;
466
+ if (this.config.customHandlers?.[status2]) {
467
+ return this.config.customHandlers[status2](err, c);
468
+ }
469
+ const response2 = {
470
+ error: err.message,
471
+ status: status2,
472
+ code: err.code
473
+ };
474
+ if (Object.keys(err.context).length > 0) {
475
+ response2.context = err.context;
476
+ }
477
+ if (this.config.includeStack && err.stack) {
478
+ response2.stack = err.stack;
479
+ }
480
+ const requestId2 = c.get("requestId");
481
+ if (requestId2) response2.requestId = requestId2;
482
+ return c.json(response2, status2);
483
+ }
484
+ if (err instanceof IskraError2) {
485
+ const status2 = 500;
486
+ if (this.config.customHandlers?.[status2]) {
487
+ return this.config.customHandlers[status2](err, c);
488
+ }
489
+ const response2 = {
490
+ error: this.config.includeStack ? err.message : "Internal Server Error",
491
+ status: status2,
492
+ code: err.code
493
+ };
494
+ if (this.config.includeStack && err.stack) {
495
+ response2.stack = err.stack;
496
+ }
497
+ const requestId2 = c.get("requestId");
498
+ if (requestId2) response2.requestId = requestId2;
499
+ return c.json(response2, status2);
500
+ }
501
+ if (err instanceof HTTPException3) {
502
+ const status2 = err.status;
503
+ if (this.config.customHandlers?.[status2]) {
504
+ return this.config.customHandlers[status2](err, c);
505
+ }
506
+ const response2 = {
507
+ error: err.message || this.getStatusText(status2),
508
+ status: status2
509
+ };
510
+ if (this.config.includeStack && err.stack) {
511
+ response2.stack = err.stack;
512
+ }
513
+ return c.json(response2, status2);
514
+ }
515
+ const status = 500;
516
+ if (this.config.customHandlers?.[status]) {
517
+ return this.config.customHandlers[status](err, c);
518
+ }
519
+ const response = {
520
+ error: this.config.includeStack ? err.message : "Internal Server Error",
521
+ status
522
+ };
523
+ if (this.config.includeStack && err.stack) {
524
+ response.stack = err.stack;
525
+ }
526
+ const requestId = c.get("requestId");
527
+ if (requestId) {
528
+ response.requestId = requestId;
529
+ }
530
+ return c.json(response, status);
531
+ }
532
+ getStatusText(status) {
533
+ const statusTexts = {
534
+ 400: "Bad Request",
535
+ 401: "Unauthorized",
536
+ 403: "Forbidden",
537
+ 404: "Not Found",
538
+ 500: "Internal Server Error"
539
+ };
540
+ return statusTexts[status] || "Error";
541
+ }
542
+ };
543
+ function createHttpError(status, message) {
544
+ return new HTTPException3(status, { message });
545
+ }
546
+
547
+ // src/features/health.ts
548
+ var HealthCheckFeature = class {
549
+ name = "health";
550
+ kernel;
551
+ config;
552
+ constructor(config = {}) {
553
+ this.config = {
554
+ path: config.path || "/health",
555
+ readinessPath: config.readinessPath || "/health/ready",
556
+ livenessPath: config.livenessPath || "/health/live",
557
+ includeDetails: config.includeDetails !== void 0 ? config.includeDetails : true,
558
+ checks: config.checks
559
+ };
560
+ }
561
+ async initialize(kernel) {
562
+ this.kernel = kernel;
563
+ const app = kernel.getApp();
564
+ app.get(this.config.path, async (c) => await this.handleHealthCheck(c));
565
+ app.get(this.config.readinessPath, async (c) => await this.handleReadinessCheck(c));
566
+ app.get(this.config.livenessPath, async (c) => await this.handleLivenessCheck(c));
567
+ console.log("\u2705 Health check feature initialized");
568
+ }
569
+ async handleHealthCheck(c) {
570
+ const response = {
571
+ status: "ok",
572
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
573
+ };
574
+ if (this.config.includeDetails && this.kernel) {
575
+ response.features = Array.from(this.kernel.features.keys());
576
+ const featureHealth = {};
577
+ const dbFeature = this.kernel.getFeature("db");
578
+ if (dbFeature) await this.checkFeatureHealth(c, dbFeature, featureHealth, "db", "query", "SELECT 1");
579
+ const cacheFeature = this.kernel.getFeature("cache");
580
+ if (cacheFeature) await this.checkFeatureHealth(c, cacheFeature, featureHealth, "cache", "exists", "__health_check__");
581
+ if (Object.keys(featureHealth).length > 0) {
582
+ response.checks = featureHealth;
583
+ }
584
+ if (this.config.checks) {
585
+ const customChecks = {};
586
+ for (const [name, check] of Object.entries(this.config.checks)) {
587
+ try {
588
+ customChecks[name] = await check(c);
589
+ } catch (error) {
590
+ customChecks[name] = { status: "error", error: String(error) };
591
+ }
592
+ }
593
+ response.customChecks = customChecks;
594
+ }
595
+ }
596
+ return c.json(response);
597
+ }
598
+ async checkFeatureHealth(c, feature, report, key, method, ...args) {
599
+ try {
600
+ const instance = c.get(key);
601
+ if (instance && typeof instance[method] === "function") {
602
+ await instance[method](...args);
603
+ report[key] = { status: "ok" };
604
+ }
605
+ } catch (e) {
606
+ report[key] = { status: "error", error: String(e) };
607
+ }
608
+ }
609
+ async handleReadinessCheck(c) {
610
+ return c.json({ status: "ready" });
611
+ }
612
+ async handleLivenessCheck(c) {
613
+ return c.json({
614
+ status: "alive",
615
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
616
+ uptime: process.uptime()
617
+ });
618
+ }
619
+ };
620
+
621
+ // src/features/logger.ts
622
+ var SimpleLogger = class {
623
+ constructor(config) {
624
+ this.config = config;
625
+ }
626
+ config;
627
+ info(message, ...args) {
628
+ if (this.shouldLog("info")) console.log(`[INFO] ${message}`, ...args);
629
+ }
630
+ error(message, ...args) {
631
+ if (this.shouldLog("error")) console.error(`[ERROR] ${message}`, ...args);
632
+ }
633
+ warn(message, ...args) {
634
+ if (this.shouldLog("warn")) console.warn(`[WARN] ${message}`, ...args);
635
+ }
636
+ debug(message, ...args) {
637
+ if (this.shouldLog("debug")) console.debug(`[DEBUG] ${message}`, ...args);
638
+ }
639
+ shouldLog(level) {
640
+ return true;
641
+ }
642
+ };
643
+ var LoggerFeature = class {
644
+ name = "logger";
645
+ config;
646
+ logger;
647
+ constructor(config = {}) {
648
+ this.config = config;
649
+ this.logger = new SimpleLogger(config);
650
+ }
651
+ async initialize(kernel) {
652
+ const app = kernel.getApp();
653
+ app.use("*", async (c, next) => {
654
+ c.set("logger", this.logger);
655
+ await next();
656
+ });
657
+ if (this.config.logRequests) {
658
+ app.use("*", async (c, next) => {
659
+ const start = Date.now();
660
+ const requestId = c.get("requestId");
661
+ this.logger.info(`Incoming request ${c.req.method} ${c.req.path}`, { requestId });
662
+ await next();
663
+ const duration = Date.now() - start;
664
+ if (this.config.logResponses) {
665
+ this.logger.info(`Request completed ${c.res.status} ${duration}ms`, { requestId });
666
+ }
667
+ });
668
+ }
669
+ console.log("\u2705 Logger feature initialized");
670
+ }
671
+ };
672
+
673
+ // src/features/request-id.ts
674
+ var RequestIdFeature = class {
675
+ name = "request-id";
676
+ config;
677
+ constructor(config = {}) {
678
+ this.config = {
679
+ headerName: config.headerName || "X-Request-ID",
680
+ generator: config.generator || this.defaultGenerator
681
+ };
682
+ }
683
+ async initialize(kernel) {
684
+ const app = kernel.getApp();
685
+ app.use("*", async (c, next) => {
686
+ let requestId = c.req.header(this.config.headerName);
687
+ if (!requestId) {
688
+ requestId = this.config.generator();
689
+ }
690
+ c.set("requestId", requestId);
691
+ await next();
692
+ c.res.headers.set(this.config.headerName, requestId);
693
+ });
694
+ console.log("\u2705 Request ID feature initialized");
695
+ }
696
+ defaultGenerator() {
697
+ return crypto.randomUUID();
698
+ }
699
+ };
700
+
701
+ // src/features/storage/adapters/local.ts
702
+ import path from "path";
703
+ import fs from "fs/promises";
704
+ var LocalStorageAdapter = class extends BaseStorageAdapter {
705
+ basePath;
706
+ constructor(config) {
707
+ super();
708
+ this.basePath = config.basePath || "./storage";
709
+ }
710
+ async connect() {
711
+ await fs.mkdir(this.basePath, { recursive: true });
712
+ this.connected = true;
713
+ console.log(`\u2705 Local storage connected at: ${this.basePath}`);
714
+ }
715
+ async disconnect() {
716
+ this.connected = false;
717
+ }
718
+ async put(filePath, data, options) {
719
+ this.ensureConnected();
720
+ const sanitizedPath = this.sanitizePath(filePath);
721
+ const fullPath = path.join(this.basePath, sanitizedPath);
722
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
723
+ await fs.writeFile(fullPath, data);
724
+ const stat = await fs.stat(fullPath);
725
+ return {
726
+ name: path.basename(sanitizedPath),
727
+ path: sanitizedPath,
728
+ size: stat.size,
729
+ mimeType: options?.contentType || this.getMimeType(sanitizedPath),
730
+ lastModified: stat.mtime,
731
+ url: await this.url(sanitizedPath)
732
+ };
733
+ }
734
+ async get(filePath) {
735
+ this.ensureConnected();
736
+ const sanitizedPath = this.sanitizePath(filePath);
737
+ const fullPath = path.join(this.basePath, sanitizedPath);
738
+ try {
739
+ return await fs.readFile(fullPath);
740
+ } catch (error) {
741
+ if (error.code === "ENOENT") return null;
742
+ throw error;
743
+ }
744
+ }
745
+ async delete(filePath) {
746
+ this.ensureConnected();
747
+ const sanitizedPath = this.sanitizePath(filePath);
748
+ const fullPath = path.join(this.basePath, sanitizedPath);
749
+ try {
750
+ await fs.unlink(fullPath);
751
+ } catch (error) {
752
+ if (error.code !== "ENOENT") throw error;
753
+ }
754
+ }
755
+ async exists(filePath) {
756
+ this.ensureConnected();
757
+ const sanitizedPath = this.sanitizePath(filePath);
758
+ const fullPath = path.join(this.basePath, sanitizedPath);
759
+ try {
760
+ await fs.access(fullPath);
761
+ return true;
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
766
+ async isDirectory(filePath) {
767
+ this.ensureConnected();
768
+ const sanitizedPath = this.sanitizePath(filePath);
769
+ const fullPath = path.join(this.basePath, sanitizedPath);
770
+ try {
771
+ const stat = await fs.stat(fullPath);
772
+ return stat.isDirectory();
773
+ } catch {
774
+ return false;
775
+ }
776
+ }
777
+ async list(prefix) {
778
+ this.ensureConnected();
779
+ const searchPath = prefix ? path.join(this.basePath, this.sanitizePath(prefix)) : this.basePath;
780
+ const files = [];
781
+ const walk = async (dir) => {
782
+ try {
783
+ const entries = await fs.readdir(dir, { withFileTypes: true });
784
+ for (const entry of entries) {
785
+ const fullPath = path.join(dir, entry.name);
786
+ if (entry.isDirectory()) {
787
+ await walk(fullPath);
788
+ } else {
789
+ const relativePath = path.relative(this.basePath, fullPath);
790
+ const stat = await fs.stat(fullPath);
791
+ files.push({
792
+ name: entry.name,
793
+ path: this.sanitizePath(relativePath),
794
+ size: stat.size,
795
+ mimeType: this.getMimeType(entry.name),
796
+ lastModified: stat.mtime,
797
+ url: await this.url(this.sanitizePath(relativePath))
798
+ });
799
+ }
800
+ }
801
+ } catch (e) {
802
+ if (e.code !== "ENOENT") throw e;
803
+ }
804
+ };
805
+ await walk(searchPath);
806
+ return files;
807
+ }
808
+ async url(filePath, _expiresIn) {
809
+ const sanitizedPath = this.sanitizePath(filePath);
810
+ return `/storage/${sanitizedPath}`;
811
+ }
812
+ };
813
+
814
+ // src/features/storage/index.ts
815
+ var StorageFeature = class {
816
+ constructor(config) {
817
+ this.config = config;
818
+ }
819
+ config;
820
+ name = "storage";
821
+ adapter;
822
+ async initialize(kernel) {
823
+ if (this.config.adapter === "local") {
824
+ this.adapter = new LocalStorageAdapter(this.config);
825
+ } else if (this.config.adapter === "s3" || this.config.adapter === "minio") {
826
+ const { S3StorageAdapter } = await import("./s3-7IG4ESFW.js");
827
+ this.adapter = new S3StorageAdapter(this.config);
828
+ } else {
829
+ throw new Error(`Adapter ${this.config.adapter} not supported.`);
830
+ }
831
+ await this.adapter.connect();
832
+ const app = kernel.getApp();
833
+ app.use("*", async (c, next) => {
834
+ c.set("storage", this.adapter);
835
+ await next();
836
+ });
837
+ console.log(`\u2705 StorageFeature initialized - ${this.config.adapter}`);
838
+ }
839
+ async shutdown() {
840
+ if (this.adapter) {
841
+ await this.adapter.disconnect();
842
+ }
843
+ }
844
+ getAdapter() {
845
+ return this.adapter;
846
+ }
847
+ };
848
+
849
+ // src/features/db.ts
850
+ import { drizzle } from "drizzle-orm/postgres-js";
851
+ import { drizzle as drizzleMysql } from "drizzle-orm/mysql2";
852
+ import { drizzle as drizzleBunSqlite } from "drizzle-orm/bun-sqlite";
853
+ import postgres from "postgres";
854
+ import mysql from "mysql2/promise";
855
+ import { Database } from "bun:sqlite";
856
+ var DbFeature = class {
857
+ constructor(config) {
858
+ this.config = config;
859
+ this.adapter = config.adapter;
860
+ }
861
+ config;
862
+ name = "db";
863
+ client;
864
+ db;
865
+ adapter;
866
+ async initialize(kernel) {
867
+ const config = this.config;
868
+ console.log(`\u2699\uFE0F Initializing DB driver: ${config.adapter}`);
869
+ try {
870
+ switch (config.adapter) {
871
+ case "postgres": {
872
+ if (!config.connection) throw new Error("Missing connection info");
873
+ const pgConfig = config.connection.connectionString ? config.connection.connectionString : {
874
+ host: config.connection.host || "localhost",
875
+ port: config.connection.port || 5432,
876
+ database: config.connection.database,
877
+ user: config.connection.user,
878
+ password: config.connection.password
879
+ };
880
+ this.client = postgres(pgConfig);
881
+ this.db = drizzle(this.client);
882
+ break;
883
+ }
884
+ case "mysql": {
885
+ if (!config.connection) throw new Error("Missing connection info");
886
+ const mysqlConfig = config.connection.connectionString ? config.connection.connectionString : {
887
+ host: config.connection.host || "localhost",
888
+ port: config.connection.port || 3306,
889
+ database: config.connection.database,
890
+ user: config.connection.user,
891
+ password: config.connection.password
892
+ };
893
+ this.client = await mysql.createConnection(mysqlConfig);
894
+ this.db = drizzleMysql(this.client);
895
+ break;
896
+ }
897
+ case "sqlite": {
898
+ const url = config.connection?.database || ":memory:";
899
+ this.client = new Database(url);
900
+ this.db = drizzleBunSqlite(this.client);
901
+ break;
902
+ }
903
+ default:
904
+ throw new Error(`Unsupported DB adapter: ${config.adapter}`);
905
+ }
906
+ console.log("\u2705 DB connected successfully.");
907
+ } catch (error) {
908
+ console.error("\u274C Failed to connect to DB", error);
909
+ throw error;
910
+ }
911
+ const app = kernel.getApp();
912
+ app.use("*", async (c, next) => {
913
+ c.set("db", this.db);
914
+ await next();
915
+ });
916
+ }
917
+ async shutdown() {
918
+ if (this.client) {
919
+ if (this.client.end) {
920
+ await this.client.end();
921
+ } else if (this.client.close) {
922
+ this.client.close();
923
+ }
924
+ }
925
+ }
926
+ };
927
+
928
+ // src/features/cache.ts
929
+ import Redis from "ioredis";
930
+ var MemoryAdapter = class {
931
+ store = /* @__PURE__ */ new Map();
932
+ async get(key) {
933
+ const item = this.store.get(key);
934
+ if (!item) return null;
935
+ if (item.expires && item.expires < Date.now()) {
936
+ this.store.delete(key);
937
+ return null;
938
+ }
939
+ return item.value;
940
+ }
941
+ async set(key, value, ttl) {
942
+ const expires = ttl ? Date.now() + ttl * 1e3 : null;
943
+ this.store.set(key, { value, expires });
944
+ }
945
+ async delete(key) {
946
+ this.store.delete(key);
947
+ }
948
+ async exists(key) {
949
+ return this.store.has(key);
950
+ }
951
+ async increment(key) {
952
+ const item = this.store.get(key);
953
+ if (!item || item.expires && item.expires < Date.now()) {
954
+ return 0;
955
+ }
956
+ const newVal = Number(item.value) + 1;
957
+ item.value = newVal;
958
+ return newVal;
959
+ }
960
+ };
961
+ var RedisAdapter = class {
962
+ client;
963
+ constructor(options) {
964
+ this.client = new Redis(options);
965
+ }
966
+ async get(key) {
967
+ const value = await this.client.get(key);
968
+ try {
969
+ return value ? JSON.parse(value) : null;
970
+ } catch {
971
+ return value;
972
+ }
973
+ }
974
+ async set(key, value, ttl) {
975
+ const stringValue = typeof value === "string" ? value : JSON.stringify(value);
976
+ if (ttl) {
977
+ await this.client.set(key, stringValue, "EX", ttl);
978
+ } else {
979
+ await this.client.set(key, stringValue);
980
+ }
981
+ }
982
+ async delete(key) {
983
+ await this.client.del(key);
984
+ }
985
+ async exists(key) {
986
+ const result = await this.client.exists(key);
987
+ return result === 1;
988
+ }
989
+ async increment(key) {
990
+ return await this.client.incr(key);
991
+ }
992
+ async disconnect() {
993
+ await this.client.quit();
994
+ }
995
+ };
996
+ var CacheFeature = class {
997
+ constructor(config = { adapter: "memory" }) {
998
+ this.config = config;
999
+ }
1000
+ config;
1001
+ name = "cache";
1002
+ client;
1003
+ async initialize(kernel) {
1004
+ console.log(`\u2699\uFE0F Initializing Cache: ${this.config.adapter}`);
1005
+ if (this.config.adapter === "redis") {
1006
+ const conn = this.config.connection || {};
1007
+ try {
1008
+ this.client = new RedisAdapter({
1009
+ host: conn.host || "localhost",
1010
+ port: conn.port || 6379,
1011
+ password: conn.password,
1012
+ db: conn.db || 0
1013
+ });
1014
+ } catch (err) {
1015
+ console.warn("\u26A0\uFE0F Redis connection failed, falling back to memory cache");
1016
+ this.client = new MemoryAdapter();
1017
+ }
1018
+ } else {
1019
+ this.client = new MemoryAdapter();
1020
+ }
1021
+ const app = kernel.getApp();
1022
+ app.use("*", async (c, next) => {
1023
+ c.set("cache", this.client);
1024
+ await next();
1025
+ });
1026
+ console.log("\u2705 Cache initialized.");
1027
+ }
1028
+ async shutdown() {
1029
+ if (this.client.disconnect) {
1030
+ await this.client.disconnect();
1031
+ }
1032
+ }
1033
+ };
1034
+
1035
+ // src/features/session.ts
1036
+ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
1037
+ import { createHmac } from "crypto";
1038
+ import { sql, eq } from "drizzle-orm";
1039
+ import { pgTable, text as pgText, bigint as pgBigint } from "drizzle-orm/pg-core";
1040
+ import { mysqlTable, varchar as myVarchar, text as myText, bigint as myBigint } from "drizzle-orm/mysql-core";
1041
+ import { sqliteTable, text as sqliteText, integer as sqliteInteger } from "drizzle-orm/sqlite-core";
1042
+ var MemorySessionStore = class {
1043
+ store = /* @__PURE__ */ new Map();
1044
+ cleanupInterval;
1045
+ constructor() {
1046
+ this.cleanupInterval = setInterval(() => this.cleanup(), 3e5);
1047
+ }
1048
+ async get(id) {
1049
+ const entry = this.store.get(id);
1050
+ if (!entry) return null;
1051
+ if (Date.now() > entry.expiresAt) {
1052
+ this.store.delete(id);
1053
+ return null;
1054
+ }
1055
+ return entry.data;
1056
+ }
1057
+ async set(id, data, ttl) {
1058
+ this.store.set(id, { data, expiresAt: Date.now() + ttl * 1e3 });
1059
+ }
1060
+ async destroy(id) {
1061
+ this.store.delete(id);
1062
+ }
1063
+ cleanup() {
1064
+ const now = Date.now();
1065
+ for (const [key, entry] of this.store.entries()) {
1066
+ if (now > entry.expiresAt) this.store.delete(key);
1067
+ }
1068
+ }
1069
+ dispose() {
1070
+ clearInterval(this.cleanupInterval);
1071
+ }
1072
+ };
1073
+ var CacheSessionStore = class {
1074
+ constructor(cache) {
1075
+ this.cache = cache;
1076
+ }
1077
+ cache;
1078
+ async get(id) {
1079
+ const data = await this.cache.get(`session:${id}`);
1080
+ return data || null;
1081
+ }
1082
+ async set(id, data, ttl) {
1083
+ await this.cache.set(`session:${id}`, data, ttl);
1084
+ }
1085
+ async destroy(id) {
1086
+ await this.cache.delete(`session:${id}`);
1087
+ }
1088
+ };
1089
+ var sessionTables = {
1090
+ postgres: pgTable("sessions", {
1091
+ id: pgText("id").primaryKey(),
1092
+ data: pgText("data").notNull(),
1093
+ expiresAt: pgBigint("expires_at", { mode: "number" }).notNull()
1094
+ }),
1095
+ mysql: mysqlTable("sessions", {
1096
+ id: myVarchar("id", { length: 255 }).primaryKey(),
1097
+ data: myText("data").notNull(),
1098
+ expiresAt: myBigint("expires_at", { mode: "number" }).notNull()
1099
+ }),
1100
+ sqlite: sqliteTable("sessions", {
1101
+ id: sqliteText("id").primaryKey(),
1102
+ data: sqliteText("data").notNull(),
1103
+ expiresAt: sqliteInteger("expires_at").notNull()
1104
+ })
1105
+ };
1106
+ var createTableDdl = {
1107
+ postgres: "CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, data TEXT NOT NULL, expires_at BIGINT NOT NULL)",
1108
+ mysql: "CREATE TABLE IF NOT EXISTS sessions (id VARCHAR(255) PRIMARY KEY, data TEXT NOT NULL, expires_at BIGINT NOT NULL)",
1109
+ sqlite: "CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, data TEXT NOT NULL, expires_at INTEGER NOT NULL)"
1110
+ };
1111
+ var DbSessionStore = class {
1112
+ constructor(db, dialect = "sqlite") {
1113
+ this.db = db;
1114
+ this.dialect = sessionTables[dialect] ? dialect : "sqlite";
1115
+ this.table = sessionTables[this.dialect];
1116
+ }
1117
+ db;
1118
+ initialized = false;
1119
+ dialect;
1120
+ table;
1121
+ async ensureTable() {
1122
+ if (this.initialized) return;
1123
+ try {
1124
+ const ddl = sql.raw(createTableDdl[this.dialect]);
1125
+ if (this.dialect === "sqlite") {
1126
+ await this.db.run(ddl);
1127
+ } else {
1128
+ await this.db.execute(ddl);
1129
+ }
1130
+ } catch (err) {
1131
+ console.error("[session] Failed to ensure sessions table:", err);
1132
+ }
1133
+ this.initialized = true;
1134
+ }
1135
+ async get(id) {
1136
+ await this.ensureTable();
1137
+ try {
1138
+ const rows = await this.db.select().from(this.table).where(eq(this.table.id, id)).limit(1);
1139
+ const row = rows?.[0];
1140
+ if (!row) return null;
1141
+ if (Date.now() > Number(row.expiresAt)) {
1142
+ await this.destroy(id);
1143
+ return null;
1144
+ }
1145
+ return JSON.parse(row.data);
1146
+ } catch (err) {
1147
+ console.error("[session] Failed to read session:", err);
1148
+ return null;
1149
+ }
1150
+ }
1151
+ async set(id, data, ttl) {
1152
+ await this.ensureTable();
1153
+ const row = { id, data: JSON.stringify(data), expiresAt: Date.now() + ttl * 1e3 };
1154
+ try {
1155
+ await this.db.delete(this.table).where(eq(this.table.id, id));
1156
+ await this.db.insert(this.table).values(row);
1157
+ } catch (err) {
1158
+ console.error("[session] Failed to write session:", err);
1159
+ }
1160
+ }
1161
+ async destroy(id) {
1162
+ try {
1163
+ await this.db.delete(this.table).where(eq(this.table.id, id));
1164
+ } catch (err) {
1165
+ console.error("[session] Failed to destroy session:", err);
1166
+ }
1167
+ }
1168
+ };
1169
+ function signValue(value, secret) {
1170
+ const signature = createHmac("sha256", secret).update(value).digest("base64url");
1171
+ return `${value}.${signature}`;
1172
+ }
1173
+ function verifySignedValue(signed, secret) {
1174
+ const lastDot = signed.lastIndexOf(".");
1175
+ if (lastDot === -1) return null;
1176
+ const value = signed.substring(0, lastDot);
1177
+ const signature = signed.substring(lastDot + 1);
1178
+ const expected = createHmac("sha256", secret).update(value).digest("base64url");
1179
+ if (signature !== expected) return null;
1180
+ return value;
1181
+ }
1182
+ var SessionFeature = class {
1183
+ constructor(config) {
1184
+ this.config = config;
1185
+ this.ttl = config.ttl || 86400;
1186
+ this.cookieName = config.cookieName || "sid";
1187
+ if (config.store === "cache") {
1188
+ this.dependencies = ["cache"];
1189
+ } else if (config.store === "db") {
1190
+ this.dependencies = ["db"];
1191
+ }
1192
+ }
1193
+ config;
1194
+ name = "session";
1195
+ dependencies;
1196
+ store;
1197
+ ttl;
1198
+ cookieName;
1199
+ async initialize(kernel) {
1200
+ console.log(`\u2699\uFE0F Initializing Session: store=${this.config.store}`);
1201
+ switch (this.config.store) {
1202
+ case "memory":
1203
+ this.store = new MemorySessionStore();
1204
+ break;
1205
+ case "cache": {
1206
+ const cacheFeature = kernel.getFeature("cache");
1207
+ if (!cacheFeature?.client) {
1208
+ console.warn("\u26A0\uFE0F Cache feature not available, falling back to memory session store");
1209
+ this.store = new MemorySessionStore();
1210
+ } else {
1211
+ this.store = new CacheSessionStore(cacheFeature.client);
1212
+ }
1213
+ break;
1214
+ }
1215
+ case "db": {
1216
+ const dbFeature = kernel.getFeature("db");
1217
+ if (!dbFeature?.db) {
1218
+ console.warn("\u26A0\uFE0F DB feature not available, falling back to memory session store");
1219
+ this.store = new MemorySessionStore();
1220
+ } else {
1221
+ this.store = new DbSessionStore(dbFeature.db, dbFeature.adapter);
1222
+ }
1223
+ break;
1224
+ }
1225
+ }
1226
+ const app = kernel.getApp();
1227
+ app.use("*", async (c, next) => {
1228
+ const signedCookie = getCookie(c, this.cookieName);
1229
+ let sessionId = null;
1230
+ let session = {};
1231
+ if (signedCookie) {
1232
+ sessionId = verifySignedValue(signedCookie, this.config.secret);
1233
+ if (sessionId) {
1234
+ const data = await this.store.get(sessionId);
1235
+ if (data) {
1236
+ session = data;
1237
+ } else {
1238
+ sessionId = null;
1239
+ }
1240
+ }
1241
+ }
1242
+ if (!sessionId) {
1243
+ sessionId = crypto.randomUUID();
1244
+ }
1245
+ let destroyed = false;
1246
+ c.set("session", session);
1247
+ c.set("sessionId", sessionId);
1248
+ c.set("destroySession", async () => {
1249
+ destroyed = true;
1250
+ await this.store.destroy(sessionId);
1251
+ deleteCookie(c, this.cookieName);
1252
+ });
1253
+ await next();
1254
+ if (destroyed) return;
1255
+ const currentSession = c.get("session");
1256
+ if (currentSession && Object.keys(currentSession).length > 0) {
1257
+ await this.store.set(sessionId, currentSession, this.ttl);
1258
+ const signed = signValue(sessionId, this.config.secret);
1259
+ const opts = this.config.cookieOptions || {};
1260
+ setCookie(c, this.cookieName, signed, {
1261
+ httpOnly: true,
1262
+ sameSite: opts.sameSite || "Lax",
1263
+ secure: opts.secure ?? false,
1264
+ domain: opts.domain,
1265
+ path: opts.path || "/",
1266
+ maxAge: this.ttl
1267
+ });
1268
+ }
1269
+ });
1270
+ console.log("\u2705 Session initialized");
1271
+ }
1272
+ async shutdown() {
1273
+ if (this.store instanceof MemorySessionStore) {
1274
+ this.store.dispose?.();
1275
+ }
1276
+ }
1277
+ };
1278
+
1279
+ // src/features/auth/index.ts
1280
+ import { HTTPException as HTTPException4 } from "hono/http-exception";
1281
+
1282
+ // src/features/auth/better-auth-config.ts
1283
+ import { betterAuth } from "better-auth";
1284
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
1285
+ import { genericOAuth } from "better-auth/plugins/generic-oauth";
1286
+
1287
+ // src/features/auth/schema.ts
1288
+ import { pgTable as pgTable2, text, timestamp, boolean } from "drizzle-orm/pg-core";
1289
+ import { mysqlTable as mysqlTable2, varchar, timestamp as mysqlTimestamp, boolean as mysqlBoolean, text as mysqlText } from "drizzle-orm/mysql-core";
1290
+ import { sqliteTable as sqliteTable2, text as sqliteText2, integer } from "drizzle-orm/sqlite-core";
1291
+ var pgUser = pgTable2("user", {
1292
+ id: text("id").primaryKey(),
1293
+ name: text("name"),
1294
+ email: text("email").notNull().unique(),
1295
+ emailVerified: boolean("emailVerified").notNull(),
1296
+ image: text("image"),
1297
+ createdAt: timestamp("createdAt").notNull(),
1298
+ updatedAt: timestamp("updatedAt").notNull()
1299
+ });
1300
+ var pgSession = pgTable2("session", {
1301
+ id: text("id").primaryKey(),
1302
+ expiresAt: timestamp("expiresAt").notNull(),
1303
+ token: text("token").notNull().unique(),
1304
+ createdAt: timestamp("createdAt").notNull(),
1305
+ updatedAt: timestamp("updatedAt").notNull(),
1306
+ ipAddress: text("ipAddress"),
1307
+ userAgent: text("userAgent"),
1308
+ userId: text("userId").notNull().references(() => pgUser.id)
1309
+ });
1310
+ var pgAccount = pgTable2("account", {
1311
+ id: text("id").primaryKey(),
1312
+ accountId: text("accountId").notNull(),
1313
+ providerId: text("providerId").notNull(),
1314
+ userId: text("userId").notNull().references(() => pgUser.id),
1315
+ accessToken: text("accessToken"),
1316
+ refreshToken: text("refreshToken"),
1317
+ idToken: text("idToken"),
1318
+ accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
1319
+ refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
1320
+ scope: text("scope"),
1321
+ password: text("password"),
1322
+ createdAt: timestamp("createdAt").notNull(),
1323
+ updatedAt: timestamp("updatedAt").notNull()
1324
+ });
1325
+ var pgVerification = pgTable2("verification", {
1326
+ id: text("id").primaryKey(),
1327
+ identifier: text("identifier").notNull(),
1328
+ value: text("value").notNull(),
1329
+ expiresAt: timestamp("expiresAt").notNull(),
1330
+ createdAt: timestamp("createdAt"),
1331
+ updatedAt: timestamp("updatedAt")
1332
+ });
1333
+ var pgSchema = {
1334
+ user: pgUser,
1335
+ session: pgSession,
1336
+ account: pgAccount,
1337
+ verification: pgVerification
1338
+ };
1339
+ var mysqlUser = mysqlTable2("user", {
1340
+ id: varchar("id", { length: 36 }).primaryKey(),
1341
+ name: varchar("name", { length: 255 }),
1342
+ email: varchar("email", { length: 255 }).notNull().unique(),
1343
+ emailVerified: mysqlBoolean("emailVerified").notNull(),
1344
+ image: varchar("image", { length: 255 }),
1345
+ createdAt: mysqlTimestamp("createdAt").notNull(),
1346
+ updatedAt: mysqlTimestamp("updatedAt").notNull()
1347
+ });
1348
+ var mysqlSession = mysqlTable2("session", {
1349
+ id: varchar("id", { length: 36 }).primaryKey(),
1350
+ expiresAt: mysqlTimestamp("expiresAt").notNull(),
1351
+ token: varchar("token", { length: 255 }).notNull().unique(),
1352
+ createdAt: mysqlTimestamp("createdAt").notNull(),
1353
+ updatedAt: mysqlTimestamp("updatedAt").notNull(),
1354
+ ipAddress: varchar("ipAddress", { length: 45 }),
1355
+ userAgent: mysqlText("userAgent"),
1356
+ userId: varchar("userId", { length: 36 }).notNull().references(() => mysqlUser.id)
1357
+ });
1358
+ var mysqlAccount = mysqlTable2("account", {
1359
+ id: varchar("id", { length: 36 }).primaryKey(),
1360
+ accountId: varchar("accountId", { length: 255 }).notNull(),
1361
+ providerId: varchar("providerId", { length: 255 }).notNull(),
1362
+ userId: varchar("userId", { length: 36 }).notNull().references(() => mysqlUser.id),
1363
+ accessToken: mysqlText("accessToken"),
1364
+ refreshToken: mysqlText("refreshToken"),
1365
+ idToken: mysqlText("idToken"),
1366
+ accessTokenExpiresAt: mysqlTimestamp("accessTokenExpiresAt"),
1367
+ refreshTokenExpiresAt: mysqlTimestamp("refreshTokenExpiresAt"),
1368
+ scope: mysqlText("scope"),
1369
+ password: varchar("password", { length: 255 }),
1370
+ createdAt: mysqlTimestamp("createdAt").notNull(),
1371
+ updatedAt: mysqlTimestamp("updatedAt").notNull()
1372
+ });
1373
+ var mysqlVerification = mysqlTable2("verification", {
1374
+ id: varchar("id", { length: 36 }).primaryKey(),
1375
+ identifier: varchar("identifier", { length: 255 }).notNull(),
1376
+ value: varchar("value", { length: 255 }).notNull(),
1377
+ expiresAt: mysqlTimestamp("expiresAt").notNull(),
1378
+ createdAt: mysqlTimestamp("createdAt"),
1379
+ updatedAt: mysqlTimestamp("updatedAt")
1380
+ });
1381
+ var mysqlSchema = {
1382
+ user: mysqlUser,
1383
+ session: mysqlSession,
1384
+ account: mysqlAccount,
1385
+ verification: mysqlVerification
1386
+ };
1387
+ var sqliteUser = sqliteTable2("user", {
1388
+ id: sqliteText2("id").primaryKey(),
1389
+ name: sqliteText2("name"),
1390
+ email: sqliteText2("email").notNull().unique(),
1391
+ emailVerified: integer("emailVerified", { mode: "boolean" }).notNull(),
1392
+ image: sqliteText2("image"),
1393
+ createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
1394
+ updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull()
1395
+ });
1396
+ var sqliteSession = sqliteTable2("session", {
1397
+ id: sqliteText2("id").primaryKey(),
1398
+ expiresAt: integer("expiresAt", { mode: "timestamp" }).notNull(),
1399
+ token: sqliteText2("token").notNull().unique(),
1400
+ createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
1401
+ updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull(),
1402
+ ipAddress: sqliteText2("ipAddress"),
1403
+ userAgent: sqliteText2("userAgent"),
1404
+ userId: sqliteText2("userId").notNull().references(() => sqliteUser.id)
1405
+ });
1406
+ var sqliteAccount = sqliteTable2("account", {
1407
+ id: sqliteText2("id").primaryKey(),
1408
+ accountId: sqliteText2("accountId").notNull(),
1409
+ providerId: sqliteText2("providerId").notNull(),
1410
+ userId: sqliteText2("userId").notNull().references(() => sqliteUser.id),
1411
+ accessToken: sqliteText2("accessToken"),
1412
+ refreshToken: sqliteText2("refreshToken"),
1413
+ idToken: sqliteText2("idToken"),
1414
+ accessTokenExpiresAt: integer("accessTokenExpiresAt", { mode: "timestamp" }),
1415
+ refreshTokenExpiresAt: integer("refreshTokenExpiresAt", { mode: "timestamp" }),
1416
+ scope: sqliteText2("scope"),
1417
+ password: sqliteText2("password"),
1418
+ createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
1419
+ updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull()
1420
+ });
1421
+ var sqliteVerification = sqliteTable2("verification", {
1422
+ id: sqliteText2("id").primaryKey(),
1423
+ identifier: sqliteText2("identifier").notNull(),
1424
+ value: sqliteText2("value").notNull(),
1425
+ expiresAt: integer("expiresAt", { mode: "timestamp" }).notNull(),
1426
+ createdAt: integer("createdAt", { mode: "timestamp" }),
1427
+ updatedAt: integer("updatedAt", { mode: "timestamp" })
1428
+ });
1429
+ var sqliteSchema = {
1430
+ user: sqliteUser,
1431
+ session: sqliteSession,
1432
+ account: sqliteAccount,
1433
+ verification: sqliteVerification
1434
+ };
1435
+
1436
+ // src/features/auth/better-auth-config.ts
1437
+ function createBetterAuth(options) {
1438
+ const {
1439
+ db,
1440
+ adapterType,
1441
+ secret,
1442
+ baseURL = "http://localhost:3000",
1443
+ basePath = "/api/auth",
1444
+ trustedOrigins = [],
1445
+ enableEmailPassword = true,
1446
+ disableCSRFCheck = false,
1447
+ socialProviders,
1448
+ oidcConfig
1449
+ } = options;
1450
+ const baseOrigin = new URL(baseURL).origin;
1451
+ const allTrustedOrigins = trustedOrigins.includes(baseOrigin) ? trustedOrigins : [baseOrigin, ...trustedOrigins];
1452
+ console.log("[BetterAuth Config] Creating Drizzle adapter...");
1453
+ let schema;
1454
+ let provider;
1455
+ switch (adapterType) {
1456
+ case "postgres":
1457
+ schema = pgSchema;
1458
+ provider = "pg";
1459
+ break;
1460
+ case "mysql":
1461
+ schema = mysqlSchema;
1462
+ provider = "mysql";
1463
+ break;
1464
+ case "sqlite":
1465
+ schema = sqliteSchema;
1466
+ provider = "sqlite";
1467
+ break;
1468
+ default:
1469
+ throw new Error(`Unsupported adapter type: ${adapterType}`);
1470
+ }
1471
+ const database = drizzleAdapter(db, {
1472
+ provider,
1473
+ schema
1474
+ });
1475
+ console.log("[BetterAuth Config] Initializing betterAuth with config:", {
1476
+ baseURL,
1477
+ basePath,
1478
+ provider,
1479
+ enableEmailPassword
1480
+ });
1481
+ const plugins = [];
1482
+ if (oidcConfig) {
1483
+ const authorizationUrl = oidcConfig.authorizationEndpoint || `${oidcConfig.issuer}/protocol/openid-connect/auth`;
1484
+ const tokenUrl = oidcConfig.tokenEndpoint || `${oidcConfig.issuer}/protocol/openid-connect/token`;
1485
+ const userInfoUrl = oidcConfig.userinfoEndpoint || `${oidcConfig.issuer}/protocol/openid-connect/userinfo`;
1486
+ plugins.push(
1487
+ genericOAuth({
1488
+ config: [
1489
+ {
1490
+ providerId: oidcConfig.providerId || "oidc",
1491
+ clientId: oidcConfig.clientId,
1492
+ clientSecret: oidcConfig.clientSecret,
1493
+ authorizationUrl,
1494
+ tokenUrl,
1495
+ userInfoUrl,
1496
+ discoveryUrl: oidcConfig.discoveryEndpoint || `${oidcConfig.issuer}/.well-known/openid-configuration`,
1497
+ scopes: oidcConfig.scopes || ["openid", "email", "profile"],
1498
+ pkce: oidcConfig.pkce !== void 0 ? oidcConfig.pkce : false,
1499
+ mapProfileToUser: (profile) => {
1500
+ return {
1501
+ id: profile.sub || profile.id,
1502
+ email: profile.email,
1503
+ name: profile.name || profile.preferred_username,
1504
+ image: profile.picture || profile.image,
1505
+ emailVerified: profile.email_verified || false
1506
+ };
1507
+ }
1508
+ }
1509
+ ]
1510
+ })
1511
+ );
1512
+ }
1513
+ return betterAuth({
1514
+ database,
1515
+ secret,
1516
+ baseURL,
1517
+ basePath,
1518
+ trustedOrigins: allTrustedOrigins,
1519
+ emailAndPassword: enableEmailPassword ? {
1520
+ enabled: true,
1521
+ autoSignIn: true
1522
+ } : void 0,
1523
+ socialProviders: Object.keys(socialProviders || {}).length > 0 ? socialProviders : void 0,
1524
+ plugins,
1525
+ session: {
1526
+ expiresIn: 60 * 60 * 24 * 7,
1527
+ updateAge: 60 * 60 * 24,
1528
+ cookieCache: {
1529
+ enabled: true,
1530
+ maxAge: 5 * 60
1531
+ }
1532
+ },
1533
+ advanced: {
1534
+ disableCSRFCheck,
1535
+ generateId: () => crypto.randomUUID().replace(/-/g, "")
1536
+ }
1537
+ });
1538
+ }
1539
+
1540
+ // src/features/auth/index.ts
1541
+ import { z as z2 } from "@hono/zod-openapi";
1542
+ var SignInSchema = z2.object({
1543
+ email: z2.string().email(),
1544
+ password: z2.string().min(1)
1545
+ });
1546
+ var SignUpSchema = z2.object({
1547
+ email: z2.string().email(),
1548
+ password: z2.string().min(8),
1549
+ name: z2.string().optional()
1550
+ });
1551
+ var AuthSuccessSchema = z2.object({
1552
+ token: z2.string().optional(),
1553
+ user: z2.object({
1554
+ id: z2.string(),
1555
+ email: z2.string(),
1556
+ name: z2.string().optional()
1557
+ }).optional(),
1558
+ session: z2.any().optional()
1559
+ });
1560
+ var SessionResponseSchema = z2.object({
1561
+ session: z2.any().nullable(),
1562
+ user: z2.any().nullable()
1563
+ });
1564
+ var AuthFeature = class {
1565
+ name = "auth";
1566
+ dependencies = ["db"];
1567
+ auth;
1568
+ config;
1569
+ kernel;
1570
+ authMode;
1571
+ constructor(config) {
1572
+ this.config = {
1573
+ ...config,
1574
+ basePath: config.basePath || "/api/sso",
1575
+ baseURL: config.baseURL || "http://localhost:3000"
1576
+ };
1577
+ if (config.authMode === "oidc" || config.authMode === "email") {
1578
+ this.authMode = config.authMode;
1579
+ } else {
1580
+ this.authMode = config.oidcConfig ? "oidc" : "email";
1581
+ }
1582
+ }
1583
+ async initialize(kernel) {
1584
+ this.kernel = kernel;
1585
+ const dbFeature = kernel.getFeature("db");
1586
+ if (!dbFeature) {
1587
+ throw new Error("AuthFeature requires DbFeature");
1588
+ }
1589
+ const db = dbFeature.db;
1590
+ const adapterType = dbFeature.adapter;
1591
+ if (!adapterType) {
1592
+ throw new Error("DbFeature must expose 'adapter' type (postgres, mysql, sqlite)");
1593
+ }
1594
+ this.auth = createBetterAuth({
1595
+ db,
1596
+ adapterType,
1597
+ secret: this.config.secret,
1598
+ baseURL: this.config.baseURL,
1599
+ basePath: this.config.basePath,
1600
+ trustedOrigins: this.config.trustedOrigins,
1601
+ enableEmailPassword: true,
1602
+ disableCSRFCheck: this.config.disableCSRFCheck,
1603
+ socialProviders: this.config.socialProviders,
1604
+ oidcConfig: this.config.oidcConfig
1605
+ });
1606
+ const app = kernel.getApp();
1607
+ app.use("*", async (c, next) => {
1608
+ try {
1609
+ const session = await this.auth.api.getSession({
1610
+ headers: c.req.raw.headers
1611
+ });
1612
+ if (session && session.user) {
1613
+ c.set("user", session.user);
1614
+ c.set("authUser", session.user);
1615
+ }
1616
+ } catch (error) {
1617
+ }
1618
+ await next();
1619
+ });
1620
+ console.log("\u2705 Auth feature initialized (better-auth)");
1621
+ }
1622
+ routes(app) {
1623
+ if (!this.auth) {
1624
+ throw new Error("Auth not initialized");
1625
+ }
1626
+ const base = this.config.basePath;
1627
+ const openapi = this.kernel?.getFeature("openapi");
1628
+ if (openapi) {
1629
+ openapi.addRoute(
1630
+ {
1631
+ method: "post",
1632
+ path: `${base}/sign-in/email`,
1633
+ tags: ["Auth"],
1634
+ summary: "Sign in with email and password",
1635
+ request: {
1636
+ body: {
1637
+ content: { "application/json": { schema: SignInSchema } }
1638
+ }
1639
+ },
1640
+ responses: {
1641
+ 200: {
1642
+ description: "Sign in successful",
1643
+ content: { "application/json": { schema: AuthSuccessSchema } }
1644
+ },
1645
+ 401: { description: "Invalid credentials" }
1646
+ }
1647
+ },
1648
+ (c) => this.auth.handler(c.req.raw)
1649
+ );
1650
+ openapi.addRoute(
1651
+ {
1652
+ method: "post",
1653
+ path: `${base}/sign-up/email`,
1654
+ tags: ["Auth"],
1655
+ summary: "Create a new account",
1656
+ request: {
1657
+ body: {
1658
+ content: { "application/json": { schema: SignUpSchema } }
1659
+ }
1660
+ },
1661
+ responses: {
1662
+ 200: {
1663
+ description: "Account created",
1664
+ content: { "application/json": { schema: AuthSuccessSchema } }
1665
+ },
1666
+ 400: { description: "Validation error" }
1667
+ }
1668
+ },
1669
+ (c) => this.auth.handler(c.req.raw)
1670
+ );
1671
+ openapi.addRoute(
1672
+ {
1673
+ method: "post",
1674
+ path: `${base}/sign-out`,
1675
+ tags: ["Auth"],
1676
+ summary: "Sign out",
1677
+ responses: {
1678
+ 200: { description: "Signed out" }
1679
+ }
1680
+ },
1681
+ (c) => this.auth.handler(c.req.raw)
1682
+ );
1683
+ openapi.addRoute(
1684
+ {
1685
+ method: "get",
1686
+ path: `${base}/get-session`,
1687
+ tags: ["Auth"],
1688
+ summary: "Get current session",
1689
+ responses: {
1690
+ 200: {
1691
+ description: "Current session",
1692
+ content: { "application/json": { schema: SessionResponseSchema } }
1693
+ }
1694
+ }
1695
+ },
1696
+ (c) => this.auth.handler(c.req.raw)
1697
+ );
1698
+ }
1699
+ app.on(["POST", "GET"], `${base}/*`, (c) => {
1700
+ return this.auth.handler(c.req.raw);
1701
+ });
1702
+ }
1703
+ getAuth() {
1704
+ return this.auth;
1705
+ }
1706
+ };
1707
+ function requireAuth(kernel) {
1708
+ return async (c, next) => {
1709
+ const authFeature = kernel.getFeature("auth");
1710
+ if (!authFeature) throw new HTTPException4(500, { message: "Auth not initialized" });
1711
+ const auth = authFeature.getAuth();
1712
+ if (!auth) throw new HTTPException4(500, { message: "Auth not ready" });
1713
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
1714
+ if (!session || !session.user) {
1715
+ throw new HTTPException4(401, { message: "Unauthorized" });
1716
+ }
1717
+ c.set("user", session.user);
1718
+ c.set("authUser", session.user);
1719
+ await next();
1720
+ };
1721
+ }
1722
+
1723
+ // src/features/rate-limit.ts
1724
+ import { HTTPException as HTTPException5 } from "hono/http-exception";
1725
+ var MemoryStore = class {
1726
+ store = /* @__PURE__ */ new Map();
1727
+ async get(key) {
1728
+ const entry = this.store.get(key);
1729
+ if (!entry) return null;
1730
+ if (Date.now() > entry.expiresAt) {
1731
+ this.store.delete(key);
1732
+ return null;
1733
+ }
1734
+ return entry.count;
1735
+ }
1736
+ async set(key, value, ttl) {
1737
+ this.store.set(key, { count: value, expiresAt: Date.now() + ttl });
1738
+ }
1739
+ async increment(key, ttl) {
1740
+ const entry = this.store.get(key);
1741
+ if (!entry || Date.now() > entry.expiresAt) {
1742
+ this.store.set(key, { count: 1, expiresAt: Date.now() + ttl });
1743
+ return 1;
1744
+ }
1745
+ entry.count++;
1746
+ return entry.count;
1747
+ }
1748
+ cleanup() {
1749
+ const now = Date.now();
1750
+ for (const [key, entry] of this.store.entries()) {
1751
+ if (now > entry.expiresAt) this.store.delete(key);
1752
+ }
1753
+ }
1754
+ };
1755
+ var CacheStoreWrapper = class {
1756
+ constructor(cache) {
1757
+ this.cache = cache;
1758
+ }
1759
+ cache;
1760
+ async get(key) {
1761
+ return await this.cache.get(key);
1762
+ }
1763
+ async set(key, value, ttl) {
1764
+ await this.cache.set(key, value, ttl / 1e3);
1765
+ }
1766
+ async increment(key, ttl) {
1767
+ const ttlSeconds = Math.ceil(ttl / 1e3);
1768
+ const current = await this.cache.get(key);
1769
+ if (current === null) {
1770
+ await this.cache.set(key, 1, ttlSeconds);
1771
+ return 1;
1772
+ }
1773
+ if (this.cache.increment) return await this.cache.increment(key);
1774
+ const newVal = Number(current) + 1;
1775
+ await this.cache.set(key, newVal, ttlSeconds);
1776
+ return newVal;
1777
+ }
1778
+ };
1779
+ var RateLimitFeature = class {
1780
+ name = "rate-limit";
1781
+ config;
1782
+ store;
1783
+ cleanupInterval;
1784
+ constructor(config = {}) {
1785
+ this.config = {
1786
+ windowMs: config.windowMs || 15 * 60 * 1e3,
1787
+ max: config.max || 100,
1788
+ standardHeaders: config.standardHeaders ?? true,
1789
+ store: config.store || "memory",
1790
+ keyGenerator: config.keyGenerator,
1791
+ skip: config.skip,
1792
+ handler: config.handler
1793
+ };
1794
+ }
1795
+ async initialize(kernel) {
1796
+ if (this.config.store === "cache") {
1797
+ const cacheFeature = kernel.getFeature("cache");
1798
+ if (cacheFeature?.client) {
1799
+ this.store = new CacheStoreWrapper(cacheFeature.client);
1800
+ } else {
1801
+ console.warn("\u26A0\uFE0F Cache feature not available for rate-limit, falling back to memory store");
1802
+ const mem = new MemoryStore();
1803
+ this.store = mem;
1804
+ this.cleanupInterval = setInterval(() => mem.cleanup(), 3e5);
1805
+ }
1806
+ }
1807
+ if (this.config.store === "memory") {
1808
+ const mem = new MemoryStore();
1809
+ this.store = mem;
1810
+ this.cleanupInterval = setInterval(() => mem.cleanup(), 3e5);
1811
+ }
1812
+ const app = kernel.getApp();
1813
+ app.use("*", async (c, next) => {
1814
+ await this.middleware(c, next);
1815
+ });
1816
+ console.log("\u2705 Rate limit feature initialized");
1817
+ }
1818
+ async middleware(c, next) {
1819
+ if (this.config.skip && this.config.skip(c)) {
1820
+ await next();
1821
+ return;
1822
+ }
1823
+ let store = this.store;
1824
+ if (!store && this.config.store === "cache") {
1825
+ const cache = c.get("cache");
1826
+ if (cache) {
1827
+ store = new CacheStoreWrapper(cache);
1828
+ } else {
1829
+ if (!this.store) {
1830
+ this.store = new MemoryStore();
1831
+ this.cleanupInterval = setInterval(() => this.store.cleanup(), 3e5);
1832
+ }
1833
+ store = this.store;
1834
+ }
1835
+ }
1836
+ if (!store) {
1837
+ await next();
1838
+ return;
1839
+ }
1840
+ const key = this.config.keyGenerator ? this.config.keyGenerator(c) : this.defaultKeyGenerator(c);
1841
+ const rlKey = `rate_limit:${key}`;
1842
+ const count = await store.increment(rlKey, this.config.windowMs);
1843
+ if (count > this.config.max) {
1844
+ if (this.config.handler) return this.config.handler(c);
1845
+ throw new HTTPException5(429, { message: "Too many requests" });
1846
+ }
1847
+ if (this.config.standardHeaders) {
1848
+ c.header("X-RateLimit-Limit", String(this.config.max));
1849
+ c.header("X-RateLimit-Remaining", String(Math.max(0, this.config.max - count)));
1850
+ c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + this.config.windowMs) / 1e3)));
1851
+ }
1852
+ await next();
1853
+ }
1854
+ defaultKeyGenerator(c) {
1855
+ return c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "unknown";
1856
+ }
1857
+ async shutdown() {
1858
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
1859
+ }
1860
+ };
1861
+
1862
+ // src/features/permissions.ts
1863
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
1864
+ var DEFAULT_ROLES = {
1865
+ admin: { name: "admin", permissions: ["*"], description: "Full access" },
1866
+ user: { name: "user", permissions: ["read:own", "write:own"], description: "User access" },
1867
+ guest: { name: "guest", permissions: ["read:public"], description: "Guest access" }
1868
+ };
1869
+ var PermissionsFeature = class {
1870
+ name = "permissions";
1871
+ dependencies = ["auth"];
1872
+ config;
1873
+ roles = /* @__PURE__ */ new Map();
1874
+ constructor(config = {}) {
1875
+ this.config = {
1876
+ loadPermissions: config.loadPermissions || (async () => ["read:own"]),
1877
+ loadRoles: config.loadRoles || (async () => ["user"]),
1878
+ anonymousPermissions: config.anonymousPermissions || ["read:public"],
1879
+ enableRBAC: config.enableRBAC ?? true,
1880
+ cachePermissions: config.cachePermissions ?? true,
1881
+ cacheTTL: config.cacheTTL || 3600
1882
+ };
1883
+ Object.values(DEFAULT_ROLES).forEach((r) => this.roles.set(r.name, r));
1884
+ }
1885
+ async initialize(kernel) {
1886
+ const app = kernel.getApp();
1887
+ app.use("*", async (c, next) => {
1888
+ await this.permissionsMiddleware(c, next, kernel);
1889
+ });
1890
+ console.log("\u2705 Permissions feature initialized");
1891
+ }
1892
+ async permissionsMiddleware(c, next, _kernel) {
1893
+ const user = c.get("user") || c.get("session")?.user;
1894
+ const userId = user?.id;
1895
+ let permissions = [];
1896
+ let roles = [];
1897
+ if (userId) {
1898
+ if (this.config.cachePermissions) {
1899
+ const cache = c.get("cache");
1900
+ if (cache) {
1901
+ const cached = await cache.get(`permissions:${userId}`);
1902
+ if (cached) {
1903
+ permissions = cached.permissions || [];
1904
+ roles = cached.roles || [];
1905
+ }
1906
+ }
1907
+ }
1908
+ if (permissions.length === 0) {
1909
+ permissions = await this.config.loadPermissions(userId);
1910
+ if (this.config.enableRBAC) {
1911
+ roles = await this.config.loadRoles(userId);
1912
+ for (const rName of roles) {
1913
+ const r = this.roles.get(rName);
1914
+ if (r) permissions.push(...r.permissions);
1915
+ }
1916
+ }
1917
+ if (this.config.cachePermissions) {
1918
+ const cache = c.get("cache");
1919
+ if (cache) {
1920
+ await cache.set(`permissions:${userId}`, { permissions, roles }, this.config.cacheTTL);
1921
+ }
1922
+ }
1923
+ }
1924
+ } else {
1925
+ permissions = this.config.anonymousPermissions;
1926
+ }
1927
+ permissions = [...new Set(permissions)];
1928
+ c.set("userPermissions", permissions);
1929
+ c.set("checkPermission", (p) => this.checkPermission(permissions, p));
1930
+ c.set("hasRole", (r) => roles.includes(r));
1931
+ c.set("hasAnyRole", (...rs) => rs.some((r) => roles.includes(r)));
1932
+ c.set("hasAllRoles", (...rs) => rs.every((r) => roles.includes(r)));
1933
+ await next();
1934
+ }
1935
+ checkPermission(userPerms, required) {
1936
+ if (userPerms.includes("*")) return true;
1937
+ if (userPerms.includes(required)) return true;
1938
+ const parts = required.split(":");
1939
+ for (let i = parts.length - 1; i > 0; i--) {
1940
+ const pattern = parts.slice(0, i).join(":") + ":*";
1941
+ if (userPerms.includes(pattern)) return true;
1942
+ }
1943
+ return false;
1944
+ }
1945
+ };
1946
+ function requirePermission(permission) {
1947
+ return async (c, next) => {
1948
+ const check = c.get("checkPermission");
1949
+ if (!check || !check(permission)) throw new HTTPException6(403, { message: `Missing permission: ${permission}` });
1950
+ await next();
1951
+ };
1952
+ }
1953
+ function requireRole(role) {
1954
+ return async (c, next) => {
1955
+ const check = c.get("hasRole");
1956
+ if (!check || !check(role)) throw new HTTPException6(403, { message: `Missing role: ${role}` });
1957
+ await next();
1958
+ };
1959
+ }
1960
+
1961
+ // src/features/api-key.ts
1962
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
1963
+ var ApiKeyStore = class {
1964
+ constructor(config, kernel) {
1965
+ this.config = config;
1966
+ this.kernel = kernel;
1967
+ this.initializeStaticKeys();
1968
+ }
1969
+ config;
1970
+ kernel;
1971
+ staticKeysMap = /* @__PURE__ */ new Map();
1972
+ cache;
1973
+ getCache() {
1974
+ if (!this.cache && this.config.enableCache) {
1975
+ const cacheFeature = this.kernel.getFeature("cache");
1976
+ if (cacheFeature) {
1977
+ this.cache = cacheFeature.client;
1978
+ }
1979
+ }
1980
+ return this.cache;
1981
+ }
1982
+ initializeStaticKeys() {
1983
+ if (!this.config.staticKeys || this.config.staticKeys.length === 0) {
1984
+ return;
1985
+ }
1986
+ for (const staticKey of this.config.staticKeys) {
1987
+ const metadata = {
1988
+ id: this.generateId(staticKey.key),
1989
+ key: staticKey.key,
1990
+ name: staticKey.name,
1991
+ scopes: staticKey.scopes,
1992
+ rateLimit: staticKey.rateLimit,
1993
+ expiresAt: staticKey.expiresAt,
1994
+ createdAt: /* @__PURE__ */ new Date(),
1995
+ metadata: staticKey.metadata,
1996
+ lastUsedAt: void 0
1997
+ // Explicitly undefined initially
1998
+ };
1999
+ this.staticKeysMap.set(staticKey.key, metadata);
2000
+ }
2001
+ console.log(`\u2705 Loaded ${this.staticKeysMap.size} static API keys`);
2002
+ }
2003
+ generateId(key) {
2004
+ return key.substring(0, 8);
2005
+ }
2006
+ compareKeys(a, b) {
2007
+ if (a.length !== b.length) return false;
2008
+ let result = 0;
2009
+ for (let i = 0; i < a.length; i++) {
2010
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
2011
+ }
2012
+ return result === 0;
2013
+ }
2014
+ isExpired(expiresAt) {
2015
+ if (!expiresAt) return false;
2016
+ return /* @__PURE__ */ new Date() > expiresAt;
2017
+ }
2018
+ async validate(key) {
2019
+ if (!key) return { isValid: false, error: "API key is required" };
2020
+ const cache = this.getCache();
2021
+ const cacheKey = `apikey:${key}`;
2022
+ if (cache) {
2023
+ try {
2024
+ const cached = await cache.get(cacheKey);
2025
+ if (cached) {
2026
+ const metadata = typeof cached === "string" ? JSON.parse(cached) : cached;
2027
+ return { isValid: true, key: metadata };
2028
+ }
2029
+ } catch (err) {
2030
+ }
2031
+ }
2032
+ for (const [staticKey, metadata] of this.staticKeysMap) {
2033
+ if (this.compareKeys(key, staticKey)) {
2034
+ if (this.isExpired(metadata.expiresAt)) {
2035
+ return { isValid: false, error: "API key has expired" };
2036
+ }
2037
+ metadata.lastUsedAt = /* @__PURE__ */ new Date();
2038
+ if (cache) {
2039
+ const ttl = this.config.cacheTtl ? Math.floor(this.config.cacheTtl / 1e3) : 300;
2040
+ try {
2041
+ await cache.set(cacheKey, JSON.stringify(metadata), ttl);
2042
+ } catch {
2043
+ }
2044
+ }
2045
+ return { isValid: true, key: metadata };
2046
+ }
2047
+ }
2048
+ return { isValid: false, error: "Invalid API key" };
2049
+ }
2050
+ hasScopes(key, requiredScopes) {
2051
+ if (!requiredScopes || requiredScopes.length === 0) return true;
2052
+ if (!key.scopes || key.scopes.length === 0) return false;
2053
+ for (const requiredScope of requiredScopes) {
2054
+ const hasScope = key.scopes.some((scope) => {
2055
+ if (scope.endsWith("*")) {
2056
+ const prefix = scope.slice(0, -1);
2057
+ return requiredScope.startsWith(prefix);
2058
+ }
2059
+ return scope === requiredScope;
2060
+ });
2061
+ if (!hasScope) return false;
2062
+ }
2063
+ return true;
2064
+ }
2065
+ };
2066
+ var ApiKeyFeature = class {
2067
+ name = "apiKey";
2068
+ store;
2069
+ config;
2070
+ constructor(config = {}) {
2071
+ this.config = {
2072
+ staticKeys: config.staticKeys || [],
2073
+ headerName: config.headerName || "X-API-Key",
2074
+ queryParamName: config.queryParamName || "api_key",
2075
+ extractStrategies: config.extractStrategies || ["header", "bearer"],
2076
+ // Defaults for other optional properties
2077
+ vaultService: void 0,
2078
+ customExtractor: void 0,
2079
+ enableCache: config.enableCache ?? true,
2080
+ cacheTtl: config.cacheTtl ?? 3e5,
2081
+ requireScopes: config.requireScopes ?? false,
2082
+ skipPaths: config.skipPaths || [],
2083
+ onError: config.onError,
2084
+ onValidated: config.onValidated
2085
+ };
2086
+ }
2087
+ async initialize(kernel) {
2088
+ this.store = new ApiKeyStore(this.config, kernel);
2089
+ const app = kernel.getApp();
2090
+ app.use("*", async (c, next) => {
2091
+ if (this.shouldSkipPath(c.req.path)) {
2092
+ await next();
2093
+ return;
2094
+ }
2095
+ const apiKey = this.extractApiKey(c);
2096
+ if (!apiKey) {
2097
+ c.set("apiKey", void 0);
2098
+ c.set("apiKeyScopes", void 0);
2099
+ await next();
2100
+ return;
2101
+ }
2102
+ const result = await this.store.validate(apiKey);
2103
+ if (!result.isValid) {
2104
+ throw new HTTPException7(401, { message: result.error || "Invalid API key" });
2105
+ }
2106
+ c.set("apiKey", result.key);
2107
+ c.set("apiKeyScopes", result.key?.scopes || []);
2108
+ c.set("hasScope", (scope) => this.store.hasScopes(result.key, [scope]));
2109
+ c.set("hasAnyScope", (...scopes) => scopes.some((s) => this.store.hasScopes(result.key, [s])));
2110
+ c.set("hasAllScopes", (...scopes) => this.store.hasScopes(result.key, scopes));
2111
+ await next();
2112
+ });
2113
+ console.log("\u2705 API Key feature initialized");
2114
+ }
2115
+ shouldSkipPath(path2) {
2116
+ if (!this.config.skipPaths?.length) return false;
2117
+ return this.config.skipPaths.some((skip) => {
2118
+ if (skip.endsWith("*")) return path2.startsWith(skip.slice(0, -1));
2119
+ return path2 === skip;
2120
+ });
2121
+ }
2122
+ extractApiKey(c) {
2123
+ for (const strategy of this.config.extractStrategies || []) {
2124
+ let key = null;
2125
+ switch (strategy) {
2126
+ case "header":
2127
+ key = c.req.header(this.config.headerName) || null;
2128
+ break;
2129
+ case "bearer": {
2130
+ const auth = c.req.header("Authorization");
2131
+ if (auth?.startsWith("Bearer ")) key = auth.substring(7);
2132
+ break;
2133
+ }
2134
+ case "query":
2135
+ key = c.req.query(this.config.queryParamName) || null;
2136
+ break;
2137
+ }
2138
+ if (key) return key;
2139
+ }
2140
+ return null;
2141
+ }
2142
+ };
2143
+ function requireApiKey() {
2144
+ return async (c, next) => {
2145
+ if (!c.get("apiKey")) throw new HTTPException7(401, { message: "API key is required" });
2146
+ await next();
2147
+ };
2148
+ }
2149
+ function requireScope(...scopes) {
2150
+ return async (c, next) => {
2151
+ const hasAll = c.get("hasAllScopes");
2152
+ if (!c.get("apiKey")) throw new HTTPException7(401, { message: "API key is required" });
2153
+ if (!hasAll || !hasAll(...scopes)) throw new HTTPException7(403, { message: "Insufficient scopes" });
2154
+ await next();
2155
+ };
2156
+ }
2157
+
2158
+ // src/features/csrf.ts
2159
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
2160
+ import { getCookie as getCookie2, setCookie as setCookie2 } from "hono/cookie";
2161
+ var CsrfFeature = class {
2162
+ name = "csrf";
2163
+ config;
2164
+ constructor(config) {
2165
+ if (!config.secret) throw new Error("CSRF secret is required");
2166
+ this.config = {
2167
+ secret: config.secret,
2168
+ cookieName: config.cookieName || "_csrf",
2169
+ headerName: config.headerName || "X-CSRF-Token",
2170
+ ignoreMethods: config.ignoreMethods || ["GET", "HEAD", "OPTIONS"],
2171
+ cookieOptions: {
2172
+ httpOnly: true,
2173
+ secure: true,
2174
+ sameSite: "Strict",
2175
+ maxAge: 86400,
2176
+ ...config.cookieOptions
2177
+ }
2178
+ };
2179
+ }
2180
+ async initialize(kernel) {
2181
+ const app = kernel.getApp();
2182
+ app.use("*", async (c, next) => {
2183
+ await this.middleware(c, next);
2184
+ });
2185
+ console.log("\u2705 CSRF feature initialized");
2186
+ }
2187
+ async middleware(c, next) {
2188
+ const method = c.req.method.toUpperCase();
2189
+ let token = getCookie2(c, this.config.cookieName);
2190
+ if (!token) {
2191
+ token = crypto.randomUUID().replace(/-/g, "");
2192
+ setCookie2(c, this.config.cookieName, token, {
2193
+ httpOnly: this.config.cookieOptions.httpOnly,
2194
+ secure: this.config.cookieOptions.secure,
2195
+ sameSite: this.config.cookieOptions.sameSite,
2196
+ maxAge: this.config.cookieOptions.maxAge,
2197
+ path: "/"
2198
+ });
2199
+ }
2200
+ c.set("csrfToken", token);
2201
+ if (this.config.ignoreMethods.includes(method)) {
2202
+ await next();
2203
+ return;
2204
+ }
2205
+ const isValid = await this.validateToken(c, token);
2206
+ if (!isValid) {
2207
+ throw new HTTPException8(403, { message: "Invalid CSRF token" });
2208
+ }
2209
+ await next();
2210
+ }
2211
+ async validateToken(c, expectedToken) {
2212
+ const headerToken = c.req.header(this.config.headerName);
2213
+ if (headerToken === expectedToken) return true;
2214
+ try {
2215
+ const contentType = c.req.header("content-type");
2216
+ if (contentType?.includes("application/x-www-form-urlencoded")) {
2217
+ const body = await c.req.parseBody();
2218
+ const bodyToken = body._csrf || body[this.config.cookieName];
2219
+ if (String(bodyToken) === expectedToken) return true;
2220
+ }
2221
+ } catch {
2222
+ }
2223
+ return false;
2224
+ }
2225
+ };
2226
+ function requireCsrf() {
2227
+ return async (c, next) => {
2228
+ if (!c.get("csrfToken")) throw new HTTPException8(403, { message: "CSRF token required" });
2229
+ await next();
2230
+ };
2231
+ }
2232
+
2233
+ // src/responses.ts
2234
+ import { ErrorCodes as ErrorCodes2 } from "@iskra-bun/core";
2235
+ function successResponse(data, message) {
2236
+ const response = { success: true };
2237
+ if (data !== void 0) response.data = data;
2238
+ if (message) response.message = message;
2239
+ return response;
2240
+ }
2241
+ function errorResponse(error, code, details) {
2242
+ const message = error instanceof Error ? error.message : error;
2243
+ const response = {
2244
+ success: false,
2245
+ error: message,
2246
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2247
+ };
2248
+ if (code) response.code = code;
2249
+ if (details !== void 0) response.details = details;
2250
+ return response;
2251
+ }
2252
+
2253
+ // src/features/validation.ts
2254
+ function createValidationMiddleware(schema, options = {}) {
2255
+ const { logErrors = true, status = 400 } = options;
2256
+ return async (c, next) => {
2257
+ try {
2258
+ const validated = {};
2259
+ if (schema.params) {
2260
+ const parsed = schema.params.safeParse(c.req.param());
2261
+ if (!parsed.success) {
2262
+ return c.json(errorResponse("Invalid route params", ErrorCodes2.VALIDATION_ERROR, parsed.error.flatten()), status);
2263
+ }
2264
+ validated.params = parsed.data;
2265
+ }
2266
+ if (schema.query) {
2267
+ const parsed = schema.query.safeParse(c.req.query());
2268
+ if (!parsed.success) {
2269
+ return c.json(errorResponse("Invalid query params", ErrorCodes2.VALIDATION_ERROR, parsed.error.flatten()), status);
2270
+ }
2271
+ validated.query = parsed.data;
2272
+ }
2273
+ if (schema.body) {
2274
+ let data = {};
2275
+ const contentType = c.req.header("content-type") || "";
2276
+ if (contentType.includes("application/json")) {
2277
+ data = await c.req.json().catch(() => ({}));
2278
+ } else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
2279
+ data = await c.req.parseBody();
2280
+ }
2281
+ const parsed = schema.body.safeParse(data);
2282
+ if (!parsed.success) {
2283
+ return c.json(errorResponse("Invalid body", ErrorCodes2.VALIDATION_ERROR, parsed.error.flatten()), status);
2284
+ }
2285
+ validated.body = parsed.data;
2286
+ }
2287
+ c.valid = () => validated;
2288
+ await next();
2289
+ } catch (err) {
2290
+ if (logErrors) console.error("Validation error:", err);
2291
+ return c.json(errorResponse("Validation middleware failed", ErrorCodes2.INTERNAL_ERROR), 500);
2292
+ }
2293
+ };
2294
+ }
2295
+ function extendHonoWithValidation(app) {
2296
+ const methods = ["get", "post", "put", "delete"];
2297
+ for (const method of methods) {
2298
+ app[`${method}Validated`] = function(path2, schema, handler, options) {
2299
+ const middleware = createValidationMiddleware(schema, options);
2300
+ this[method](path2, middleware, handler);
2301
+ return this;
2302
+ };
2303
+ }
2304
+ }
2305
+ var ValidationFeature = class {
2306
+ name = "validation";
2307
+ async initialize(kernel) {
2308
+ const app = kernel.getApp();
2309
+ extendHonoWithValidation(app);
2310
+ console.log("\u2705 Validation feature initialized");
2311
+ }
2312
+ };
2313
+
2314
+ // src/features/json-schema-validation.ts
2315
+ import Ajv from "ajv";
2316
+ import addErrors from "ajv-errors";
2317
+ import addFormats from "ajv-formats";
2318
+ function createAjvInstance(options = {}) {
2319
+ const ajv = new Ajv({
2320
+ allErrors: options.allErrors ?? true,
2321
+ coerceTypes: options.coerceTypes ?? true,
2322
+ verbose: true,
2323
+ $data: true
2324
+ });
2325
+ addFormats(ajv);
2326
+ addErrors(ajv);
2327
+ return ajv;
2328
+ }
2329
+ function formatAjvErrors(errors) {
2330
+ const result = { fields: {}, errors: [] };
2331
+ if (!errors) return result;
2332
+ for (const err of errors) {
2333
+ let fieldPath;
2334
+ if (err.instancePath) {
2335
+ fieldPath = err.instancePath.replace(/^\//, "").replace(/\//g, ".");
2336
+ } else if (err.params && "missingProperty" in err.params) {
2337
+ fieldPath = err.params.missingProperty;
2338
+ } else {
2339
+ fieldPath = "_root";
2340
+ }
2341
+ const message = err.message || "Invalid value";
2342
+ if (!result.fields[fieldPath]) {
2343
+ result.fields[fieldPath] = [];
2344
+ }
2345
+ if (!result.fields[fieldPath].includes(message)) {
2346
+ result.fields[fieldPath].push(message);
2347
+ }
2348
+ const formatted = fieldPath === "_root" ? message : `${fieldPath}: ${message}`;
2349
+ if (!result.errors.includes(formatted)) {
2350
+ result.errors.push(formatted);
2351
+ }
2352
+ }
2353
+ return result;
2354
+ }
2355
+ function createJsonSchemaValidationMiddleware(schema, options = {}) {
2356
+ const { logErrors = true, status = 400 } = options;
2357
+ const ajv = createAjvInstance(options);
2358
+ const validators = {
2359
+ params: schema.params ? ajv.compile(schema.params) : null,
2360
+ query: schema.query ? ajv.compile(schema.query) : null,
2361
+ body: schema.body ? ajv.compile(schema.body) : null
2362
+ };
2363
+ return async (c, next) => {
2364
+ try {
2365
+ const validated = {};
2366
+ if (validators.params) {
2367
+ const data = { ...c.req.param() };
2368
+ const valid = validators.params(data);
2369
+ if (!valid) {
2370
+ const details = formatAjvErrors(validators.params.errors);
2371
+ return c.json(errorResponse("Invalid route params", ErrorCodes2.VALIDATION_ERROR, details), status);
2372
+ }
2373
+ validated.params = data;
2374
+ }
2375
+ if (validators.query) {
2376
+ const data = { ...c.req.query() };
2377
+ const valid = validators.query(data);
2378
+ if (!valid) {
2379
+ const details = formatAjvErrors(validators.query.errors);
2380
+ return c.json(errorResponse("Invalid query params", ErrorCodes2.VALIDATION_ERROR, details), status);
2381
+ }
2382
+ validated.query = data;
2383
+ }
2384
+ if (validators.body) {
2385
+ let data = {};
2386
+ const contentType = c.req.header("content-type") || "";
2387
+ if (contentType.includes("application/json")) {
2388
+ data = await c.req.json().catch(() => ({}));
2389
+ } else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
2390
+ data = await c.req.parseBody();
2391
+ }
2392
+ const valid = validators.body(data);
2393
+ if (!valid) {
2394
+ const details = formatAjvErrors(validators.body.errors);
2395
+ return c.json(errorResponse("Invalid body", ErrorCodes2.VALIDATION_ERROR, details), status);
2396
+ }
2397
+ validated.body = data;
2398
+ }
2399
+ c.valid = () => validated;
2400
+ await next();
2401
+ } catch (err) {
2402
+ if (logErrors) console.error("JSON Schema validation error:", err);
2403
+ return c.json(errorResponse("Validation middleware failed", ErrorCodes2.INTERNAL_ERROR), 500);
2404
+ }
2405
+ };
2406
+ }
2407
+ function extendHonoWithJsonSchemaValidation(app) {
2408
+ const methods = ["get", "post", "put", "delete"];
2409
+ for (const method of methods) {
2410
+ app[`${method}JsonValidated`] = function(path2, schema, handler, options) {
2411
+ const middleware = createJsonSchemaValidationMiddleware(schema, options);
2412
+ this[method](path2, middleware, handler);
2413
+ return this;
2414
+ };
2415
+ }
2416
+ }
2417
+ var JsonSchemaValidationFeature = class {
2418
+ name = "json-schema-validation";
2419
+ async initialize(kernel) {
2420
+ const app = kernel.getApp();
2421
+ extendHonoWithJsonSchemaValidation(app);
2422
+ console.log("\u2705 JSON Schema validation feature initialized");
2423
+ }
2424
+ };
2425
+
2426
+ // src/features/openapi.ts
2427
+ import { OpenAPIHono as OpenAPIHono2, createRoute as createRoute2, z as z3 } from "@hono/zod-openapi";
2428
+ var OpenAPIFeature = class {
2429
+ name = "openapi";
2430
+ app;
2431
+ config;
2432
+ kernel;
2433
+ autoGeneratedRoutes = [];
2434
+ queuedRoutes = [];
2435
+ constructor(config) {
2436
+ this.config = config;
2437
+ }
2438
+ async initialize(kernel) {
2439
+ this.kernel = kernel;
2440
+ this.app = new OpenAPIHono2({
2441
+ defaultHook: (result, c) => {
2442
+ if (!result.success) {
2443
+ return c.json(errorResponse("Validation Error", ErrorCodes2.VALIDATION_ERROR, result.error.flatten()), 400);
2444
+ }
2445
+ }
2446
+ });
2447
+ this.processQueuedRoutes();
2448
+ console.log("\u2705 OpenAPIFeature initialized");
2449
+ }
2450
+ addRoute(route, handler) {
2451
+ if (!this.app) {
2452
+ this.queuedRoutes.push({ route, handler });
2453
+ return;
2454
+ }
2455
+ this.app.openapi(createRoute2(route), handler);
2456
+ this.autoGeneratedRoutes.push({
2457
+ path: route.path,
2458
+ method: route.method.toUpperCase(),
2459
+ tags: route.tags || [],
2460
+ summary: route.summary || ""
2461
+ });
2462
+ }
2463
+ processQueuedRoutes() {
2464
+ if (!this.app) return;
2465
+ for (const { route, handler } of this.queuedRoutes) {
2466
+ this.addRoute(route, handler);
2467
+ }
2468
+ this.queuedRoutes = [];
2469
+ }
2470
+ routes(app) {
2471
+ if (this.app) {
2472
+ app.route("/", this.app);
2473
+ }
2474
+ app.get("/openapi.json", (c) => {
2475
+ if (!this.app) return c.json({ error: "OpenAPI not initialized" }, 500);
2476
+ const spec = this.app.getOpenAPIDocument({
2477
+ openapi: "3.1.0",
2478
+ info: {
2479
+ title: this.config.title,
2480
+ version: this.config.version,
2481
+ description: this.config.description || "API Documentation",
2482
+ contact: this.config.contact,
2483
+ license: this.config.license
2484
+ },
2485
+ servers: this.config.servers || [{ url: "http://localhost:8000", description: "Dev Server" }],
2486
+ tags: this.config.tags || [],
2487
+ externalDocs: this.config.externalDocs,
2488
+ security: this.config.security
2489
+ });
2490
+ if (this.config.securitySchemes) {
2491
+ if (!spec.components) spec.components = {};
2492
+ spec.components.securitySchemes = this.config.securitySchemes;
2493
+ }
2494
+ return c.json(spec);
2495
+ });
2496
+ app.get("/docs", (c) => c.html(this.generateScalarHTML()));
2497
+ }
2498
+ generateScalarHTML() {
2499
+ return `
2500
+ <!DOCTYPE html>
2501
+ <html>
2502
+ <head>
2503
+ <title>${this.config.title} - API Documentation</title>
2504
+ <meta charset="utf-8" />
2505
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2506
+ <style>body { margin: 0; padding: 0; }</style>
2507
+ </head>
2508
+ <body>
2509
+ <script id="api-reference" data-url="/openapi.json" data-configuration='{"theme":"purple","layout":"modern","showSidebar":true}'></script>
2510
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest"></script>
2511
+ </body>
2512
+ </html>`.trim();
2513
+ }
2514
+ };
2515
+
2516
+ // src/features/upload/helper.ts
2517
+ var UploadHelper = class {
2518
+ constructor(storage, projectName) {
2519
+ this.storage = storage;
2520
+ this.projectName = projectName;
2521
+ this.basePath = `${projectName}/`;
2522
+ }
2523
+ storage;
2524
+ projectName;
2525
+ basePath;
2526
+ getBasePath() {
2527
+ return this.basePath;
2528
+ }
2529
+ buildPath(filename, subfolder) {
2530
+ if (subfolder) {
2531
+ const normalized = subfolder.replace(/^\/+|\/+$/g, "");
2532
+ return `${this.basePath}${normalized}/${filename}`;
2533
+ }
2534
+ return `${this.basePath}${filename}`;
2535
+ }
2536
+ async upload(filename, data, subfolder, options) {
2537
+ const fullPath = this.buildPath(filename, subfolder);
2538
+ const fileData = typeof data === "string" ? new TextEncoder().encode(data) : data;
2539
+ const putOptions = options ? {
2540
+ contentType: options.contentType,
2541
+ metadata: options.metadata
2542
+ } : void 0;
2543
+ await this.storage.put(fullPath, fileData, putOptions);
2544
+ return {
2545
+ path: fullPath,
2546
+ filename,
2547
+ size: fileData.length,
2548
+ uploadedAt: /* @__PURE__ */ new Date()
2549
+ };
2550
+ }
2551
+ // uploadFromRequest: Not implementing raw request reading here to avoid Node/Bun specific request stream issues unless necessary
2552
+ // or implementing simplified Version
2553
+ async uploadFromRequest(request, fieldName, subfolder, options) {
2554
+ const formData = await request.formData();
2555
+ const file = formData.get(fieldName);
2556
+ if (!file || !(file instanceof File)) throw new Error(`No file found in field: ${fieldName}`);
2557
+ const data = new Uint8Array(await file.arrayBuffer());
2558
+ const opts = { ...options, contentType: options?.contentType || file.type };
2559
+ return this.upload(file.name, data, subfolder, opts);
2560
+ }
2561
+ async list(subfolder) {
2562
+ const prefix = subfolder ? `${this.basePath}${subfolder.replace(/^\/+|\/+$/g, "")}/` : this.basePath;
2563
+ return await this.storage.list(prefix);
2564
+ }
2565
+ async get(filename, subfolder) {
2566
+ const fullPath = this.buildPath(filename, subfolder);
2567
+ return await this.storage.get(fullPath);
2568
+ }
2569
+ async delete(filename, subfolder) {
2570
+ const fullPath = this.buildPath(filename, subfolder);
2571
+ await this.storage.delete(fullPath);
2572
+ }
2573
+ async getUrl(filename, subfolder, expiresIn) {
2574
+ const fullPath = this.buildPath(filename, subfolder);
2575
+ return await this.storage.url(fullPath, expiresIn);
2576
+ }
2577
+ };
2578
+
2579
+ // src/features/upload/index.ts
2580
+ var UploadFeature = class {
2581
+ name = "upload";
2582
+ dependencies = ["storage"];
2583
+ helper;
2584
+ config;
2585
+ constructor(config) {
2586
+ this.config = {
2587
+ projectName: config.projectName,
2588
+ maxFileSize: config.maxFileSize || 10 * 1024 * 1024,
2589
+ allowedExtensions: config.allowedExtensions || [],
2590
+ exposeRoutes: config.exposeRoutes || false,
2591
+ routePrefix: config.routePrefix || "/upload"
2592
+ };
2593
+ }
2594
+ async initialize(kernel) {
2595
+ const storageFeature = kernel.getFeature("storage");
2596
+ if (!storageFeature) throw new Error("Upload feature requires storage feature");
2597
+ const storage = storageFeature.getAdapter();
2598
+ if (!storage) throw new Error("Storage adapter not ready");
2599
+ this.helper = new UploadHelper(storage, this.config.projectName);
2600
+ const app = kernel.getApp();
2601
+ app.use("*", async (c, next) => {
2602
+ c.set("upload", this.helper);
2603
+ await next();
2604
+ });
2605
+ console.log(`\u2705 Upload feature initialized: ${this.config.projectName}`);
2606
+ }
2607
+ routes(app) {
2608
+ if (!this.config.exposeRoutes) return;
2609
+ const prefix = this.config.routePrefix;
2610
+ app.post(`${prefix}`, async (c) => {
2611
+ const upload = c.get("upload");
2612
+ try {
2613
+ const subfolder = c.req.query("subfolder");
2614
+ const formData = await c.req.formData();
2615
+ const file = formData.get("file");
2616
+ if (!file || !(file instanceof File)) return c.json({ error: "No file" }, 400);
2617
+ if (file.size > this.config.maxFileSize) return c.json({ error: "File too large" }, 400);
2618
+ if (this.config.allowedExtensions.length > 0) {
2619
+ if (!this.config.allowedExtensions.some((e) => file.name.toLowerCase().endsWith(e))) {
2620
+ return c.json({ error: "Invalid extension" }, 400);
2621
+ }
2622
+ }
2623
+ const data = new Uint8Array(await file.arrayBuffer());
2624
+ const result = await upload.upload(file.name, data, subfolder, { contentType: file.type });
2625
+ return c.json({ success: true, ...result });
2626
+ } catch (e) {
2627
+ return c.json({ error: e.message }, 500);
2628
+ }
2629
+ });
2630
+ app.get(`${prefix}`, async (c) => {
2631
+ const upload = c.get("upload");
2632
+ try {
2633
+ const subfolder = c.req.query("subfolder");
2634
+ const files = await upload.list(subfolder);
2635
+ return c.json({ success: true, files });
2636
+ } catch (e) {
2637
+ return c.json({ error: e.message }, 500);
2638
+ }
2639
+ });
2640
+ app.get(`${prefix}/*`, async (c) => {
2641
+ const upload = c.get("upload");
2642
+ try {
2643
+ const filePath = c.req.path.replace(`${prefix}/`, "");
2644
+ const segments = filePath.split("/");
2645
+ const filename = segments.pop() || "";
2646
+ const subfolder = segments.length > 0 ? segments.join("/") : void 0;
2647
+ const data = await upload.get(filename, subfolder);
2648
+ if (!data) {
2649
+ return c.json({ error: "File not found" }, 404);
2650
+ }
2651
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
2652
+ const mimeTypes = {
2653
+ jpg: "image/jpeg",
2654
+ jpeg: "image/jpeg",
2655
+ png: "image/png",
2656
+ gif: "image/gif",
2657
+ pdf: "application/pdf",
2658
+ txt: "text/plain",
2659
+ json: "application/json",
2660
+ zip: "application/zip"
2661
+ };
2662
+ const contentType = mimeTypes[ext] || "application/octet-stream";
2663
+ const body = new Uint8Array(data.length);
2664
+ body.set(data);
2665
+ return new Response(body, {
2666
+ headers: {
2667
+ "Content-Type": contentType,
2668
+ "Content-Disposition": `inline; filename="${filename}"`,
2669
+ "Content-Length": String(data.length)
2670
+ }
2671
+ });
2672
+ } catch (e) {
2673
+ return c.json({ error: e.message }, 500);
2674
+ }
2675
+ });
2676
+ app.delete(`${prefix}/*`, async (c) => {
2677
+ const upload = c.get("upload");
2678
+ try {
2679
+ const filePath = c.req.path.replace(`${prefix}/`, "");
2680
+ const segments = filePath.split("/");
2681
+ const filename = segments.pop() || "";
2682
+ const subfolder = segments.length > 0 ? segments.join("/") : void 0;
2683
+ await upload.delete(filename, subfolder);
2684
+ return new Response(null, { status: 204 });
2685
+ } catch (e) {
2686
+ return c.json({ error: e.message }, 500);
2687
+ }
2688
+ });
2689
+ }
2690
+ };
2691
+
2692
+ // src/features/tracing.ts
2693
+ import { httpInstrumentationMiddleware } from "@hono/otel";
2694
+ var OtelTracingFeature = class {
2695
+ name = "otel-tracing";
2696
+ config;
2697
+ constructor(config) {
2698
+ if (!config.serviceName) {
2699
+ throw new Error("serviceName is required for OtelTracingFeature");
2700
+ }
2701
+ this.config = config;
2702
+ }
2703
+ async initialize(kernel) {
2704
+ const app = kernel.getApp();
2705
+ const instrumentationConfig = {
2706
+ serviceName: this.config.serviceName
2707
+ };
2708
+ if (this.config.serviceVersion) instrumentationConfig.serviceVersion = this.config.serviceVersion;
2709
+ if (this.config.captureRequestHeaders) instrumentationConfig.captureRequestHeaders = this.config.captureRequestHeaders;
2710
+ if (this.config.captureResponseHeaders) instrumentationConfig.captureResponseHeaders = this.config.captureResponseHeaders;
2711
+ if (this.config.tracerProvider) instrumentationConfig.tracerProvider = this.config.tracerProvider;
2712
+ if (this.config.meterProvider) instrumentationConfig.meterProvider = this.config.meterProvider;
2713
+ if (this.config.tracer) instrumentationConfig.tracer = this.config.tracer;
2714
+ if (this.config.spanNameFactory) instrumentationConfig.spanNameFactory = this.config.spanNameFactory;
2715
+ if (this.config.getTime) instrumentationConfig.getTime = this.config.getTime;
2716
+ app.use("*", httpInstrumentationMiddleware(instrumentationConfig));
2717
+ console.log("\u2705 OpenTelemetry tracing feature initialized");
2718
+ }
2719
+ };
2720
+
2721
+ // src/features/email/index.ts
2722
+ var MockEmailAdapter = class {
2723
+ async send(message) {
2724
+ console.log("\u{1F4E7} [MOCK] Sent to", message.to, "Subject:", message.subject);
2725
+ return { messageId: `mock-${Date.now()}`, success: true };
2726
+ }
2727
+ async sendTemplate(name, to, data) {
2728
+ return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });
2729
+ }
2730
+ };
2731
+ var EmailFeature = class {
2732
+ constructor(config) {
2733
+ this.config = config;
2734
+ }
2735
+ config;
2736
+ name = "email";
2737
+ adapter;
2738
+ async initialize(kernel) {
2739
+ this.adapter = await this.createAdapter(this.config);
2740
+ const app = kernel.getApp();
2741
+ app.use("*", async (c, next) => {
2742
+ if (this.adapter) c.set("email", this.adapter);
2743
+ await next();
2744
+ });
2745
+ console.log(`\u2705 EmailFeature initialized (${this.config.provider})`);
2746
+ }
2747
+ async createAdapter(config) {
2748
+ switch (config.provider) {
2749
+ case "mock":
2750
+ return new MockEmailAdapter();
2751
+ case "smtp": {
2752
+ const { SmtpEmailAdapter } = await import("./smtp-WJDLYKD5.js");
2753
+ return new SmtpEmailAdapter(config);
2754
+ }
2755
+ case "sendgrid": {
2756
+ const { SendGridEmailAdapter } = await import("./sendgrid-UK2GSBEF.js");
2757
+ return new SendGridEmailAdapter(config);
2758
+ }
2759
+ case "mailgun": {
2760
+ const { MailgunEmailAdapter } = await import("./mailgun-Z46GZJNI.js");
2761
+ return new MailgunEmailAdapter(config);
2762
+ }
2763
+ default:
2764
+ throw new Error(`Provider ${config.provider} not implemented`);
2765
+ }
2766
+ }
2767
+ getAdapter() {
2768
+ if (!this.adapter) throw new Error("Email not initialized");
2769
+ return this.adapter;
2770
+ }
2771
+ };
2772
+ export {
2773
+ ApiKeyFeature,
2774
+ ApiKeyStore,
2775
+ AuthError,
2776
+ AuthFeature,
2777
+ BaseStorageAdapter,
2778
+ CacheFeature,
2779
+ ConflictError,
2780
+ CorsFeature,
2781
+ CsrfFeature,
2782
+ DbFeature,
2783
+ EmailFeature,
2784
+ ErrorCodes2 as ErrorCodes,
2785
+ ErrorHandlerFeature,
2786
+ ForbiddenError,
2787
+ HealthCheckFeature,
2788
+ HttpError,
2789
+ JsonSchemaValidationFeature,
2790
+ Kernel,
2791
+ LocalStorageAdapter,
2792
+ LoggerFeature,
2793
+ MockEmailAdapter,
2794
+ NotFoundError,
2795
+ OpenAPIFeature,
2796
+ OpenAPIHono2 as OpenAPIHono,
2797
+ OtelTracingFeature,
2798
+ PermissionsFeature,
2799
+ RateLimitFeature,
2800
+ RequestIdFeature,
2801
+ SessionFeature,
2802
+ StorageFeature,
2803
+ UploadFeature,
2804
+ ValidationError,
2805
+ ValidationFeature,
2806
+ WebDriver,
2807
+ WebPlugin,
2808
+ createHttpError,
2809
+ createJsonSchemaValidationMiddleware,
2810
+ createRoute2 as createRoute,
2811
+ createRouter,
2812
+ createValidationMiddleware,
2813
+ errorResponse,
2814
+ formatAjvErrors,
2815
+ requireApiKey,
2816
+ requireAuth,
2817
+ requireCsrf,
2818
+ requirePermission,
2819
+ requireRole,
2820
+ requireScope,
2821
+ successResponse,
2822
+ z3 as z
2823
+ };
2824
+ //# sourceMappingURL=index.js.map