@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/src/kernel.ts ADDED
@@ -0,0 +1,257 @@
1
+ import { Hono } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import type { Context, Next } from "hono";
4
+ import type { Feature, KernelConfig } from "./types";
5
+
6
+ /**
7
+ * Core microkernel orchestrator that manages features, dependencies, and application lifecycle.
8
+ */
9
+ export class Kernel {
10
+ private app: Hono;
11
+ private config: KernelConfig;
12
+ private features: Map<string, Feature> = new Map();
13
+ private initialized = false;
14
+
15
+ constructor(config: KernelConfig = {}) {
16
+ this.config = {
17
+ port: 8000,
18
+ hostname: "localhost",
19
+ ...config,
20
+ };
21
+ this.app = new Hono();
22
+
23
+ // Add default error handler for HTTPException
24
+ this.app.onError((err: Error, c: Context): Response | Promise<Response> => {
25
+ if (err instanceof HTTPException) {
26
+ return c.json(
27
+ { message: err.message },
28
+ err.status,
29
+ );
30
+ }
31
+
32
+ console.error("Unhandled error:", err);
33
+ return c.json(
34
+ { message: "Internal Server Error" },
35
+ 500,
36
+ );
37
+ });
38
+ }
39
+
40
+ async initialize(): Promise<void> {
41
+ if (this.initialized) {
42
+ throw new Error("Kernel already initialized");
43
+ }
44
+
45
+ console.log("🚀 Initializing Web-Kit Kernel...");
46
+
47
+ this.validateFeatureDependencies();
48
+ await this.validatePeerDependencies();
49
+ this.applySecurityHeaders();
50
+
51
+ const orderedFeatures = this.sortFeaturesByDependencies();
52
+ for (const feature of orderedFeatures) {
53
+ await this.initializeFeature(feature);
54
+ }
55
+
56
+ this.initialized = true;
57
+ console.log("✅ Web-Kit Kernel initialized");
58
+ }
59
+
60
+ private applySecurityHeaders(): void {
61
+ if (!this.config.securityHeaders) {
62
+ this.config.securityHeaders = {
63
+ xFrameOptions: "SAMEORIGIN",
64
+ xContentTypeOptions: true,
65
+ xXssProtection: true,
66
+ referrerPolicy: "strict-origin-when-cross-origin",
67
+ };
68
+ }
69
+
70
+ const headers = this.config.securityHeaders;
71
+
72
+ this.app.use("*", async (c: Context, next: Next) => {
73
+ await next();
74
+
75
+ if (headers.xFrameOptions) {
76
+ c.res.headers.set("X-Frame-Options", headers.xFrameOptions);
77
+ }
78
+ if (headers.xContentTypeOptions) {
79
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
80
+ }
81
+ if (headers.xXssProtection) {
82
+ c.res.headers.set("X-XSS-Protection", "1; mode=block");
83
+ }
84
+ if (headers.referrerPolicy) {
85
+ c.res.headers.set("Referrer-Policy", headers.referrerPolicy);
86
+ }
87
+ if (headers.strictTransportSecurity) {
88
+ const hsts = headers.strictTransportSecurity;
89
+ let hstsValue = `max-age=${hsts.maxAge || 31536000}`;
90
+ if (hsts.includeSubDomains) hstsValue += "; includeSubDomains";
91
+ if (hsts.preload) hstsValue += "; preload";
92
+ c.res.headers.set("Strict-Transport-Security", hstsValue);
93
+ }
94
+ if (headers.contentSecurityPolicy) {
95
+ if (typeof headers.contentSecurityPolicy === "string") {
96
+ c.res.headers.set(
97
+ "Content-Security-Policy",
98
+ headers.contentSecurityPolicy,
99
+ );
100
+ } else if (headers.contentSecurityPolicy.directives) {
101
+ const directives = Object.entries(
102
+ headers.contentSecurityPolicy.directives,
103
+ )
104
+ .map(([key, value]) => {
105
+ const values = Array.isArray(value) ? value.join(" ") : value;
106
+ return `${key} ${values}`;
107
+ })
108
+ .join("; ");
109
+ c.res.headers.set("Content-Security-Policy", directives);
110
+ }
111
+ }
112
+ if (headers.permissionsPolicy) {
113
+ const policy = Object.entries(headers.permissionsPolicy)
114
+ .map(([key, value]) => `${key}=(${value.join(" ")})`)
115
+ .join(", ");
116
+ c.res.headers.set("Permissions-Policy", policy);
117
+ }
118
+ });
119
+ }
120
+
121
+ registerFeature(feature: Feature): void {
122
+ if (this.initialized) {
123
+ throw new Error("Cannot register features after initialization");
124
+ }
125
+
126
+ if (feature.peerDependencies) {
127
+ for (const dep of feature.peerDependencies) {
128
+ try {
129
+ import(dep);
130
+ } catch {
131
+ console.warn(
132
+ `⚠️ Warning: Feature '${feature.name}' requires peer dependency: ${dep}`,
133
+ );
134
+ }
135
+ }
136
+ }
137
+
138
+ this.features.set(feature.name, feature);
139
+ console.log(`📦 Registered feature: ${feature.name}`);
140
+ }
141
+
142
+ private validateFeatureDependencies(): void {
143
+ for (const [name, feature] of this.features) {
144
+ if (feature.dependencies) {
145
+ for (const dep of feature.dependencies) {
146
+ if (!this.features.has(dep)) {
147
+ throw new Error(
148
+ `Feature '${name}' requires feature '${dep}' which is not registered`,
149
+ );
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ private async validatePeerDependencies(): Promise<void> {
157
+ for (const [featureName, feature] of this.features) {
158
+ if (feature.peerDependencies) {
159
+ for (const dep of feature.peerDependencies) {
160
+ // In Node/Bun dynamic import usually works for installed packages
161
+ // We can skip hard failure here or make it safer
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ private sortFeaturesByDependencies(): Feature[] {
168
+ const sorted: Feature[] = [];
169
+ const visited = new Set<string>();
170
+ const visiting = new Set<string>();
171
+
172
+ const visit = (name: string) => {
173
+ if (visited.has(name)) return;
174
+ if (visiting.has(name)) {
175
+ throw new Error(`Circular dependency detected for feature: ${name}`);
176
+ }
177
+
178
+ visiting.add(name);
179
+ const feature = this.features.get(name)!;
180
+
181
+ if (feature.dependencies) {
182
+ for (const dep of feature.dependencies) {
183
+ visit(dep);
184
+ }
185
+ }
186
+
187
+ visiting.delete(name);
188
+ visited.add(name);
189
+ sorted.push(feature);
190
+ };
191
+
192
+ for (const name of this.features.keys()) {
193
+ visit(name);
194
+ }
195
+
196
+ return sorted;
197
+ }
198
+
199
+ private async initializeFeature(feature: Feature): Promise<void> {
200
+ console.log(`⚙️ Initializing feature: ${feature.name}`);
201
+ await feature.initialize(this);
202
+
203
+ if (feature.routes) {
204
+ feature.routes(this.app);
205
+ }
206
+ }
207
+
208
+ getApp(): Hono {
209
+ return this.app;
210
+ }
211
+
212
+ getFeature<T extends Feature>(name: string): T | undefined {
213
+ return this.features.get(name) as T;
214
+ }
215
+
216
+ private server: any;
217
+
218
+ async start(): Promise<void> {
219
+ if (!this.initialized) {
220
+ await this.initialize();
221
+ }
222
+
223
+ // Iskra uses Bun, so we can use Bun.serve
224
+ // Hono handles this automatically if using the right adapter or just passing app.fetch to Bun.serve
225
+ // But let's assume standard Bun usage from the user
226
+ console.log(
227
+ `🌐 Server running at http://${this.config.hostname}:${this.config.port}`,
228
+ );
229
+
230
+ if (typeof Bun !== "undefined") {
231
+ this.server = Bun.serve({
232
+ port: this.config.port,
233
+ hostname: this.config.hostname,
234
+ fetch: this.app.fetch,
235
+ });
236
+ } else {
237
+ console.warn("Not running in Bun, start() might strictly need an adapter.");
238
+ }
239
+ }
240
+
241
+ async shutdown(): Promise<void> {
242
+ console.log("🛑 Shutting down...");
243
+
244
+ if (this.server) {
245
+ this.server.stop();
246
+ this.server = null;
247
+ }
248
+
249
+ const features = Array.from(this.features.values()).reverse();
250
+ for (const feature of features) {
251
+ if (feature.shutdown) {
252
+ await feature.shutdown();
253
+ }
254
+ }
255
+ console.log("👋 Server shut down gracefully");
256
+ }
257
+ }
@@ -0,0 +1,37 @@
1
+ export interface SuccessResponse<T = unknown> {
2
+ success: true;
3
+ data?: T;
4
+ message?: string;
5
+ }
6
+
7
+ export interface ErrorResponse {
8
+ success: false;
9
+ error: string;
10
+ code?: string;
11
+ details?: unknown;
12
+ timestamp?: string;
13
+ }
14
+
15
+ export type ApiResponse<T = unknown> = SuccessResponse<T> | ErrorResponse;
16
+
17
+ export function successResponse<T = unknown>(data?: T, message?: string): SuccessResponse<T> {
18
+ const response: SuccessResponse<T> = { success: true };
19
+ if (data !== undefined) response.data = data;
20
+ if (message) response.message = message;
21
+ return response;
22
+ }
23
+
24
+ export function errorResponse(error: string | Error, code?: string, details?: unknown): ErrorResponse {
25
+ const message = error instanceof Error ? error.message : error;
26
+ const response: ErrorResponse = {
27
+ success: false,
28
+ error: message,
29
+ timestamp: new Date().toISOString(),
30
+ };
31
+ if (code) response.code = code;
32
+ if (details !== undefined) response.details = details;
33
+ return response;
34
+ }
35
+
36
+ // Re-exportado desde @iskra-bun/core para compatibilidad
37
+ export { ErrorCodes, type ErrorCode } from '@iskra-bun/core';
package/src/router.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Hono, Context } from 'hono';
2
+ import { z } from 'zod';
3
+ import { zValidator } from '@hono/zod-validator';
4
+ import type { App } from '@iskra-bun/core';
5
+ import type { Driver } from '@iskra-bun/core';
6
+
7
+ export interface RouteOptions<B = any, Q = any> {
8
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
9
+ path: string;
10
+ schema?: {
11
+ body?: z.ZodType<B>;
12
+ query?: z.ZodType<Q>;
13
+ params?: z.ZodType;
14
+ };
15
+ handler: (ctx: WebContext<B, Q>) => Promise<any> | any;
16
+ doc?: {
17
+ summary?: string;
18
+ tags?: string[];
19
+ description?: string;
20
+ };
21
+ }
22
+
23
+ export interface WebContext<B = any, Q = any> {
24
+ raw: Context;
25
+ body: B;
26
+ query: Q;
27
+ params: Record<string, string>;
28
+ app: App;
29
+ }
30
+
31
+ export const createRouter = (routes: RouteOptions[]) => routes;
package/src/server.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { OpenAPIHono, createRoute } from '@hono/zod-openapi';
2
+ import { z } from 'zod';
3
+ import type { Driver, App } from '@iskra-bun/core';
4
+ import type { RouteOptions } from './router';
5
+
6
+ export interface WebServerOptions {
7
+ port?: number;
8
+ routes?: RouteOptions[];
9
+ debug?: boolean;
10
+ openApi?: {
11
+ path: string;
12
+ title: string;
13
+ version: string;
14
+ };
15
+ }
16
+
17
+ export class WebDriver implements Driver {
18
+ name = 'WebDriver';
19
+ private app: App | null = null;
20
+ private server: OpenAPIHono;
21
+ private options: WebServerOptions;
22
+ private runningServer: any;
23
+
24
+ constructor(options: WebServerOptions = {}) {
25
+ this.options = options;
26
+ this.server = new OpenAPIHono();
27
+ }
28
+
29
+ init(app: App) {
30
+ this.app = app;
31
+ this.setupRoutes();
32
+ this.setupOpenApi();
33
+ }
34
+
35
+ private setupOpenApi() {
36
+ if (this.options.openApi) {
37
+ this.server.doc(this.options.openApi.path, {
38
+ openapi: '3.0.0',
39
+ info: {
40
+ version: this.options.openApi.version,
41
+ title: this.options.openApi.title,
42
+ },
43
+ });
44
+ }
45
+ }
46
+
47
+ private setupRoutes() {
48
+ if (!this.options.routes) return;
49
+
50
+ for (const route of this.options.routes) {
51
+ // Map simple RouteOptions to OpenAPI RouteConfig
52
+ // We assume JSON for body
53
+
54
+ const routeConfig: any = {
55
+ method: route.method.toLowerCase(),
56
+ path: route.path,
57
+ tags: route.doc?.tags,
58
+ summary: route.doc?.summary,
59
+ description: route.doc?.description,
60
+ request: {},
61
+ responses: {
62
+ 200: {
63
+ description: 'Successful response',
64
+ content: {
65
+ 'application/json': {
66
+ schema: z.any() // We don't enforce response schema yet
67
+ }
68
+ }
69
+ },
70
+ 500: {
71
+ description: 'Internal Server Error'
72
+ }
73
+ }
74
+ };
75
+
76
+ if (route.schema?.body) {
77
+ routeConfig.request.body = {
78
+ content: {
79
+ 'application/json': {
80
+ schema: route.schema.body
81
+ }
82
+ }
83
+ };
84
+ }
85
+ if (route.schema?.query) {
86
+ routeConfig.request.query = route.schema.query;
87
+ }
88
+ if (route.schema?.params) {
89
+ routeConfig.request.params = route.schema.params;
90
+ }
91
+
92
+ const openApiRoute = createRoute(routeConfig);
93
+
94
+ this.server.openapi(openApiRoute, async (c) => {
95
+ if (!this.app) throw new Error('App not initialized');
96
+
97
+ const webCtx = {
98
+ raw: c,
99
+ // Hono/zod-openapi puts validated data in c.req.valid('json') etc similar to validator middleware
100
+ body: route.schema?.body ? (c as any).req.valid('json') : undefined,
101
+ query: route.schema?.query ? (c as any).req.valid('query') : undefined,
102
+ params: c.req.param(),
103
+ app: this.app
104
+ };
105
+
106
+ try {
107
+ const result = await route.handler(webCtx);
108
+ if (result instanceof Response) return result;
109
+ return c.json(result);
110
+ } catch (err: any) {
111
+ this.app.logger.error(err);
112
+ return c.json({ error: err.message }, 500);
113
+ }
114
+ });
115
+ }
116
+ }
117
+
118
+ start() {
119
+ const port = this.options.port || 3000;
120
+ this.app?.logger.info(`Starting WebServer on port ${port}...`);
121
+
122
+ // Bun.serve works with OpenAPIHono just like Hono
123
+ this.runningServer = Bun.serve({
124
+ fetch: this.server.fetch,
125
+ port
126
+ });
127
+ }
128
+
129
+ stop() {
130
+ if (this.runningServer) {
131
+ this.runningServer.stop();
132
+ }
133
+ this.app?.logger.info('WebServer stopped');
134
+ }
135
+ }