@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/CHANGELOG.md +41 -0
- package/dist/index.d.ts +38 -192
- package/dist/index.js +178 -446
- package/dist/index.js.map +1 -1
- package/package.json +6 -7
- package/src/features/api-key.ts +44 -14
- package/src/features/auth/index.ts +53 -6
- package/src/features/csrf.ts +50 -4
- package/src/features/db.ts +26 -11
- package/src/features/email/index.ts +50 -68
- package/src/features/health.ts +42 -6
- package/src/features/storage/index.ts +5 -14
- package/src/features/upload/helper.ts +13 -2
- package/src/server.ts +17 -1
- package/src/types.ts +3 -0
- package/dist/chunk-POXNRNTC.js +0 -51
- package/dist/chunk-POXNRNTC.js.map +0 -1
- package/dist/mailgun-Z46GZJNI.js +0 -83
- package/dist/mailgun-Z46GZJNI.js.map +0 -1
- package/dist/s3-7IG4ESFW.js +0 -171
- package/dist/s3-7IG4ESFW.js.map +0 -1
- package/dist/sendgrid-UK2GSBEF.js +0 -43
- package/dist/sendgrid-UK2GSBEF.js.map +0 -1
- package/dist/smtp-WJDLYKD5.js +0 -50
- package/dist/smtp-WJDLYKD5.js.map +0 -1
- package/src/features/auth/better-auth-config.ts +0 -160
- package/src/features/auth/schema.ts +0 -174
- package/src/features/auth/types.ts +0 -114
- package/src/features/email/providers/mailgun.ts +0 -99
- package/src/features/email/providers/sendgrid.ts +0 -42
- package/src/features/email/providers/smtp.ts +0 -51
- package/src/features/storage/adapters/local.ts +0 -133
- package/src/features/storage/adapters/s3.ts +0 -193
- package/src/features/storage/base.ts +0 -112
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:
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
621
|
+
console.error(`Health feature check "${key}" failed:`, e);
|
|
622
|
+
report[key] = { status: "error" };
|
|
607
623
|
}
|
|
608
624
|
}
|
|
609
625
|
async handleReadinessCheck(c) {
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
2004
|
-
return
|
|
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 =
|
|
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
|
|
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(
|
|
1731
|
+
await cache.set(cacheKey, JSON.stringify(usedMetadata), ttl);
|
|
2042
1732
|
} catch {
|
|
2043
1733
|
}
|
|
2044
1734
|
}
|
|
2045
|
-
return { isValid: true, key:
|
|
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
|
-
|
|
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
|
-
|
|
2077
|
-
|
|
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(
|
|
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
|
|
2119
|
-
return
|
|
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 =
|
|
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
|
|
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)
|
|
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(
|
|
2012
|
+
app[`${method}Validated`] = function(path, schema, handler, options) {
|
|
2299
2013
|
const middleware = createValidationMiddleware(schema, options);
|
|
2300
|
-
this[method](
|
|
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(
|
|
2124
|
+
app[`${method}JsonValidated`] = function(path, schema, handler, options) {
|
|
2411
2125
|
const middleware = createJsonSchemaValidationMiddleware(schema, options);
|
|
2412
|
-
this[method](
|
|
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
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
2728
|
-
|
|
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
|
|
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,
|