@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,174 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
|
2
|
+
import { mysqlTable, varchar, timestamp as mysqlTimestamp, boolean as mysqlBoolean, text as mysqlText } from "drizzle-orm/mysql-core";
|
|
3
|
+
import { sqliteTable, text as sqliteText, integer } from "drizzle-orm/sqlite-core";
|
|
4
|
+
|
|
5
|
+
// ==========================================
|
|
6
|
+
// PostgreSQL Schema
|
|
7
|
+
// ==========================================
|
|
8
|
+
|
|
9
|
+
export const pgUser = pgTable("user", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
name: text("name"),
|
|
12
|
+
email: text("email").notNull().unique(),
|
|
13
|
+
emailVerified: boolean("emailVerified").notNull(),
|
|
14
|
+
image: text("image"),
|
|
15
|
+
createdAt: timestamp("createdAt").notNull(),
|
|
16
|
+
updatedAt: timestamp("updatedAt").notNull()
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const pgSession = pgTable("session", {
|
|
20
|
+
id: text("id").primaryKey(),
|
|
21
|
+
expiresAt: timestamp("expiresAt").notNull(),
|
|
22
|
+
token: text("token").notNull().unique(),
|
|
23
|
+
createdAt: timestamp("createdAt").notNull(),
|
|
24
|
+
updatedAt: timestamp("updatedAt").notNull(),
|
|
25
|
+
ipAddress: text("ipAddress"),
|
|
26
|
+
userAgent: text("userAgent"),
|
|
27
|
+
userId: text("userId").notNull().references(() => pgUser.id)
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const pgAccount = pgTable("account", {
|
|
31
|
+
id: text("id").primaryKey(),
|
|
32
|
+
accountId: text("accountId").notNull(),
|
|
33
|
+
providerId: text("providerId").notNull(),
|
|
34
|
+
userId: text("userId").notNull().references(() => pgUser.id),
|
|
35
|
+
accessToken: text("accessToken"),
|
|
36
|
+
refreshToken: text("refreshToken"),
|
|
37
|
+
idToken: text("idToken"),
|
|
38
|
+
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
|
|
39
|
+
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
|
40
|
+
scope: text("scope"),
|
|
41
|
+
password: text("password"),
|
|
42
|
+
createdAt: timestamp("createdAt").notNull(),
|
|
43
|
+
updatedAt: timestamp("updatedAt").notNull()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const pgVerification = pgTable("verification", {
|
|
47
|
+
id: text("id").primaryKey(),
|
|
48
|
+
identifier: text("identifier").notNull(),
|
|
49
|
+
value: text("value").notNull(),
|
|
50
|
+
expiresAt: timestamp("expiresAt").notNull(),
|
|
51
|
+
createdAt: timestamp("createdAt"),
|
|
52
|
+
updatedAt: timestamp("updatedAt")
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const pgSchema = {
|
|
56
|
+
user: pgUser,
|
|
57
|
+
session: pgSession,
|
|
58
|
+
account: pgAccount,
|
|
59
|
+
verification: pgVerification
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ==========================================
|
|
63
|
+
// MySQL Schema
|
|
64
|
+
// ==========================================
|
|
65
|
+
|
|
66
|
+
export const mysqlUser = mysqlTable("user", {
|
|
67
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
68
|
+
name: varchar("name", { length: 255 }),
|
|
69
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
70
|
+
emailVerified: mysqlBoolean("emailVerified").notNull(),
|
|
71
|
+
image: varchar("image", { length: 255 }),
|
|
72
|
+
createdAt: mysqlTimestamp("createdAt").notNull(),
|
|
73
|
+
updatedAt: mysqlTimestamp("updatedAt").notNull()
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const mysqlSession = mysqlTable("session", {
|
|
77
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
78
|
+
expiresAt: mysqlTimestamp("expiresAt").notNull(),
|
|
79
|
+
token: varchar("token", { length: 255 }).notNull().unique(),
|
|
80
|
+
createdAt: mysqlTimestamp("createdAt").notNull(),
|
|
81
|
+
updatedAt: mysqlTimestamp("updatedAt").notNull(),
|
|
82
|
+
ipAddress: varchar("ipAddress", { length: 45 }),
|
|
83
|
+
userAgent: mysqlText("userAgent"),
|
|
84
|
+
userId: varchar("userId", { length: 36 }).notNull().references(() => mysqlUser.id)
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const mysqlAccount = mysqlTable("account", {
|
|
88
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
89
|
+
accountId: varchar("accountId", { length: 255 }).notNull(),
|
|
90
|
+
providerId: varchar("providerId", { length: 255 }).notNull(),
|
|
91
|
+
userId: varchar("userId", { length: 36 }).notNull().references(() => mysqlUser.id),
|
|
92
|
+
accessToken: mysqlText("accessToken"),
|
|
93
|
+
refreshToken: mysqlText("refreshToken"),
|
|
94
|
+
idToken: mysqlText("idToken"),
|
|
95
|
+
accessTokenExpiresAt: mysqlTimestamp("accessTokenExpiresAt"),
|
|
96
|
+
refreshTokenExpiresAt: mysqlTimestamp("refreshTokenExpiresAt"),
|
|
97
|
+
scope: mysqlText("scope"),
|
|
98
|
+
password: varchar("password", { length: 255 }),
|
|
99
|
+
createdAt: mysqlTimestamp("createdAt").notNull(),
|
|
100
|
+
updatedAt: mysqlTimestamp("updatedAt").notNull()
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const mysqlVerification = mysqlTable("verification", {
|
|
104
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
105
|
+
identifier: varchar("identifier", { length: 255 }).notNull(),
|
|
106
|
+
value: varchar("value", { length: 255 }).notNull(),
|
|
107
|
+
expiresAt: mysqlTimestamp("expiresAt").notNull(),
|
|
108
|
+
createdAt: mysqlTimestamp("createdAt"),
|
|
109
|
+
updatedAt: mysqlTimestamp("updatedAt")
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const mysqlSchema = {
|
|
113
|
+
user: mysqlUser,
|
|
114
|
+
session: mysqlSession,
|
|
115
|
+
account: mysqlAccount,
|
|
116
|
+
verification: mysqlVerification
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ==========================================
|
|
120
|
+
// SQLite Schema
|
|
121
|
+
// ==========================================
|
|
122
|
+
|
|
123
|
+
export const sqliteUser = sqliteTable("user", {
|
|
124
|
+
id: sqliteText("id").primaryKey(),
|
|
125
|
+
name: sqliteText("name"),
|
|
126
|
+
email: sqliteText("email").notNull().unique(),
|
|
127
|
+
emailVerified: integer("emailVerified", { mode: "boolean" }).notNull(),
|
|
128
|
+
image: sqliteText("image"),
|
|
129
|
+
createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
|
|
130
|
+
updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull()
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const sqliteSession = sqliteTable("session", {
|
|
134
|
+
id: sqliteText("id").primaryKey(),
|
|
135
|
+
expiresAt: integer("expiresAt", { mode: "timestamp" }).notNull(),
|
|
136
|
+
token: sqliteText("token").notNull().unique(),
|
|
137
|
+
createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
|
|
138
|
+
updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull(),
|
|
139
|
+
ipAddress: sqliteText("ipAddress"),
|
|
140
|
+
userAgent: sqliteText("userAgent"),
|
|
141
|
+
userId: sqliteText("userId").notNull().references(() => sqliteUser.id)
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const sqliteAccount = sqliteTable("account", {
|
|
145
|
+
id: sqliteText("id").primaryKey(),
|
|
146
|
+
accountId: sqliteText("accountId").notNull(),
|
|
147
|
+
providerId: sqliteText("providerId").notNull(),
|
|
148
|
+
userId: sqliteText("userId").notNull().references(() => sqliteUser.id),
|
|
149
|
+
accessToken: sqliteText("accessToken"),
|
|
150
|
+
refreshToken: sqliteText("refreshToken"),
|
|
151
|
+
idToken: sqliteText("idToken"),
|
|
152
|
+
accessTokenExpiresAt: integer("accessTokenExpiresAt", { mode: "timestamp" }),
|
|
153
|
+
refreshTokenExpiresAt: integer("refreshTokenExpiresAt", { mode: "timestamp" }),
|
|
154
|
+
scope: sqliteText("scope"),
|
|
155
|
+
password: sqliteText("password"),
|
|
156
|
+
createdAt: integer("createdAt", { mode: "timestamp" }).notNull(),
|
|
157
|
+
updatedAt: integer("updatedAt", { mode: "timestamp" }).notNull()
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const sqliteVerification = sqliteTable("verification", {
|
|
161
|
+
id: sqliteText("id").primaryKey(),
|
|
162
|
+
identifier: sqliteText("identifier").notNull(),
|
|
163
|
+
value: sqliteText("value").notNull(),
|
|
164
|
+
expiresAt: integer("expiresAt", { mode: "timestamp" }).notNull(),
|
|
165
|
+
createdAt: integer("createdAt", { mode: "timestamp" }),
|
|
166
|
+
updatedAt: integer("updatedAt", { mode: "timestamp" })
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const sqliteSchema = {
|
|
170
|
+
user: sqliteUser,
|
|
171
|
+
session: sqliteSession,
|
|
172
|
+
account: sqliteAccount,
|
|
173
|
+
verification: sqliteVerification
|
|
174
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth feature types and interfaces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
emailVerified: boolean;
|
|
9
|
+
name?: string | null;
|
|
10
|
+
image?: string | null;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
// deno-lint-ignore no-explicit-any
|
|
14
|
+
[key: string]: any; // Allow custom fields
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Account {
|
|
18
|
+
id: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
accountId: string;
|
|
21
|
+
providerId: string;
|
|
22
|
+
accessToken?: string;
|
|
23
|
+
refreshToken?: string;
|
|
24
|
+
idToken?: string;
|
|
25
|
+
expiresAt?: Date;
|
|
26
|
+
scope?: string;
|
|
27
|
+
password?: string; // For credentials provider
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
updatedAt: Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Verification {
|
|
33
|
+
id: string;
|
|
34
|
+
identifier: string; // email or phone
|
|
35
|
+
value: string; // verification token
|
|
36
|
+
expiresAt: Date;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuthSession {
|
|
41
|
+
id: string;
|
|
42
|
+
userId: string;
|
|
43
|
+
expiresAt: Date;
|
|
44
|
+
ipAddress?: string;
|
|
45
|
+
userAgent?: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
updatedAt: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PasswordResetToken {
|
|
51
|
+
id: string;
|
|
52
|
+
userId: string;
|
|
53
|
+
token: string;
|
|
54
|
+
expiresAt: Date;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface EmailVerificationToken {
|
|
59
|
+
id: string;
|
|
60
|
+
userId: string;
|
|
61
|
+
token: string;
|
|
62
|
+
email: string;
|
|
63
|
+
expiresAt: Date;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Auth context available in route handlers
|
|
69
|
+
*/
|
|
70
|
+
export interface AuthContext {
|
|
71
|
+
user: User | null;
|
|
72
|
+
session: AuthSession | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Registration input
|
|
77
|
+
*/
|
|
78
|
+
export interface SignUpInput {
|
|
79
|
+
email: string;
|
|
80
|
+
password: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
// deno-lint-ignore no-explicit-any
|
|
83
|
+
[key: string]: any; // Allow custom fields
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Login input
|
|
88
|
+
*/
|
|
89
|
+
export interface SignInInput {
|
|
90
|
+
email: string;
|
|
91
|
+
password: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Password reset request
|
|
96
|
+
*/
|
|
97
|
+
export interface PasswordResetRequest {
|
|
98
|
+
email: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Password reset confirmation
|
|
103
|
+
*/
|
|
104
|
+
export interface PasswordResetConfirm {
|
|
105
|
+
token: string;
|
|
106
|
+
newPassword: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Email verification
|
|
111
|
+
*/
|
|
112
|
+
export interface EmailVerificationRequest {
|
|
113
|
+
token: string;
|
|
114
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Feature, CacheConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import type { Context, Next } from "hono";
|
|
4
|
+
import Redis from "ioredis";
|
|
5
|
+
|
|
6
|
+
// Standard Cache Interface
|
|
7
|
+
export interface CacheAdapter {
|
|
8
|
+
get(key: string): Promise<any>;
|
|
9
|
+
set(key: string, value: any, ttl?: number): Promise<void>;
|
|
10
|
+
delete(key: string): Promise<void>;
|
|
11
|
+
exists(key: string): Promise<boolean>;
|
|
12
|
+
increment?(key: string): Promise<number>;
|
|
13
|
+
disconnect?(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Simple Memory Adapter
|
|
17
|
+
class MemoryAdapter implements CacheAdapter {
|
|
18
|
+
private store = new Map<string, { value: any, expires: number | null }>();
|
|
19
|
+
|
|
20
|
+
async get(key: string) {
|
|
21
|
+
const item = this.store.get(key);
|
|
22
|
+
if (!item) return null;
|
|
23
|
+
if (item.expires && item.expires < Date.now()) {
|
|
24
|
+
this.store.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return item.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async set(key: string, value: any, ttl?: number) {
|
|
31
|
+
const expires = ttl ? Date.now() + ttl * 1000 : null;
|
|
32
|
+
this.store.set(key, { value, expires });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async delete(key: string) {
|
|
36
|
+
this.store.delete(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async exists(key: string) {
|
|
40
|
+
return this.store.has(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async increment(key: string): Promise<number> {
|
|
44
|
+
const item = this.store.get(key);
|
|
45
|
+
if (!item || (item.expires && item.expires < Date.now())) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const newVal = Number(item.value) + 1;
|
|
49
|
+
item.value = newVal;
|
|
50
|
+
return newVal;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Redis Adapter
|
|
55
|
+
class RedisAdapter implements CacheAdapter {
|
|
56
|
+
private client: Redis;
|
|
57
|
+
|
|
58
|
+
constructor(options: any) {
|
|
59
|
+
this.client = new Redis(options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async get(key: string) {
|
|
63
|
+
const value = await this.client.get(key);
|
|
64
|
+
try {
|
|
65
|
+
return value ? JSON.parse(value) : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async set(key: string, value: any, ttl?: number) {
|
|
72
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
73
|
+
if (ttl) {
|
|
74
|
+
await this.client.set(key, stringValue, 'EX', ttl);
|
|
75
|
+
} else {
|
|
76
|
+
await this.client.set(key, stringValue);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(key: string) {
|
|
81
|
+
await this.client.del(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async exists(key: string) {
|
|
85
|
+
const result = await this.client.exists(key);
|
|
86
|
+
return result === 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async increment(key: string): Promise<number> {
|
|
90
|
+
return await this.client.incr(key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async disconnect() {
|
|
94
|
+
await this.client.quit();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare module "hono" {
|
|
99
|
+
interface ContextVariableMap {
|
|
100
|
+
cache: CacheAdapter;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class CacheFeature implements Feature {
|
|
105
|
+
name = "cache";
|
|
106
|
+
public client!: CacheAdapter;
|
|
107
|
+
|
|
108
|
+
constructor(private config: CacheConfig = { adapter: "memory" }) { }
|
|
109
|
+
|
|
110
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
111
|
+
console.log(`⚙️ Initializing Cache: ${this.config.adapter}`);
|
|
112
|
+
|
|
113
|
+
if (this.config.adapter === "redis") {
|
|
114
|
+
const conn = this.config.connection || {};
|
|
115
|
+
try {
|
|
116
|
+
this.client = new RedisAdapter({
|
|
117
|
+
host: conn.host || "localhost",
|
|
118
|
+
port: conn.port || 6379,
|
|
119
|
+
password: conn.password,
|
|
120
|
+
db: conn.db || 0
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.warn("⚠️ Redis connection failed, falling back to memory cache");
|
|
124
|
+
this.client = new MemoryAdapter();
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
this.client = new MemoryAdapter();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const app = kernel.getApp();
|
|
131
|
+
app.use("*", async (c: Context, next: Next) => {
|
|
132
|
+
c.set("cache", this.client);
|
|
133
|
+
await next();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('✅ Cache initialized.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async shutdown(): Promise<void> {
|
|
140
|
+
if (this.client.disconnect) {
|
|
141
|
+
await this.client.disconnect();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Feature, CorsConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
4
|
+
|
|
5
|
+
export class CorsFeature implements Feature {
|
|
6
|
+
name = "cors";
|
|
7
|
+
|
|
8
|
+
constructor(private config: CorsConfig = {}) {
|
|
9
|
+
if (!this.config.origin) {
|
|
10
|
+
this.config.origin = "*";
|
|
11
|
+
}
|
|
12
|
+
if (this.config.credentials === undefined) {
|
|
13
|
+
this.config.credentials = false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
18
|
+
const app = kernel.getApp();
|
|
19
|
+
|
|
20
|
+
const honoConfig: any = { ...this.config };
|
|
21
|
+
|
|
22
|
+
if (typeof this.config.origin === "function") {
|
|
23
|
+
honoConfig.origin = (origin: string) => {
|
|
24
|
+
// @ts-expect-error - origin is narrowed to a function above
|
|
25
|
+
const result = this.config.origin(origin);
|
|
26
|
+
return result ? origin : null;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
app.use("*", cors(honoConfig));
|
|
31
|
+
console.log("✅ CORS feature initialized");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Feature, CsrfConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import type { Context, Next } from "hono";
|
|
4
|
+
import { HTTPException } from "hono/http-exception";
|
|
5
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
6
|
+
|
|
7
|
+
declare module "hono" {
|
|
8
|
+
interface ContextVariableMap {
|
|
9
|
+
csrfToken: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CsrfFeature implements Feature {
|
|
14
|
+
name = "csrf";
|
|
15
|
+
private config: Required<CsrfConfig>;
|
|
16
|
+
|
|
17
|
+
constructor(config: CsrfConfig) {
|
|
18
|
+
if (!config.secret) throw new Error("CSRF secret is required");
|
|
19
|
+
this.config = {
|
|
20
|
+
secret: config.secret,
|
|
21
|
+
cookieName: config.cookieName || "_csrf",
|
|
22
|
+
headerName: config.headerName || "X-CSRF-Token",
|
|
23
|
+
ignoreMethods: config.ignoreMethods || ["GET", "HEAD", "OPTIONS"],
|
|
24
|
+
cookieOptions: {
|
|
25
|
+
httpOnly: true,
|
|
26
|
+
secure: true,
|
|
27
|
+
sameSite: "Strict",
|
|
28
|
+
maxAge: 86400,
|
|
29
|
+
...config.cookieOptions
|
|
30
|
+
}
|
|
31
|
+
} as Required<CsrfConfig>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
35
|
+
const app = kernel.getApp();
|
|
36
|
+
app.use("*", async (c: Context, next: Next) => {
|
|
37
|
+
await this.middleware(c, next);
|
|
38
|
+
});
|
|
39
|
+
console.log("✅ CSRF feature initialized");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async middleware(c: Context, next: Next) {
|
|
43
|
+
const method = c.req.method.toUpperCase();
|
|
44
|
+
let token = getCookie(c, this.config.cookieName);
|
|
45
|
+
|
|
46
|
+
if (!token) {
|
|
47
|
+
token = crypto.randomUUID().replace(/-/g, '');
|
|
48
|
+
setCookie(c, this.config.cookieName, token, {
|
|
49
|
+
httpOnly: this.config.cookieOptions.httpOnly,
|
|
50
|
+
secure: this.config.cookieOptions.secure,
|
|
51
|
+
sameSite: this.config.cookieOptions.sameSite as any,
|
|
52
|
+
maxAge: this.config.cookieOptions.maxAge,
|
|
53
|
+
path: "/"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
c.set("csrfToken", token);
|
|
58
|
+
|
|
59
|
+
if (this.config.ignoreMethods.includes(method)) {
|
|
60
|
+
await next();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isValid = await this.validateToken(c, token);
|
|
65
|
+
if (!isValid) {
|
|
66
|
+
throw new HTTPException(403, { message: "Invalid CSRF token" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await next();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async validateToken(c: Context, expectedToken: string): Promise<boolean> {
|
|
73
|
+
const headerToken = c.req.header(this.config.headerName);
|
|
74
|
+
if (headerToken === expectedToken) return true;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const contentType = c.req.header("content-type");
|
|
78
|
+
if (contentType?.includes("application/x-www-form-urlencoded")) {
|
|
79
|
+
const body = await c.req.parseBody();
|
|
80
|
+
const bodyToken = body._csrf || body[this.config.cookieName];
|
|
81
|
+
if (String(bodyToken) === expectedToken) return true;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* ignored */ }
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function requireCsrf() {
|
|
90
|
+
return async (c: Context, next: Next) => {
|
|
91
|
+
if (!c.get("csrfToken")) throw new HTTPException(403, { message: "CSRF token required" });
|
|
92
|
+
await next();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Feature, DbConfig } from "../types";
|
|
2
|
+
import type { Kernel } from "../kernel";
|
|
3
|
+
import type { Context, Next } from "hono";
|
|
4
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
5
|
+
import { drizzle as drizzleMysql } from 'drizzle-orm/mysql2';
|
|
6
|
+
import { drizzle as drizzleBunSqlite } from 'drizzle-orm/bun-sqlite';
|
|
7
|
+
import postgres from 'postgres';
|
|
8
|
+
import mysql from 'mysql2/promise';
|
|
9
|
+
import { Database } from 'bun:sqlite';
|
|
10
|
+
|
|
11
|
+
declare module "hono" {
|
|
12
|
+
interface ContextVariableMap {
|
|
13
|
+
db: any;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class DbFeature implements Feature {
|
|
18
|
+
name = "db";
|
|
19
|
+
private client: any;
|
|
20
|
+
public db: any;
|
|
21
|
+
public readonly adapter: string;
|
|
22
|
+
|
|
23
|
+
constructor(private config: DbConfig) {
|
|
24
|
+
this.adapter = config.adapter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async initialize(kernel: Kernel): Promise<void> {
|
|
28
|
+
const config = this.config;
|
|
29
|
+
console.log(`⚙️ Initializing DB driver: ${config.adapter}`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
switch (config.adapter) {
|
|
33
|
+
case 'postgres': {
|
|
34
|
+
if (!config.connection) throw new Error("Missing connection info");
|
|
35
|
+
const pgConfig = config.connection.connectionString ? config.connection.connectionString : {
|
|
36
|
+
host: config.connection.host || 'localhost',
|
|
37
|
+
port: config.connection.port || 5432,
|
|
38
|
+
database: config.connection.database!,
|
|
39
|
+
user: config.connection.user!,
|
|
40
|
+
password: config.connection.password!
|
|
41
|
+
};
|
|
42
|
+
this.client = postgres(pgConfig as any);
|
|
43
|
+
this.db = drizzle(this.client);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'mysql': {
|
|
47
|
+
if (!config.connection) throw new Error("Missing connection info");
|
|
48
|
+
const mysqlConfig = config.connection.connectionString ? config.connection.connectionString : {
|
|
49
|
+
host: config.connection.host || 'localhost',
|
|
50
|
+
port: config.connection.port || 3306,
|
|
51
|
+
database: config.connection.database!,
|
|
52
|
+
user: config.connection.user!,
|
|
53
|
+
password: config.connection.password!
|
|
54
|
+
};
|
|
55
|
+
this.client = await mysql.createConnection(mysqlConfig as any);
|
|
56
|
+
this.db = drizzleMysql(this.client);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'sqlite': {
|
|
60
|
+
const url = config.connection?.database || ':memory:';
|
|
61
|
+
this.client = new Database(url);
|
|
62
|
+
this.db = drizzleBunSqlite(this.client);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unsupported DB adapter: ${config.adapter}`);
|
|
67
|
+
}
|
|
68
|
+
console.log('✅ DB connected successfully.');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('❌ Failed to connect to DB', error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const app = kernel.getApp();
|
|
75
|
+
app.use("*", async (c: Context, next: Next) => {
|
|
76
|
+
c.set("db", this.db);
|
|
77
|
+
await next();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async shutdown(): Promise<void> {
|
|
82
|
+
if (this.client) {
|
|
83
|
+
if (this.client.end) {
|
|
84
|
+
await this.client.end();
|
|
85
|
+
} else if (this.client.close) {
|
|
86
|
+
this.client.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|