@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
@@ -0,0 +1,243 @@
1
+ import type { Feature, ApiKeyConfig, ApiKeyMetadata, ApiKeyValidationResult } from "../types";
2
+ import type { Kernel } from "../kernel";
3
+ import type { Context, Next } from "hono";
4
+ import { HTTPException } from "hono/http-exception";
5
+
6
+ // --- ApiKeyStore ---
7
+
8
+ export class ApiKeyStore {
9
+ private staticKeysMap: Map<string, ApiKeyMetadata> = new Map();
10
+ private cache?: any;
11
+
12
+ constructor(private config: Required<ApiKeyConfig>, private kernel: Kernel) {
13
+ this.initializeStaticKeys();
14
+ }
15
+
16
+ private getCache() {
17
+ if (!this.cache && this.config.enableCache) {
18
+ const cacheFeature = this.kernel.getFeature<any>('cache');
19
+ if (cacheFeature) {
20
+ this.cache = cacheFeature.client;
21
+ }
22
+ }
23
+ return this.cache;
24
+ }
25
+
26
+ private initializeStaticKeys(): void {
27
+ if (!this.config.staticKeys || this.config.staticKeys.length === 0) {
28
+ return;
29
+ }
30
+
31
+ for (const staticKey of this.config.staticKeys) {
32
+ const metadata: ApiKeyMetadata = {
33
+ id: this.generateId(staticKey.key),
34
+ key: staticKey.key,
35
+ name: staticKey.name,
36
+ scopes: staticKey.scopes,
37
+ rateLimit: staticKey.rateLimit,
38
+ expiresAt: staticKey.expiresAt,
39
+ createdAt: new Date(),
40
+ metadata: staticKey.metadata,
41
+ lastUsedAt: undefined // Explicitly undefined initially
42
+ };
43
+
44
+ this.staticKeysMap.set(staticKey.key, metadata);
45
+ }
46
+ console.log(`✅ Loaded ${this.staticKeysMap.size} static API keys`);
47
+ }
48
+
49
+ private generateId(key: string): string {
50
+ return key.substring(0, 8);
51
+ }
52
+
53
+ private compareKeys(a: string, b: string): boolean {
54
+ if (a.length !== b.length) return false;
55
+ let result = 0;
56
+ for (let i = 0; i < a.length; i++) {
57
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
58
+ }
59
+ return result === 0;
60
+ }
61
+
62
+ private isExpired(expiresAt?: Date): boolean {
63
+ if (!expiresAt) return false;
64
+ return new Date() > expiresAt;
65
+ }
66
+
67
+ async validate(key: string): Promise<ApiKeyValidationResult> {
68
+ if (!key) return { isValid: false, error: "API key is required" };
69
+
70
+ const cache = this.getCache();
71
+ const cacheKey = `apikey:${key}`;
72
+
73
+ if (cache) {
74
+ try {
75
+ const cached = await cache.get(cacheKey);
76
+ if (cached) {
77
+ const metadata = typeof cached === 'string' ? JSON.parse(cached) : cached;
78
+ return { isValid: true, key: metadata };
79
+ }
80
+ } catch (err) {
81
+ // Cache error, ignore
82
+ }
83
+ }
84
+
85
+ // Check static keys
86
+ for (const [staticKey, metadata] of this.staticKeysMap) {
87
+ if (this.compareKeys(key, staticKey)) {
88
+ if (this.isExpired(metadata.expiresAt)) {
89
+ return { isValid: false, error: "API key has expired" };
90
+ }
91
+ metadata.lastUsedAt = new Date();
92
+
93
+ if (cache) {
94
+ const ttl = this.config.cacheTtl ? Math.floor(this.config.cacheTtl / 1000) : 300;
95
+ try {
96
+ await cache.set(cacheKey, JSON.stringify(metadata), ttl);
97
+ } catch {
98
+ // ignore cache write failures — validation already succeeded
99
+ }
100
+ }
101
+
102
+ return { isValid: true, key: metadata };
103
+ }
104
+ }
105
+
106
+ return { isValid: false, error: "Invalid API key" };
107
+ }
108
+
109
+ hasScopes(key: ApiKeyMetadata, requiredScopes: string[]): boolean {
110
+ if (!requiredScopes || requiredScopes.length === 0) return true;
111
+ if (!key.scopes || key.scopes.length === 0) return false;
112
+
113
+ for (const requiredScope of requiredScopes) {
114
+ const hasScope = key.scopes.some((scope) => {
115
+ if (scope.endsWith("*")) {
116
+ const prefix = scope.slice(0, -1);
117
+ return requiredScope.startsWith(prefix);
118
+ }
119
+ return scope === requiredScope;
120
+ });
121
+
122
+ if (!hasScope) return false;
123
+ }
124
+ return true;
125
+ }
126
+ }
127
+
128
+ // --- ApiKeyFeature ---
129
+
130
+ declare module "hono" {
131
+ interface ContextVariableMap {
132
+ apiKey?: ApiKeyMetadata;
133
+ apiKeyScopes?: string[];
134
+ hasScope?: (scope: string) => boolean;
135
+ hasAnyScope?: (...scopes: string[]) => boolean;
136
+ hasAllScopes?: (...scopes: string[]) => boolean;
137
+ }
138
+ }
139
+
140
+ export class ApiKeyFeature implements Feature {
141
+ name = "apiKey";
142
+ private store?: ApiKeyStore;
143
+ private config: Required<ApiKeyConfig>;
144
+
145
+ constructor(config: ApiKeyConfig = {}) {
146
+ this.config = {
147
+ staticKeys: config.staticKeys || [],
148
+ headerName: config.headerName || "X-API-Key",
149
+ queryParamName: config.queryParamName || "api_key",
150
+ extractStrategies: config.extractStrategies || ["header", "bearer"],
151
+ // Defaults for other optional properties
152
+ vaultService: undefined,
153
+ customExtractor: undefined,
154
+ enableCache: config.enableCache ?? true,
155
+ cacheTtl: config.cacheTtl ?? 300000,
156
+ requireScopes: config.requireScopes ?? false,
157
+ skipPaths: config.skipPaths || [],
158
+ onError: config.onError,
159
+ onValidated: config.onValidated
160
+ } as unknown as Required<ApiKeyConfig>;
161
+ }
162
+
163
+ async initialize(kernel: Kernel): Promise<void> {
164
+ this.store = new ApiKeyStore(this.config, kernel);
165
+ const app = kernel.getApp();
166
+
167
+ app.use("*", async (c: Context, next: Next) => {
168
+ if (this.shouldSkipPath(c.req.path)) {
169
+ await next();
170
+ return;
171
+ }
172
+
173
+ const apiKey = this.extractApiKey(c);
174
+ if (!apiKey) {
175
+ c.set("apiKey", undefined);
176
+ c.set("apiKeyScopes", undefined);
177
+ await next();
178
+ return;
179
+ }
180
+
181
+ const result = await this.store!.validate(apiKey);
182
+ if (!result.isValid) {
183
+ throw new HTTPException(401, { message: result.error || "Invalid API key" });
184
+ }
185
+
186
+ c.set("apiKey", result.key);
187
+ c.set("apiKeyScopes", result.key?.scopes || []);
188
+
189
+ c.set("hasScope", (scope: string) => this.store!.hasScopes(result.key!, [scope]));
190
+ c.set("hasAnyScope", (...scopes: string[]) => scopes.some(s => this.store!.hasScopes(result.key!, [s])));
191
+ c.set("hasAllScopes", (...scopes: string[]) => this.store!.hasScopes(result.key!, scopes));
192
+
193
+ await next();
194
+ });
195
+
196
+ console.log("✅ API Key feature initialized");
197
+ }
198
+
199
+ private shouldSkipPath(path: string): boolean {
200
+ if (!this.config.skipPaths?.length) return false;
201
+ return this.config.skipPaths.some(skip => {
202
+ if (skip.endsWith("*")) return path.startsWith(skip.slice(0, -1));
203
+ return path === skip;
204
+ });
205
+ }
206
+
207
+ private extractApiKey(c: Context): string | null {
208
+ for (const strategy of (this.config.extractStrategies || [])) {
209
+ let key: string | null = null;
210
+ switch (strategy) {
211
+ case "header":
212
+ key = c.req.header(this.config.headerName) || null;
213
+ break;
214
+ case "bearer": {
215
+ const auth = c.req.header("Authorization");
216
+ if (auth?.startsWith("Bearer ")) key = auth.substring(7);
217
+ break;
218
+ }
219
+ case "query":
220
+ key = c.req.query(this.config.queryParamName) || null;
221
+ break;
222
+ }
223
+ if (key) return key;
224
+ }
225
+ return null;
226
+ }
227
+ }
228
+
229
+ export function requireApiKey() {
230
+ return async (c: Context, next: Next) => {
231
+ if (!c.get("apiKey")) throw new HTTPException(401, { message: "API key is required" });
232
+ await next();
233
+ };
234
+ }
235
+
236
+ export function requireScope(...scopes: string[]) {
237
+ return async (c: Context, next: Next) => {
238
+ const hasAll = c.get("hasAllScopes");
239
+ if (!c.get("apiKey")) throw new HTTPException(401, { message: "API key is required" });
240
+ if (!hasAll || !hasAll(...scopes)) throw new HTTPException(403, { message: "Insufficient scopes" });
241
+ await next();
242
+ };
243
+ }
@@ -0,0 +1,160 @@
1
+ import { betterAuth, type Auth as BetterAuthInstance } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { genericOAuth } from "better-auth/plugins/generic-oauth";
4
+ import { pgSchema, mysqlSchema, sqliteSchema } from "./schema";
5
+
6
+ export interface BetterAuthConfigOptions {
7
+ db: any; // Drizzle instance
8
+ adapterType: "postgres" | "mysql" | "sqlite";
9
+ secret: string;
10
+ baseURL?: string;
11
+ basePath?: string;
12
+ trustedOrigins?: string[];
13
+ enableEmailPassword?: boolean;
14
+ disableCSRFCheck?: boolean;
15
+ // deno-lint-ignore no-explicit-any
16
+ socialProviders?: Record<string, any>;
17
+ oidcConfig?: {
18
+ clientId: string;
19
+ clientSecret: string;
20
+ issuer: string;
21
+ providerId?: string;
22
+ authorizationEndpoint?: string;
23
+ tokenEndpoint?: string;
24
+ userinfoEndpoint?: string;
25
+ jwksEndpoint?: string;
26
+ discoveryEndpoint?: string;
27
+ scopes?: string[];
28
+ pkce?: boolean;
29
+ mapping?: {
30
+ id?: string;
31
+ email?: string;
32
+ emailVerified?: string;
33
+ name?: string;
34
+ image?: string;
35
+ extraFields?: Record<string, string>;
36
+ };
37
+ };
38
+ }
39
+
40
+ export function createBetterAuth(options: BetterAuthConfigOptions): BetterAuthInstance {
41
+ const {
42
+ db,
43
+ adapterType,
44
+ secret,
45
+ baseURL = "http://localhost:3000",
46
+ basePath = "/api/auth",
47
+ trustedOrigins = [],
48
+ enableEmailPassword = true,
49
+ disableCSRFCheck = false,
50
+ socialProviders,
51
+ oidcConfig,
52
+ } = options;
53
+
54
+ const baseOrigin = new URL(baseURL).origin;
55
+ const allTrustedOrigins = trustedOrigins.includes(baseOrigin)
56
+ ? trustedOrigins
57
+ : [baseOrigin, ...trustedOrigins];
58
+
59
+ console.log("[BetterAuth Config] Creating Drizzle adapter...");
60
+
61
+ let schema;
62
+ let provider: "pg" | "mysql" | "sqlite";
63
+
64
+ switch (adapterType) {
65
+ case "postgres":
66
+ schema = pgSchema;
67
+ provider = "pg";
68
+ break;
69
+ case "mysql":
70
+ schema = mysqlSchema;
71
+ provider = "mysql";
72
+ break;
73
+ case "sqlite":
74
+ schema = sqliteSchema;
75
+ provider = "sqlite";
76
+ break;
77
+ default:
78
+ throw new Error(`Unsupported adapter type: ${adapterType}`);
79
+ }
80
+
81
+ const database = drizzleAdapter(db, {
82
+ provider,
83
+ schema
84
+ });
85
+
86
+ console.log("[BetterAuth Config] Initializing betterAuth with config:", {
87
+ baseURL,
88
+ basePath,
89
+ provider,
90
+ enableEmailPassword
91
+ });
92
+
93
+ const plugins = [];
94
+ if (oidcConfig) {
95
+ const authorizationUrl = oidcConfig.authorizationEndpoint ||
96
+ `${oidcConfig.issuer}/protocol/openid-connect/auth`;
97
+ const tokenUrl = oidcConfig.tokenEndpoint ||
98
+ `${oidcConfig.issuer}/protocol/openid-connect/token`;
99
+ const userInfoUrl = oidcConfig.userinfoEndpoint ||
100
+ `${oidcConfig.issuer}/protocol/openid-connect/userinfo`;
101
+
102
+ plugins.push(
103
+ genericOAuth({
104
+ config: [
105
+ {
106
+ providerId: oidcConfig.providerId || "oidc",
107
+ clientId: oidcConfig.clientId,
108
+ clientSecret: oidcConfig.clientSecret,
109
+ authorizationUrl,
110
+ tokenUrl,
111
+ userInfoUrl,
112
+ discoveryUrl: oidcConfig.discoveryEndpoint ||
113
+ `${oidcConfig.issuer}/.well-known/openid-configuration`,
114
+ scopes: oidcConfig.scopes || ["openid", "email", "profile"],
115
+ pkce: oidcConfig.pkce !== undefined ? oidcConfig.pkce : false,
116
+ mapProfileToUser: (profile: any) => {
117
+ return {
118
+ id: profile.sub || profile.id,
119
+ email: profile.email,
120
+ name: profile.name || profile.preferred_username,
121
+ image: profile.picture || profile.image,
122
+ emailVerified: profile.email_verified || false,
123
+ };
124
+ },
125
+ },
126
+ ],
127
+ }),
128
+ );
129
+ }
130
+
131
+ return betterAuth({
132
+ database,
133
+ secret,
134
+ baseURL,
135
+ basePath,
136
+ trustedOrigins: allTrustedOrigins,
137
+ emailAndPassword: enableEmailPassword
138
+ ? {
139
+ enabled: true,
140
+ autoSignIn: true,
141
+ }
142
+ : undefined,
143
+ socialProviders: Object.keys(socialProviders || {}).length > 0 ? socialProviders : undefined,
144
+ plugins,
145
+ session: {
146
+ expiresIn: 60 * 60 * 24 * 7,
147
+ updateAge: 60 * 60 * 24,
148
+ cookieCache: {
149
+ enabled: true,
150
+ maxAge: 5 * 60,
151
+ },
152
+ },
153
+ advanced: {
154
+ disableCSRFCheck,
155
+ generateId: () => crypto.randomUUID().replace(/-/g, ""),
156
+ },
157
+ }) as unknown as BetterAuthInstance;
158
+ }
159
+
160
+ export type Auth = ReturnType<typeof createBetterAuth>;
@@ -0,0 +1,229 @@
1
+ import type { AuthConfig, Feature, Kernel } from "../../types";
2
+ import type { Context, Hono, Next } from "hono";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { type Auth, createBetterAuth } from "./better-auth-config";
5
+ import { z } from "@hono/zod-openapi";
6
+ import type { DbFeature } from "../db";
7
+ import type { OpenAPIFeature } from "../openapi";
8
+ import type { User } from "./types";
9
+
10
+ declare module "hono" {
11
+ interface ContextVariableMap {
12
+ user: User | null;
13
+ authUser: User | null;
14
+ }
15
+ }
16
+
17
+ // ─── OpenAPI Schemas ─────────────────────────────────────────────────────────
18
+
19
+ const SignInSchema = z.object({
20
+ email: z.string().email(),
21
+ password: z.string().min(1),
22
+ });
23
+
24
+ const SignUpSchema = z.object({
25
+ email: z.string().email(),
26
+ password: z.string().min(8),
27
+ name: z.string().optional(),
28
+ });
29
+
30
+ const AuthSuccessSchema = z.object({
31
+ token: z.string().optional(),
32
+ user: z.object({
33
+ id: z.string(),
34
+ email: z.string(),
35
+ name: z.string().optional(),
36
+ }).optional(),
37
+ session: z.any().optional(),
38
+ });
39
+
40
+ const SessionResponseSchema = z.object({
41
+ session: z.any().nullable(),
42
+ user: z.any().nullable(),
43
+ });
44
+
45
+ // ─── Auth Feature ────────────────────────────────────────────────────────────
46
+
47
+ export class AuthFeature implements Feature {
48
+ name = "auth";
49
+ dependencies = ["db"];
50
+
51
+ private auth: Auth | undefined;
52
+ private config: Required<Pick<AuthConfig, "secret" | "basePath">> & AuthConfig;
53
+ private kernel?: Kernel;
54
+ private authMode: "oidc" | "email";
55
+
56
+ constructor(config: AuthConfig) {
57
+ this.config = {
58
+ ...config,
59
+ basePath: config.basePath || "/api/sso",
60
+ baseURL: config.baseURL || "http://localhost:3000",
61
+ };
62
+
63
+ if (config.authMode === "oidc" || config.authMode === "email") {
64
+ this.authMode = config.authMode;
65
+ } else {
66
+ this.authMode = config.oidcConfig ? "oidc" : "email";
67
+ }
68
+ }
69
+
70
+ async initialize(kernel: Kernel): Promise<void> {
71
+ this.kernel = kernel;
72
+ const dbFeature = kernel.getFeature("db") as unknown as DbFeature;
73
+ if (!dbFeature) {
74
+ throw new Error("AuthFeature requires DbFeature");
75
+ }
76
+
77
+ const db = dbFeature.db;
78
+ const adapterType = dbFeature.adapter as "postgres" | "mysql" | "sqlite";
79
+
80
+ if (!adapterType) {
81
+ throw new Error("DbFeature must expose 'adapter' type (postgres, mysql, sqlite)");
82
+ }
83
+
84
+ this.auth = createBetterAuth({
85
+ db,
86
+ adapterType,
87
+ secret: this.config.secret,
88
+ baseURL: this.config.baseURL,
89
+ basePath: this.config.basePath,
90
+ trustedOrigins: this.config.trustedOrigins,
91
+ enableEmailPassword: true,
92
+ disableCSRFCheck: this.config.disableCSRFCheck,
93
+ socialProviders: this.config.socialProviders,
94
+ oidcConfig: this.config.oidcConfig,
95
+ });
96
+
97
+ const app = kernel.getApp();
98
+ app.use("*", async (c: Context, next: Next) => {
99
+ try {
100
+ const session = await this.auth!.api.getSession({
101
+ headers: c.req.raw.headers,
102
+ });
103
+
104
+ if (session && session.user) {
105
+ c.set("user", session.user);
106
+ c.set("authUser", session.user);
107
+ }
108
+ } catch (error) {
109
+ // Ignore session errors (just not logged in)
110
+ }
111
+ await next();
112
+ });
113
+
114
+ console.log("✅ Auth feature initialized (better-auth)");
115
+ }
116
+
117
+ routes(app: Hono): void {
118
+ if (!this.auth) {
119
+ throw new Error("Auth not initialized");
120
+ }
121
+
122
+ const base = this.config.basePath!;
123
+ const openapi = this.kernel?.getFeature("openapi") as OpenAPIFeature | undefined;
124
+
125
+ // Register explicit OpenAPI-documented routes
126
+ if (openapi) {
127
+ openapi.addRoute(
128
+ {
129
+ method: "post",
130
+ path: `${base}/sign-in/email`,
131
+ tags: ["Auth"],
132
+ summary: "Sign in with email and password",
133
+ request: {
134
+ body: {
135
+ content: { "application/json": { schema: SignInSchema } },
136
+ },
137
+ },
138
+ responses: {
139
+ 200: {
140
+ description: "Sign in successful",
141
+ content: { "application/json": { schema: AuthSuccessSchema } },
142
+ },
143
+ 401: { description: "Invalid credentials" },
144
+ },
145
+ },
146
+ (c: any) => this.auth!.handler(c.req.raw),
147
+ );
148
+
149
+ openapi.addRoute(
150
+ {
151
+ method: "post",
152
+ path: `${base}/sign-up/email`,
153
+ tags: ["Auth"],
154
+ summary: "Create a new account",
155
+ request: {
156
+ body: {
157
+ content: { "application/json": { schema: SignUpSchema } },
158
+ },
159
+ },
160
+ responses: {
161
+ 200: {
162
+ description: "Account created",
163
+ content: { "application/json": { schema: AuthSuccessSchema } },
164
+ },
165
+ 400: { description: "Validation error" },
166
+ },
167
+ },
168
+ (c: any) => this.auth!.handler(c.req.raw),
169
+ );
170
+
171
+ openapi.addRoute(
172
+ {
173
+ method: "post",
174
+ path: `${base}/sign-out`,
175
+ tags: ["Auth"],
176
+ summary: "Sign out",
177
+ responses: {
178
+ 200: { description: "Signed out" },
179
+ },
180
+ },
181
+ (c: any) => this.auth!.handler(c.req.raw),
182
+ );
183
+
184
+ openapi.addRoute(
185
+ {
186
+ method: "get",
187
+ path: `${base}/get-session`,
188
+ tags: ["Auth"],
189
+ summary: "Get current session",
190
+ responses: {
191
+ 200: {
192
+ description: "Current session",
193
+ content: { "application/json": { schema: SessionResponseSchema } },
194
+ },
195
+ },
196
+ },
197
+ (c: any) => this.auth!.handler(c.req.raw),
198
+ );
199
+ }
200
+
201
+ // Catch-all for all other auth routes (OAuth callbacks, etc.)
202
+ app.on(["POST", "GET"], `${base}/*`, (c) => {
203
+ return this.auth!.handler(c.req.raw);
204
+ });
205
+ }
206
+
207
+ getAuth(): Auth | undefined {
208
+ return this.auth;
209
+ }
210
+ }
211
+
212
+ export function requireAuth(kernel: Kernel) {
213
+ return async (c: Context, next: Next) => {
214
+ const authFeature = kernel.getFeature("auth") as AuthFeature;
215
+ if (!authFeature) throw new HTTPException(500, { message: "Auth not initialized" });
216
+
217
+ const auth = authFeature.getAuth();
218
+ if (!auth) throw new HTTPException(500, { message: "Auth not ready" });
219
+
220
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
221
+ if (!session || !session.user) {
222
+ throw new HTTPException(401, { message: "Unauthorized" });
223
+ }
224
+
225
+ c.set("user", session.user);
226
+ c.set("authUser", session.user);
227
+ await next();
228
+ };
229
+ }