@iskra-bun/web-kit 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- BaseStorageAdapter
3
- } from "./chunk-POXNRNTC.js";
4
-
5
1
  // src/kernel.ts
6
2
  import { Hono } from "hono";
7
3
  import { HTTPException } from "hono/http-exception";
@@ -263,9 +259,21 @@ var WebDriver = class {
263
259
  }
264
260
  init(app) {
265
261
  this.app = app;
262
+ this.setupSecurityHeaders();
266
263
  this.setupRoutes();
267
264
  this.setupOpenApi();
268
265
  }
266
+ // Apply the same standard security headers as the Kernel HTTP stack
267
+ // (web-kit/src/kernel.ts) so the standalone WebDriver server is at parity.
268
+ setupSecurityHeaders() {
269
+ this.server.use("*", async (c, next) => {
270
+ await next();
271
+ c.res.headers.set("X-Frame-Options", "SAMEORIGIN");
272
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
273
+ c.res.headers.set("X-XSS-Protection", "1; mode=block");
274
+ c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
275
+ });
276
+ }
269
277
  setupOpenApi() {
270
278
  if (this.options.openApi) {
271
279
  this.server.doc(this.options.openApi.path, {
@@ -334,7 +342,7 @@ var WebDriver = class {
334
342
  return c.json(result);
335
343
  } catch (err) {
336
344
  this.app.logger.error(err);
337
- return c.json({ error: err.message }, 500);
345
+ return c.json({ error: "Internal Server Error" }, 500);
338
346
  }
339
347
  });
340
348
  }
@@ -549,14 +557,20 @@ var HealthCheckFeature = class {
549
557
  name = "health";
550
558
  kernel;
551
559
  config;
560
+ readinessChecks;
552
561
  constructor(config = {}) {
553
562
  this.config = {
554
563
  path: config.path || "/health",
555
564
  readinessPath: config.readinessPath || "/health/ready",
556
565
  livenessPath: config.livenessPath || "/health/live",
557
- includeDetails: config.includeDetails !== void 0 ? config.includeDetails : true,
566
+ includeDetails: config.includeDetails !== void 0 ? config.includeDetails : false,
558
567
  checks: config.checks
559
568
  };
569
+ const initial = config.readinessChecks ?? {};
570
+ this.readinessChecks = new Map(Object.entries(initial));
571
+ }
572
+ addReadinessCheck(name, check) {
573
+ this.readinessChecks = new Map([...this.readinessChecks, [name, check]]);
560
574
  }
561
575
  async initialize(kernel) {
562
576
  this.kernel = kernel;
@@ -587,7 +601,8 @@ var HealthCheckFeature = class {
587
601
  try {
588
602
  customChecks[name] = await check(c);
589
603
  } catch (error) {
590
- customChecks[name] = { status: "error", error: String(error) };
604
+ console.error(`Health custom check "${name}" failed:`, error);
605
+ customChecks[name] = { status: "error" };
591
606
  }
592
607
  }
593
608
  response.customChecks = customChecks;
@@ -603,11 +618,30 @@ var HealthCheckFeature = class {
603
618
  report[key] = { status: "ok" };
604
619
  }
605
620
  } catch (e) {
606
- report[key] = { status: "error", error: String(e) };
621
+ console.error(`Health feature check "${key}" failed:`, e);
622
+ report[key] = { status: "error" };
607
623
  }
608
624
  }
609
625
  async handleReadinessCheck(c) {
610
- return c.json({ status: "ready" });
626
+ if (this.readinessChecks.size === 0) {
627
+ return c.json({ status: "ready" });
628
+ }
629
+ const results = {};
630
+ const failed = [];
631
+ for (const [name, check] of this.readinessChecks) {
632
+ try {
633
+ const passed = await check();
634
+ results[name] = passed;
635
+ if (!passed) failed.push(name);
636
+ } catch {
637
+ results[name] = false;
638
+ failed.push(name);
639
+ }
640
+ }
641
+ if (failed.length > 0) {
642
+ return c.json({ status: "not ready", checks: results, failed }, 503);
643
+ }
644
+ return c.json({ status: "ready", checks: results });
611
645
  }
612
646
  async handleLivenessCheck(c) {
613
647
  return c.json({
@@ -698,120 +732,9 @@ var RequestIdFeature = class {
698
732
  }
699
733
  };
700
734
 
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
735
  // src/features/storage/index.ts
736
+ import { createStorageAdapter } from "@iskra-bun/storage-kit";
737
+ import { BaseStorageAdapter as BaseStorageAdapter2, LocalStorageAdapter } from "@iskra-bun/storage-kit";
815
738
  var StorageFeature = class {
816
739
  constructor(config) {
817
740
  this.config = config;
@@ -820,14 +743,7 @@ var StorageFeature = class {
820
743
  name = "storage";
821
744
  adapter;
822
745
  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
- }
746
+ this.adapter = await createStorageAdapter(this.config);
831
747
  await this.adapter.connect();
832
748
  const app = kernel.getApp();
833
749
  app.use("*", async (c, next) => {
@@ -905,7 +821,8 @@ var DbFeature = class {
905
821
  }
906
822
  console.log("\u2705 DB connected successfully.");
907
823
  } catch (error) {
908
- console.error("\u274C Failed to connect to DB", error);
824
+ const message = error instanceof Error ? error.message : String(error);
825
+ console.error(`\u274C Failed to connect to DB: ${message}`);
909
826
  throw error;
910
827
  }
911
828
  const app = kernel.getApp();
@@ -1278,266 +1195,7 @@ var SessionFeature = class {
1278
1195
 
1279
1196
  // src/features/auth/index.ts
1280
1197
  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
1198
+ import { createBetterAuth } from "@iskra-bun/auth-kit";
1541
1199
  import { z as z2 } from "@hono/zod-openapi";
1542
1200
  var SignInSchema = z2.object({
1543
1201
  email: z2.string().email(),
@@ -1568,7 +1226,13 @@ var AuthFeature = class {
1568
1226
  config;
1569
1227
  kernel;
1570
1228
  authMode;
1571
- constructor(config) {
1229
+ createAuth;
1230
+ // The second parameter is an internal seam: it defaults to the real
1231
+ // createBetterAuth and lets tests inject a fake without globally mocking the
1232
+ // @iskra-bun/auth-kit module (bun's mock.module is process-global and cannot
1233
+ // be restored, which would otherwise leak into auth-kit's own test suite).
1234
+ constructor(config, createAuth = createBetterAuth) {
1235
+ this.createAuth = createAuth;
1572
1236
  this.config = {
1573
1237
  ...config,
1574
1238
  basePath: config.basePath || "/api/sso",
@@ -1591,7 +1255,8 @@ var AuthFeature = class {
1591
1255
  if (!adapterType) {
1592
1256
  throw new Error("DbFeature must expose 'adapter' type (postgres, mysql, sqlite)");
1593
1257
  }
1594
- this.auth = createBetterAuth({
1258
+ const disableCSRFCheck = process.env.NODE_ENV !== "production" ? this.config.disableCSRFCheck === true : false;
1259
+ this.auth = this.createAuth({
1595
1260
  db,
1596
1261
  adapterType,
1597
1262
  secret: this.config.secret,
@@ -1599,11 +1264,12 @@ var AuthFeature = class {
1599
1264
  basePath: this.config.basePath,
1600
1265
  trustedOrigins: this.config.trustedOrigins,
1601
1266
  enableEmailPassword: true,
1602
- disableCSRFCheck: this.config.disableCSRFCheck,
1267
+ disableCSRFCheck,
1603
1268
  socialProviders: this.config.socialProviders,
1604
1269
  oidcConfig: this.config.oidcConfig
1605
1270
  });
1606
1271
  const app = kernel.getApp();
1272
+ app.use(`${this.config.basePath}/*`, this.authRateLimitMiddleware());
1607
1273
  app.use("*", async (c, next) => {
1608
1274
  try {
1609
1275
  const session = await this.auth.api.getSession({
@@ -1614,11 +1280,30 @@ var AuthFeature = class {
1614
1280
  c.set("authUser", session.user);
1615
1281
  }
1616
1282
  } catch (error) {
1283
+ const logger = c.get("logger");
1284
+ if (logger?.debug) logger.debug("Auth session read failed", { error });
1617
1285
  }
1618
1286
  await next();
1619
1287
  });
1620
1288
  console.log("\u2705 Auth feature initialized (better-auth)");
1621
1289
  }
1290
+ // ─── Auth-route rate limiting ────────────────────────────────────────────
1291
+ authRateLimitHits = /* @__PURE__ */ new Map();
1292
+ authRateLimitWindowMs = 15 * 60 * 1e3;
1293
+ authRateLimitMax = 20;
1294
+ authRateLimitMiddleware() {
1295
+ return async (c, next) => {
1296
+ const ip = c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "unknown";
1297
+ const now = Date.now();
1298
+ const entry = this.authRateLimitHits.get(ip);
1299
+ const next_entry = !entry || now > entry.expiresAt ? { count: 1, expiresAt: now + this.authRateLimitWindowMs } : { count: entry.count + 1, expiresAt: entry.expiresAt };
1300
+ this.authRateLimitHits.set(ip, next_entry);
1301
+ if (next_entry.count > this.authRateLimitMax) {
1302
+ throw new HTTPException4(429, { message: "Too many authentication attempts" });
1303
+ }
1304
+ await next();
1305
+ };
1306
+ }
1622
1307
  routes(app) {
1623
1308
  if (!this.auth) {
1624
1309
  throw new Error("Auth not initialized");
@@ -1960,6 +1645,7 @@ function requireRole(role) {
1960
1645
 
1961
1646
  // src/features/api-key.ts
1962
1647
  import { HTTPException as HTTPException7 } from "hono/http-exception";
1648
+ import { createHash } from "crypto";
1963
1649
  var ApiKeyStore = class {
1964
1650
  constructor(config, kernel) {
1965
1651
  this.config = config;
@@ -2000,8 +1686,8 @@ var ApiKeyStore = class {
2000
1686
  }
2001
1687
  console.log(`\u2705 Loaded ${this.staticKeysMap.size} static API keys`);
2002
1688
  }
2003
- generateId(key) {
2004
- return key.substring(0, 8);
1689
+ generateId(_key) {
1690
+ return crypto.randomUUID();
2005
1691
  }
2006
1692
  compareKeys(a, b) {
2007
1693
  if (a.length !== b.length) return false;
@@ -2015,10 +1701,14 @@ var ApiKeyStore = class {
2015
1701
  if (!expiresAt) return false;
2016
1702
  return /* @__PURE__ */ new Date() > expiresAt;
2017
1703
  }
1704
+ cacheKeyFor(key) {
1705
+ const hash = createHash("sha256").update(key).digest("hex");
1706
+ return `apikey:${hash}`;
1707
+ }
2018
1708
  async validate(key) {
2019
1709
  if (!key) return { isValid: false, error: "API key is required" };
2020
1710
  const cache = this.getCache();
2021
- const cacheKey = `apikey:${key}`;
1711
+ const cacheKey = this.cacheKeyFor(key);
2022
1712
  if (cache) {
2023
1713
  try {
2024
1714
  const cached = await cache.get(cacheKey);
@@ -2034,15 +1724,15 @@ var ApiKeyStore = class {
2034
1724
  if (this.isExpired(metadata.expiresAt)) {
2035
1725
  return { isValid: false, error: "API key has expired" };
2036
1726
  }
2037
- metadata.lastUsedAt = /* @__PURE__ */ new Date();
1727
+ const usedMetadata = { ...metadata, lastUsedAt: /* @__PURE__ */ new Date() };
2038
1728
  if (cache) {
2039
1729
  const ttl = this.config.cacheTtl ? Math.floor(this.config.cacheTtl / 1e3) : 300;
2040
1730
  try {
2041
- await cache.set(cacheKey, JSON.stringify(metadata), ttl);
1731
+ await cache.set(cacheKey, JSON.stringify(usedMetadata), ttl);
2042
1732
  } catch {
2043
1733
  }
2044
1734
  }
2045
- return { isValid: true, key: metadata };
1735
+ return { isValid: true, key: usedMetadata };
2046
1736
  }
2047
1737
  }
2048
1738
  return { isValid: false, error: "Invalid API key" };
@@ -2068,14 +1758,13 @@ var ApiKeyFeature = class {
2068
1758
  store;
2069
1759
  config;
2070
1760
  constructor(config = {}) {
2071
- this.config = {
1761
+ const defaults = {
2072
1762
  staticKeys: config.staticKeys || [],
2073
1763
  headerName: config.headerName || "X-API-Key",
2074
1764
  queryParamName: config.queryParamName || "api_key",
2075
1765
  extractStrategies: config.extractStrategies || ["header", "bearer"],
2076
- // Defaults for other optional properties
2077
- vaultService: void 0,
2078
- customExtractor: void 0,
1766
+ vaultService: config.vaultService,
1767
+ customExtractor: config.customExtractor,
2079
1768
  enableCache: config.enableCache ?? true,
2080
1769
  cacheTtl: config.cacheTtl ?? 3e5,
2081
1770
  requireScopes: config.requireScopes ?? false,
@@ -2083,6 +1772,7 @@ var ApiKeyFeature = class {
2083
1772
  onError: config.onError,
2084
1773
  onValidated: config.onValidated
2085
1774
  };
1775
+ this.config = defaults;
2086
1776
  }
2087
1777
  async initialize(kernel) {
2088
1778
  this.store = new ApiKeyStore(this.config, kernel);
@@ -2103,6 +1793,9 @@ var ApiKeyFeature = class {
2103
1793
  if (!result.isValid) {
2104
1794
  throw new HTTPException7(401, { message: result.error || "Invalid API key" });
2105
1795
  }
1796
+ if (this.config.requireScopes && !(result.key?.scopes && result.key.scopes.length > 0)) {
1797
+ throw new HTTPException7(403, { message: "API key has no scopes" });
1798
+ }
2106
1799
  c.set("apiKey", result.key);
2107
1800
  c.set("apiKeyScopes", result.key?.scopes || []);
2108
1801
  c.set("hasScope", (scope) => this.store.hasScopes(result.key, [scope]));
@@ -2112,11 +1805,11 @@ var ApiKeyFeature = class {
2112
1805
  });
2113
1806
  console.log("\u2705 API Key feature initialized");
2114
1807
  }
2115
- shouldSkipPath(path2) {
1808
+ shouldSkipPath(path) {
2116
1809
  if (!this.config.skipPaths?.length) return false;
2117
1810
  return this.config.skipPaths.some((skip) => {
2118
- if (skip.endsWith("*")) return path2.startsWith(skip.slice(0, -1));
2119
- return path2 === skip;
1811
+ if (skip.endsWith("*")) return path.startsWith(skip.slice(0, -1));
1812
+ return path === skip;
2120
1813
  });
2121
1814
  }
2122
1815
  extractApiKey(c) {
@@ -2158,6 +1851,24 @@ function requireScope(...scopes) {
2158
1851
  // src/features/csrf.ts
2159
1852
  import { HTTPException as HTTPException8 } from "hono/http-exception";
2160
1853
  import { getCookie as getCookie2, setCookie as setCookie2 } from "hono/cookie";
1854
+ import { createHmac as createHmac2, timingSafeEqual, randomUUID } from "crypto";
1855
+ function constantTimeEqual(a, b) {
1856
+ const aBuf = Buffer.from(a);
1857
+ const bBuf = Buffer.from(b);
1858
+ if (aBuf.length !== bBuf.length) return false;
1859
+ return timingSafeEqual(aBuf, bBuf);
1860
+ }
1861
+ function signCsrfToken(random, secret) {
1862
+ const signature = createHmac2("sha256", secret).update(random).digest("base64url");
1863
+ return `${random}.${signature}`;
1864
+ }
1865
+ function verifyCsrfToken(token, secret) {
1866
+ const lastDot = token.lastIndexOf(".");
1867
+ if (lastDot === -1) return false;
1868
+ const random = token.substring(0, lastDot);
1869
+ const expected = signCsrfToken(random, secret);
1870
+ return constantTimeEqual(token, expected);
1871
+ }
2161
1872
  var CsrfFeature = class {
2162
1873
  name = "csrf";
2163
1874
  config;
@@ -2188,7 +1899,7 @@ var CsrfFeature = class {
2188
1899
  const method = c.req.method.toUpperCase();
2189
1900
  let token = getCookie2(c, this.config.cookieName);
2190
1901
  if (!token) {
2191
- token = crypto.randomUUID().replace(/-/g, "");
1902
+ token = signCsrfToken(randomUUID().replace(/-/g, ""), this.config.secret);
2192
1903
  setCookie2(c, this.config.cookieName, token, {
2193
1904
  httpOnly: this.config.cookieOptions.httpOnly,
2194
1905
  secure: this.config.cookieOptions.secure,
@@ -2209,16 +1920,19 @@ var CsrfFeature = class {
2209
1920
  await next();
2210
1921
  }
2211
1922
  async validateToken(c, expectedToken) {
1923
+ if (!verifyCsrfToken(expectedToken, this.config.secret)) return false;
2212
1924
  const headerToken = c.req.header(this.config.headerName);
2213
- if (headerToken === expectedToken) return true;
1925
+ if (headerToken && constantTimeEqual(headerToken, expectedToken)) return true;
2214
1926
  try {
2215
1927
  const contentType = c.req.header("content-type");
2216
1928
  if (contentType?.includes("application/x-www-form-urlencoded")) {
2217
1929
  const body = await c.req.parseBody();
2218
1930
  const bodyToken = body._csrf || body[this.config.cookieName];
2219
- if (String(bodyToken) === expectedToken) return true;
1931
+ if (bodyToken && constantTimeEqual(String(bodyToken), expectedToken)) return true;
2220
1932
  }
2221
- } catch {
1933
+ } catch (error) {
1934
+ const logger = c.get("logger");
1935
+ if (logger?.debug) logger.debug("CSRF body token parse failed", { error });
2222
1936
  }
2223
1937
  return false;
2224
1938
  }
@@ -2295,9 +2009,9 @@ function createValidationMiddleware(schema, options = {}) {
2295
2009
  function extendHonoWithValidation(app) {
2296
2010
  const methods = ["get", "post", "put", "delete"];
2297
2011
  for (const method of methods) {
2298
- app[`${method}Validated`] = function(path2, schema, handler, options) {
2012
+ app[`${method}Validated`] = function(path, schema, handler, options) {
2299
2013
  const middleware = createValidationMiddleware(schema, options);
2300
- this[method](path2, middleware, handler);
2014
+ this[method](path, middleware, handler);
2301
2015
  return this;
2302
2016
  };
2303
2017
  }
@@ -2407,9 +2121,9 @@ function createJsonSchemaValidationMiddleware(schema, options = {}) {
2407
2121
  function extendHonoWithJsonSchemaValidation(app) {
2408
2122
  const methods = ["get", "post", "put", "delete"];
2409
2123
  for (const method of methods) {
2410
- app[`${method}JsonValidated`] = function(path2, schema, handler, options) {
2124
+ app[`${method}JsonValidated`] = function(path, schema, handler, options) {
2411
2125
  const middleware = createJsonSchemaValidationMiddleware(schema, options);
2412
- this[method](path2, middleware, handler);
2126
+ this[method](path, middleware, handler);
2413
2127
  return this;
2414
2128
  };
2415
2129
  }
@@ -2526,6 +2240,15 @@ var UploadHelper = class {
2526
2240
  getBasePath() {
2527
2241
  return this.basePath;
2528
2242
  }
2243
+ // Defense-in-depth: reduce an attacker-controlled filename to a safe basename
2244
+ // and strip it to an allowlisted charset so traversal segments ("../",
2245
+ // "..\\", absolute paths) can never escape the project base path. storage-kit's
2246
+ // sanitizePath remains the primary control; this is a second, independent gate.
2247
+ safeBasename(filename) {
2248
+ const base = filename.split(/[\\/]/).pop() || "";
2249
+ const cleaned = base.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^\.+/, "");
2250
+ return cleaned.length > 0 ? cleaned : "file";
2251
+ }
2529
2252
  buildPath(filename, subfolder) {
2530
2253
  if (subfolder) {
2531
2254
  const normalized = subfolder.replace(/^\/+|\/+$/g, "");
@@ -2556,7 +2279,8 @@ var UploadHelper = class {
2556
2279
  if (!file || !(file instanceof File)) throw new Error(`No file found in field: ${fieldName}`);
2557
2280
  const data = new Uint8Array(await file.arrayBuffer());
2558
2281
  const opts = { ...options, contentType: options?.contentType || file.type };
2559
- return this.upload(file.name, data, subfolder, opts);
2282
+ const safeName = this.safeBasename(file.name);
2283
+ return this.upload(safeName, data, subfolder, opts);
2560
2284
  }
2561
2285
  async list(subfolder) {
2562
2286
  const prefix = subfolder ? `${this.basePath}${subfolder.replace(/^\/+|\/+$/g, "")}/` : this.basePath;
@@ -2719,15 +2443,44 @@ var OtelTracingFeature = class {
2719
2443
  };
2720
2444
 
2721
2445
  // 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 };
2446
+ import { createEmailAdapter } from "@iskra-bun/mailer-kit";
2447
+ import { MockEmailAdapter } from "@iskra-bun/mailer-kit";
2448
+ var CRLF = /[\r\n]/;
2449
+ function assertNoCrlf(value, label) {
2450
+ if (CRLF.test(value)) {
2451
+ throw new Error(`Invalid ${label}: control characters (CR/LF) are not allowed`);
2726
2452
  }
2727
- async sendTemplate(name, to, data) {
2728
- return this.send({ to, subject: `Template: ${name}`, html: `Template ${name} with data: ${JSON.stringify(data)}` });
2453
+ }
2454
+ function assertRecipients(to, label) {
2455
+ if (to === void 0) return;
2456
+ const recipients = Array.isArray(to) ? to : [to];
2457
+ for (const recipient of recipients) assertNoCrlf(recipient, label);
2458
+ }
2459
+ function validateMessage(message) {
2460
+ assertRecipients(message.to, "recipient");
2461
+ assertRecipients(message.cc, "cc recipient");
2462
+ assertRecipients(message.bcc, "bcc recipient");
2463
+ if (message.replyTo !== void 0) assertNoCrlf(message.replyTo, "replyTo");
2464
+ assertNoCrlf(message.subject, "subject");
2465
+ if (message.headers) {
2466
+ for (const [name, value] of Object.entries(message.headers)) {
2467
+ assertNoCrlf(name, "header name");
2468
+ assertNoCrlf(value, "header value");
2469
+ }
2729
2470
  }
2730
- };
2471
+ }
2472
+ function withValidation(adapter) {
2473
+ return {
2474
+ send: (message) => {
2475
+ validateMessage(message);
2476
+ return adapter.send(message);
2477
+ },
2478
+ sendTemplate: (templateName, to, data) => {
2479
+ assertRecipients(to, "recipient");
2480
+ return adapter.sendTemplate(templateName, to, data);
2481
+ }
2482
+ };
2483
+ }
2731
2484
  var EmailFeature = class {
2732
2485
  constructor(config) {
2733
2486
  this.config = config;
@@ -2736,33 +2489,12 @@ var EmailFeature = class {
2736
2489
  name = "email";
2737
2490
  adapter;
2738
2491
  async initialize(kernel) {
2739
- this.adapter = await this.createAdapter(this.config);
2492
+ this.adapter = withValidation(await createEmailAdapter(this.config));
2740
2493
  const app = kernel.getApp();
2741
2494
  app.use("*", async (c, next) => {
2742
2495
  if (this.adapter) c.set("email", this.adapter);
2743
2496
  await next();
2744
2497
  });
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
2498
  }
2767
2499
  getAdapter() {
2768
2500
  if (!this.adapter) throw new Error("Email not initialized");
@@ -2774,7 +2506,7 @@ export {
2774
2506
  ApiKeyStore,
2775
2507
  AuthError,
2776
2508
  AuthFeature,
2777
- BaseStorageAdapter,
2509
+ BaseStorageAdapter2 as BaseStorageAdapter,
2778
2510
  CacheFeature,
2779
2511
  ConflictError,
2780
2512
  CorsFeature,