@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.
- package/CHANGELOG.md +7 -0
- package/README.md +31 -0
- package/dist/chunk-POXNRNTC.js +51 -0
- package/dist/chunk-POXNRNTC.js.map +1 -0
- package/dist/index.d.ts +966 -0
- package/dist/index.js +2824 -0
- package/dist/index.js.map +1 -0
- package/dist/mailgun-Z46GZJNI.js +83 -0
- package/dist/mailgun-Z46GZJNI.js.map +1 -0
- package/dist/s3-7IG4ESFW.js +171 -0
- package/dist/s3-7IG4ESFW.js.map +1 -0
- package/dist/sendgrid-UK2GSBEF.js +43 -0
- package/dist/sendgrid-UK2GSBEF.js.map +1 -0
- package/dist/smtp-WJDLYKD5.js +50 -0
- package/dist/smtp-WJDLYKD5.js.map +1 -0
- package/package.json +74 -0
- package/src/driver.ts +55 -0
- package/src/errors.ts +66 -0
- package/src/features/api-key.ts +243 -0
- package/src/features/auth/better-auth-config.ts +160 -0
- package/src/features/auth/index.ts +229 -0
- package/src/features/auth/schema.ts +174 -0
- package/src/features/auth/types.ts +114 -0
- package/src/features/cache.ts +144 -0
- package/src/features/cors.ts +33 -0
- package/src/features/csrf.ts +94 -0
- package/src/features/db.ts +90 -0
- package/src/features/email/index.ts +103 -0
- package/src/features/email/providers/mailgun.ts +99 -0
- package/src/features/email/providers/sendgrid.ts +42 -0
- package/src/features/email/providers/smtp.ts +51 -0
- package/src/features/error-handler.ts +147 -0
- package/src/features/health.ts +94 -0
- package/src/features/json-schema-validation.ts +186 -0
- package/src/features/logger.ts +70 -0
- package/src/features/openapi.ts +107 -0
- package/src/features/permissions.ts +128 -0
- package/src/features/rate-limit.ts +173 -0
- package/src/features/request-id.ts +45 -0
- package/src/features/session.ts +322 -0
- package/src/features/storage/adapters/local.ts +133 -0
- package/src/features/storage/adapters/s3.ts +193 -0
- package/src/features/storage/base.ts +112 -0
- package/src/features/storage/index.ts +53 -0
- package/src/features/tracing.ts +49 -0
- package/src/features/upload/helper.ts +85 -0
- package/src/features/upload/index.ts +140 -0
- package/src/features/validation.ts +105 -0
- package/src/index.ts +29 -0
- package/src/kernel.ts +257 -0
- package/src/responses.ts +37 -0
- package/src/router.ts +31 -0
- package/src/server.ts +135 -0
- package/src/types.ts +272 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Feature } from "../../types";
|
|
2
|
+
import type { Kernel } from "../../kernel";
|
|
3
|
+
import type { Context, Next } from "hono";
|
|
4
|
+
|
|
5
|
+
export interface EmailConfig {
|
|
6
|
+
provider: "smtp" | "sendgrid" | "mock" | "mailgun" | "ses";
|
|
7
|
+
smtp?: {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
secure?: boolean;
|
|
13
|
+
};
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
apiSecret?: string;
|
|
16
|
+
region?: string;
|
|
17
|
+
domain?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
from?: { name?: string; email: string };
|
|
20
|
+
templateDir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface EmailMessage {
|
|
24
|
+
to: string | string[];
|
|
25
|
+
from?: { name?: string; email: string };
|
|
26
|
+
subject: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
html?: string;
|
|
29
|
+
cc?: string | string[];
|
|
30
|
+
bcc?: string | string[];
|
|
31
|
+
replyTo?: string;
|
|
32
|
+
attachments?: Array<{ filename: string; content: Uint8Array | string; contentType?: string }>;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TemplateData {
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EmailAdapter {
|
|
41
|
+
send(message: EmailMessage): Promise<{ messageId: string; success: boolean }>;
|
|
42
|
+
sendTemplate(templateName: string, to: string | string[], data: TemplateData): Promise<{ messageId: string; success: boolean }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class MockEmailAdapter implements EmailAdapter {
|
|
46
|
+
async send(message: EmailMessage) {
|
|
47
|
+
console.log("📧 [MOCK] Sent to", message.to, "Subject:", message.subject);
|
|
48
|
+
return { messageId: `mock-${Date.now()}`, success: true };
|
|
49
|
+
}
|
|
50
|
+
async sendTemplate(name: string, to: string | string[], data: TemplateData) {
|
|
51
|
+
return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare module "hono" {
|
|
56
|
+
interface ContextVariableMap {
|
|
57
|
+
email: EmailAdapter;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class EmailFeature implements Feature {
|
|
62
|
+
name = "email";
|
|
63
|
+
private adapter?: EmailAdapter;
|
|
64
|
+
|
|
65
|
+
constructor(private config: EmailConfig) { }
|
|
66
|
+
|
|
67
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
68
|
+
this.adapter = await this.createAdapter(this.config);
|
|
69
|
+
const app = kernel.getApp();
|
|
70
|
+
app.use("*", async (c: Context, next: Next) => {
|
|
71
|
+
if (this.adapter) c.set("email", this.adapter);
|
|
72
|
+
await next();
|
|
73
|
+
});
|
|
74
|
+
console.log(`✅ EmailFeature initialized (${this.config.provider})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async createAdapter(config: EmailConfig): Promise<EmailAdapter> {
|
|
78
|
+
switch (config.provider) {
|
|
79
|
+
case "mock": return new MockEmailAdapter();
|
|
80
|
+
case "smtp": {
|
|
81
|
+
// Lazy load to avoid import issues if not installed/configured in all envs (though we added deps)
|
|
82
|
+
const { SmtpEmailAdapter } = await import("./providers/smtp");
|
|
83
|
+
return new SmtpEmailAdapter(config);
|
|
84
|
+
}
|
|
85
|
+
case "sendgrid": {
|
|
86
|
+
const { SendGridEmailAdapter } = await import("./providers/sendgrid");
|
|
87
|
+
return new SendGridEmailAdapter(config);
|
|
88
|
+
}
|
|
89
|
+
case "mailgun": {
|
|
90
|
+
const { MailgunEmailAdapter } = await import("./providers/mailgun");
|
|
91
|
+
return new MailgunEmailAdapter(config);
|
|
92
|
+
}
|
|
93
|
+
default: throw new Error(`Provider ${config.provider} not implemented`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getAdapter(): EmailAdapter {
|
|
98
|
+
if (!this.adapter) throw new Error("Email not initialized");
|
|
99
|
+
return this.adapter;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { MockEmailAdapter };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailMessage, EmailConfig, TemplateData } from "../index";
|
|
2
|
+
|
|
3
|
+
export class MailgunEmailAdapter implements EmailAdapter {
|
|
4
|
+
private apiKey: string;
|
|
5
|
+
private domain: string;
|
|
6
|
+
private baseUrl: string;
|
|
7
|
+
private defaultFrom?: { name?: string; email: string };
|
|
8
|
+
|
|
9
|
+
constructor(config: EmailConfig) {
|
|
10
|
+
if (!config.apiKey) throw new Error("Mailgun requires apiKey");
|
|
11
|
+
if (!config.domain) throw new Error("Mailgun requires domain");
|
|
12
|
+
|
|
13
|
+
this.apiKey = config.apiKey;
|
|
14
|
+
this.domain = config.domain;
|
|
15
|
+
this.baseUrl = config.baseUrl || "https://api.mailgun.net/v3";
|
|
16
|
+
this.defaultFrom = config.from;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async send(message: EmailMessage): Promise<{ messageId: string; success: boolean }> {
|
|
20
|
+
const form = new FormData();
|
|
21
|
+
|
|
22
|
+
const from = message.from || this.defaultFrom;
|
|
23
|
+
if (from) {
|
|
24
|
+
form.append("from", from.name ? `${from.name} <${from.email}>` : from.email);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const to = Array.isArray(message.to) ? message.to.join(",") : message.to;
|
|
28
|
+
form.append("to", to);
|
|
29
|
+
form.append("subject", message.subject);
|
|
30
|
+
|
|
31
|
+
if (message.text) form.append("text", message.text);
|
|
32
|
+
if (message.html) form.append("html", message.html);
|
|
33
|
+
if (message.cc) form.append("cc", Array.isArray(message.cc) ? message.cc.join(",") : message.cc);
|
|
34
|
+
if (message.bcc) form.append("bcc", Array.isArray(message.bcc) ? message.bcc.join(",") : message.bcc);
|
|
35
|
+
if (message.replyTo) form.append("h:Reply-To", message.replyTo);
|
|
36
|
+
|
|
37
|
+
if (message.headers) {
|
|
38
|
+
for (const [key, value] of Object.entries(message.headers)) {
|
|
39
|
+
form.append(`h:${key}`, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (message.attachments) {
|
|
44
|
+
for (const att of message.attachments) {
|
|
45
|
+
const content = typeof att.content === "string"
|
|
46
|
+
? new TextEncoder().encode(att.content)
|
|
47
|
+
: att.content;
|
|
48
|
+
const bytes = new Uint8Array(content);
|
|
49
|
+
const blob = new Blob([bytes], { type: att.contentType || "application/octet-stream" });
|
|
50
|
+
form.append("attachment", blob, att.filename);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: "Basic " + btoa(`api:${this.apiKey}`),
|
|
58
|
+
},
|
|
59
|
+
body: form,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errorText = await response.text();
|
|
64
|
+
throw new Error(`Mailgun API error (${response.status}): ${errorText}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await response.json() as { id: string; message: string };
|
|
68
|
+
return { messageId: result.id, success: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async sendTemplate(templateName: string, to: string | string[], data: TemplateData): Promise<{ messageId: string; success: boolean }> {
|
|
72
|
+
const form = new FormData();
|
|
73
|
+
|
|
74
|
+
if (this.defaultFrom) {
|
|
75
|
+
const from = this.defaultFrom;
|
|
76
|
+
form.append("from", from.name ? `${from.name} <${from.email}>` : from.email);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
form.append("to", Array.isArray(to) ? to.join(",") : to);
|
|
80
|
+
form.append("template", templateName);
|
|
81
|
+
form.append("h:X-Mailgun-Variables", JSON.stringify(data));
|
|
82
|
+
|
|
83
|
+
const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: "Basic " + btoa(`api:${this.apiKey}`),
|
|
87
|
+
},
|
|
88
|
+
body: form,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errorText = await response.text();
|
|
93
|
+
throw new Error(`Mailgun API error (${response.status}): ${errorText}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await response.json() as { id: string; message: string };
|
|
97
|
+
return { messageId: result.id, success: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../index";
|
|
2
|
+
import sgMail from "@sendgrid/mail";
|
|
3
|
+
|
|
4
|
+
export class SendGridEmailAdapter implements EmailAdapter {
|
|
5
|
+
constructor(private config: EmailConfig) {
|
|
6
|
+
if (!config.apiKey) throw new Error("SendGrid API Key required");
|
|
7
|
+
sgMail.setApiKey(config.apiKey);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async send(message: EmailMessage) {
|
|
11
|
+
const from = message.from || this.config.from;
|
|
12
|
+
if (!from) throw new Error("From address required");
|
|
13
|
+
|
|
14
|
+
const msg = {
|
|
15
|
+
to: message.to,
|
|
16
|
+
from: from.name ? { email: from.email, name: from.name } : from.email,
|
|
17
|
+
subject: message.subject,
|
|
18
|
+
text: message.text,
|
|
19
|
+
html: message.html,
|
|
20
|
+
cc: message.cc as any,
|
|
21
|
+
bcc: message.bcc as any,
|
|
22
|
+
replyTo: message.replyTo,
|
|
23
|
+
attachments: message.attachments?.map(a => ({
|
|
24
|
+
filename: a.filename,
|
|
25
|
+
content: typeof a.content === 'string' ? a.content : Buffer.from(a.content).toString("base64"),
|
|
26
|
+
type: a.contentType,
|
|
27
|
+
disposition: "attachment"
|
|
28
|
+
}))
|
|
29
|
+
} as any;
|
|
30
|
+
|
|
31
|
+
const [response] = await sgMail.send(msg);
|
|
32
|
+
return { messageId: response.headers["x-message-id"] as string, success: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async sendTemplate(templateName: string, to: string | string[], data: TemplateData) {
|
|
36
|
+
return this.send({
|
|
37
|
+
to,
|
|
38
|
+
subject: `Template: ${templateName}`,
|
|
39
|
+
html: `<p>Template ${templateName} rendered with ${JSON.stringify(data)}</p>`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailConfig, EmailMessage, TemplateData } from "../index";
|
|
2
|
+
import * as nodemailer from "nodemailer";
|
|
3
|
+
|
|
4
|
+
export class SmtpEmailAdapter implements EmailAdapter {
|
|
5
|
+
private transporter: nodemailer.Transporter;
|
|
6
|
+
|
|
7
|
+
constructor(private config: EmailConfig) {
|
|
8
|
+
if (!config.smtp) throw new Error("SMTP config required");
|
|
9
|
+
this.transporter = nodemailer.createTransport({
|
|
10
|
+
host: config.smtp.host,
|
|
11
|
+
port: config.smtp.port,
|
|
12
|
+
secure: config.smtp.secure ?? false,
|
|
13
|
+
auth: {
|
|
14
|
+
user: config.smtp.username,
|
|
15
|
+
pass: config.smtp.password
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async send(message: EmailMessage) {
|
|
21
|
+
const from = message.from || this.config.from;
|
|
22
|
+
if (!from) throw new Error("From address required");
|
|
23
|
+
|
|
24
|
+
const info = await this.transporter.sendMail({
|
|
25
|
+
from: from.name ? `"${from.name}" <${from.email}>` : from.email,
|
|
26
|
+
to: Array.isArray(message.to) ? message.to.join(", ") : message.to,
|
|
27
|
+
subject: message.subject,
|
|
28
|
+
text: message.text,
|
|
29
|
+
html: message.html,
|
|
30
|
+
cc: message.cc ? (Array.isArray(message.cc) ? message.cc.join(", ") : message.cc) : undefined,
|
|
31
|
+
bcc: message.bcc ? (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc) : undefined,
|
|
32
|
+
replyTo: message.replyTo,
|
|
33
|
+
attachments: message.attachments?.map(a => ({
|
|
34
|
+
filename: a.filename,
|
|
35
|
+
content: typeof a.content === 'string' ? a.content : Buffer.from(a.content),
|
|
36
|
+
contentType: a.contentType
|
|
37
|
+
}))
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { messageId: (info as any).messageId, success: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async sendTemplate(templateName: string, to: string | string[], data: TemplateData) {
|
|
44
|
+
// Simple mock template engine
|
|
45
|
+
return this.send({
|
|
46
|
+
to,
|
|
47
|
+
subject: `Template: ${templateName}`,
|
|
48
|
+
html: `<p>Template ${templateName} rendered with ${JSON.stringify(data)}</p>`
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Feature, ErrorHandlerConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
import { IskraError } from '@iskra-bun/core';
|
|
6
|
+
import { HttpError } from '../errors';
|
|
7
|
+
|
|
8
|
+
export class ErrorHandlerFeature implements Feature {
|
|
9
|
+
name = "error-handler";
|
|
10
|
+
|
|
11
|
+
private config: ErrorHandlerConfig;
|
|
12
|
+
|
|
13
|
+
constructor(config: ErrorHandlerConfig = {}) {
|
|
14
|
+
this.config = {
|
|
15
|
+
includeStack: config.includeStack !== undefined
|
|
16
|
+
? config.includeStack
|
|
17
|
+
: process.env.NODE_ENV === "development",
|
|
18
|
+
customHandlers: config.customHandlers,
|
|
19
|
+
logger: config.logger,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
24
|
+
const app = kernel.getApp();
|
|
25
|
+
|
|
26
|
+
app.onError((err, c) => {
|
|
27
|
+
return this.handleError(err, c);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log("✅ Error handler feature initialized");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private handleError(err: Error | HTTPException, c: Context): Response {
|
|
34
|
+
if (this.config.logger) {
|
|
35
|
+
this.config.logger(err as Error, c);
|
|
36
|
+
} else {
|
|
37
|
+
console.error("Error:", err);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Iskra HttpError — convertir a HTTPException para mantener compatibilidad con Hono
|
|
41
|
+
if (err instanceof HttpError) {
|
|
42
|
+
const status = err.status;
|
|
43
|
+
if (this.config.customHandlers?.[status]) {
|
|
44
|
+
return this.config.customHandlers[status](err, c);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response: any = {
|
|
48
|
+
error: err.message,
|
|
49
|
+
status,
|
|
50
|
+
code: err.code,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (Object.keys(err.context).length > 0) {
|
|
54
|
+
response.context = err.context;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.config.includeStack && err.stack) {
|
|
58
|
+
response.stack = err.stack;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const requestId = c.get("requestId");
|
|
62
|
+
if (requestId) response.requestId = requestId;
|
|
63
|
+
|
|
64
|
+
return c.json(response, status as any);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// IskraError genérico (no-HTTP) — devolver como 500
|
|
68
|
+
if (err instanceof IskraError) {
|
|
69
|
+
const status = 500;
|
|
70
|
+
if (this.config.customHandlers?.[status]) {
|
|
71
|
+
return this.config.customHandlers[status](err, c);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const response: any = {
|
|
75
|
+
error: this.config.includeStack ? err.message : "Internal Server Error",
|
|
76
|
+
status,
|
|
77
|
+
code: err.code,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (this.config.includeStack && err.stack) {
|
|
81
|
+
response.stack = err.stack;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const requestId = c.get("requestId");
|
|
85
|
+
if (requestId) response.requestId = requestId;
|
|
86
|
+
|
|
87
|
+
return c.json(response, status);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Hono HTTPException nativa
|
|
91
|
+
if (err instanceof HTTPException) {
|
|
92
|
+
const status = err.status;
|
|
93
|
+
if (this.config.customHandlers?.[status]) {
|
|
94
|
+
return this.config.customHandlers[status](err, c);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const response: any = {
|
|
98
|
+
error: err.message || this.getStatusText(status),
|
|
99
|
+
status,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (this.config.includeStack && err.stack) {
|
|
103
|
+
response.stack = err.stack;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return c.json(response, status);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Error genérico
|
|
110
|
+
const status = 500;
|
|
111
|
+
if (this.config.customHandlers?.[status]) {
|
|
112
|
+
return this.config.customHandlers[status](err, c);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const response: any = {
|
|
116
|
+
error: this.config.includeStack ? err.message : "Internal Server Error",
|
|
117
|
+
status,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (this.config.includeStack && err.stack) {
|
|
121
|
+
response.stack = err.stack;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requestId = c.get("requestId");
|
|
125
|
+
if (requestId) {
|
|
126
|
+
response.requestId = requestId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return c.json(response, status);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private getStatusText(status: number): string {
|
|
133
|
+
const statusTexts: Record<number, string> = {
|
|
134
|
+
400: "Bad Request",
|
|
135
|
+
401: "Unauthorized",
|
|
136
|
+
403: "Forbidden",
|
|
137
|
+
404: "Not Found",
|
|
138
|
+
500: "Internal Server Error",
|
|
139
|
+
};
|
|
140
|
+
return statusTexts[status] || "Error";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createHttpError(status: number, message: string): HTTPException {
|
|
145
|
+
// @ts-expect-error - status is a number, HTTPException expects a ContentfulStatusCode
|
|
146
|
+
return new HTTPException(status, { message });
|
|
147
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Feature, HealthCheckConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
|
|
5
|
+
export class HealthCheckFeature implements Feature {
|
|
6
|
+
name = "health";
|
|
7
|
+
|
|
8
|
+
private kernel?: Kernel;
|
|
9
|
+
private config: Required<Omit<HealthCheckConfig, "checks">> & { checks?: HealthCheckConfig["checks"] };
|
|
10
|
+
|
|
11
|
+
constructor(config: HealthCheckConfig = {}) {
|
|
12
|
+
this.config = {
|
|
13
|
+
path: config.path || "/health",
|
|
14
|
+
readinessPath: config.readinessPath || "/health/ready",
|
|
15
|
+
livenessPath: config.livenessPath || "/health/live",
|
|
16
|
+
includeDetails: config.includeDetails !== undefined ? config.includeDetails : true,
|
|
17
|
+
checks: config.checks,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
22
|
+
this.kernel = kernel;
|
|
23
|
+
const app = kernel.getApp();
|
|
24
|
+
|
|
25
|
+
app.get(this.config.path, async (c: Context) => await this.handleHealthCheck(c));
|
|
26
|
+
app.get(this.config.readinessPath, async (c: Context) => await this.handleReadinessCheck(c));
|
|
27
|
+
app.get(this.config.livenessPath, async (c: Context) => await this.handleLivenessCheck(c));
|
|
28
|
+
|
|
29
|
+
console.log("✅ Health check feature initialized");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async handleHealthCheck(c: Context) {
|
|
33
|
+
const response: any = {
|
|
34
|
+
status: "ok",
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (this.config.includeDetails && this.kernel) {
|
|
39
|
+
// @ts-expect-error - features is a private kernel field accessed for diagnostics
|
|
40
|
+
response.features = Array.from(this.kernel.features.keys());
|
|
41
|
+
const featureHealth: any = {};
|
|
42
|
+
|
|
43
|
+
const dbFeature = this.kernel.getFeature("db");
|
|
44
|
+
if (dbFeature) await this.checkFeatureHealth(c, dbFeature, featureHealth, "db", "query", "SELECT 1");
|
|
45
|
+
|
|
46
|
+
const cacheFeature = this.kernel.getFeature("cache");
|
|
47
|
+
if (cacheFeature) await this.checkFeatureHealth(c, cacheFeature, featureHealth, "cache", "exists", "__health_check__");
|
|
48
|
+
|
|
49
|
+
if (Object.keys(featureHealth).length > 0) {
|
|
50
|
+
response.checks = featureHealth;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.config.checks) {
|
|
54
|
+
const customChecks: any = {};
|
|
55
|
+
for (const [name, check] of Object.entries(this.config.checks)) {
|
|
56
|
+
try {
|
|
57
|
+
customChecks[name] = await check(c);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
customChecks[name] = { status: "error", error: String(error) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
response.customChecks = customChecks;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return c.json(response);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async checkFeatureHealth(c: Context, feature: any, report: any, key: string, method: string, ...args: any[]) {
|
|
70
|
+
try {
|
|
71
|
+
// Abstracted check logic
|
|
72
|
+
const instance = c.get(key as any);
|
|
73
|
+
if (instance && typeof instance[method] === "function") {
|
|
74
|
+
await instance[method](...args);
|
|
75
|
+
report[key] = { status: "ok" };
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
report[key] = { status: "error", error: String(e) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async handleReadinessCheck(c: Context) {
|
|
83
|
+
// Simplified readiness check
|
|
84
|
+
return c.json({ status: "ready" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async handleLivenessCheck(c: Context) {
|
|
88
|
+
return c.json({
|
|
89
|
+
status: "alive",
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
uptime: process.uptime()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|