@sonicjs-cms/core 2.8.1 → 2.8.2
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/{app-CYEm1ytG.d.cts → app-DnQ26Lho.d.cts} +3 -0
- package/dist/{app-CYEm1ytG.d.ts → app-DnQ26Lho.d.ts} +3 -0
- package/dist/{chunk-S6K2H2TS.cjs → chunk-3G7XX4UI.cjs} +9 -9
- package/dist/{chunk-S6K2H2TS.cjs.map → chunk-3G7XX4UI.cjs.map} +1 -1
- package/dist/{chunk-JVRRG36J.cjs → chunk-3U5YHS4G.cjs} +2660 -2557
- package/dist/chunk-3U5YHS4G.cjs.map +1 -0
- package/dist/{chunk-FZRZYQYU.js → chunk-4Z5BQZT6.js} +2501 -2398
- package/dist/chunk-4Z5BQZT6.js.map +1 -0
- package/dist/{chunk-H7AMQWVI.js → chunk-74XCYEI7.js} +3 -3
- package/dist/{chunk-H7AMQWVI.js.map → chunk-74XCYEI7.js.map} +1 -1
- package/dist/chunk-FUUVSYVQ.js +541 -0
- package/dist/chunk-FUUVSYVQ.js.map +1 -0
- package/dist/{chunk-7Q2XPM2U.js → chunk-I6REMSMF.js} +21 -2
- package/dist/chunk-I6REMSMF.js.map +1 -0
- package/dist/{chunk-VCH6HXVP.js → chunk-JJS7JZCH.js} +58 -4
- package/dist/chunk-JJS7JZCH.js.map +1 -0
- package/dist/{chunk-WDQZYCQO.cjs → chunk-JSHIGVIF.cjs} +32 -39
- package/dist/chunk-JSHIGVIF.cjs.map +1 -0
- package/dist/{chunk-SHCYIZAN.cjs → chunk-LTKV7AE5.cjs} +58 -4
- package/dist/chunk-LTKV7AE5.cjs.map +1 -0
- package/dist/chunk-MNWKYY5E.cjs +44 -0
- package/dist/chunk-MNWKYY5E.cjs.map +1 -0
- package/dist/chunk-TQABQWOP.js +39 -0
- package/dist/chunk-TQABQWOP.js.map +1 -0
- package/dist/{chunk-SKLRRFJJ.cjs → chunk-VGSZWZP3.cjs} +21 -2
- package/dist/chunk-VGSZWZP3.cjs.map +1 -0
- package/dist/{chunk-KAT3OKHE.js → chunk-WI5ESQKT.js} +33 -37
- package/dist/chunk-WI5ESQKT.js.map +1 -0
- package/dist/chunk-ZWKCL46S.cjs +568 -0
- package/dist/chunk-ZWKCL46S.cjs.map +1 -0
- package/dist/{filter-bar.template-By4jeiw_.d.cts → filter-bar.template-Daw8ZDoq.d.cts} +1 -0
- package/dist/{filter-bar.template-By4jeiw_.d.ts → filter-bar.template-Daw8ZDoq.d.ts} +1 -0
- package/dist/index.cjs +112 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +43 -23
- package/dist/middleware.d.cts +86 -6
- package/dist/middleware.d.ts +86 -6
- package/dist/middleware.js +2 -2
- package/dist/migrations-F3G6CTRS.cjs +13 -0
- package/dist/{migrations-76NR5BVF.cjs.map → migrations-F3G6CTRS.cjs.map} +1 -1
- package/dist/migrations-LLNEST75.js +4 -0
- package/dist/{migrations-2NTJ44OR.js.map → migrations-LLNEST75.js.map} +1 -1
- package/dist/routes.cjs +29 -28
- package/dist/routes.d.cts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +6 -5
- package/dist/services.cjs +2 -2
- package/dist/services.js +1 -1
- package/dist/templates.cjs +20 -19
- package/dist/templates.d.cts +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +3 -2
- package/dist/utils.cjs +24 -23
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/dist/{version-vktVAxhe.d.cts → version-C_CXrN_T.d.cts} +5 -0
- package/dist/{version-vktVAxhe.d.ts → version-C_CXrN_T.d.ts} +5 -0
- package/migrations/032_user_profiles.sql +36 -0
- package/package.json +2 -2
- package/dist/chunk-7Q2XPM2U.js.map +0 -1
- package/dist/chunk-FZRZYQYU.js.map +0 -1
- package/dist/chunk-GIWIJNBH.cjs +0 -243
- package/dist/chunk-GIWIJNBH.cjs.map +0 -1
- package/dist/chunk-JVRRG36J.cjs.map +0 -1
- package/dist/chunk-KAT3OKHE.js.map +0 -1
- package/dist/chunk-QWTS6NSP.js +0 -221
- package/dist/chunk-QWTS6NSP.js.map +0 -1
- package/dist/chunk-SHCYIZAN.cjs.map +0 -1
- package/dist/chunk-SKLRRFJJ.cjs.map +0 -1
- package/dist/chunk-VCH6HXVP.js.map +0 -1
- package/dist/chunk-WDQZYCQO.cjs.map +0 -1
- package/dist/migrations-2NTJ44OR.js +0 -4
- package/dist/migrations-76NR5BVF.cjs +0 -13
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkMPT5PA6U_cjs = require('./chunk-MPT5PA6U.cjs');
|
|
4
|
+
var chunkVGSZWZP3_cjs = require('./chunk-VGSZWZP3.cjs');
|
|
5
|
+
var chunkRCQ2HIQD_cjs = require('./chunk-RCQ2HIQD.cjs');
|
|
6
|
+
var jwt = require('hono/jwt');
|
|
7
|
+
var cookie = require('hono/cookie');
|
|
8
|
+
|
|
9
|
+
// src/middleware/bootstrap.ts
|
|
10
|
+
var bootstrapComplete = false;
|
|
11
|
+
function verifySecurityConfig(env) {
|
|
12
|
+
const warnings = [];
|
|
13
|
+
if (!env.JWT_SECRET) {
|
|
14
|
+
warnings.push(
|
|
15
|
+
"JWT_SECRET is not set \u2014 using hardcoded fallback. Set via `wrangler secret put JWT_SECRET`"
|
|
16
|
+
);
|
|
17
|
+
} else if (env.JWT_SECRET.includes("change-in-production")) {
|
|
18
|
+
warnings.push(
|
|
19
|
+
"JWT_SECRET contains the default value \u2014 tokens are forgeable. Generate a strong random secret"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!env.CORS_ORIGINS) {
|
|
23
|
+
warnings.push(
|
|
24
|
+
"CORS_ORIGINS is not set \u2014 all cross-origin API requests will be rejected"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (!env.ENVIRONMENT) {
|
|
28
|
+
warnings.push(
|
|
29
|
+
'ENVIRONMENT is not set \u2014 HSTS header will not be applied. Set to "production" or "development"'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (warnings.length === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const isProduction = env.ENVIRONMENT === "production";
|
|
36
|
+
for (const warning of warnings) {
|
|
37
|
+
console.warn(`[SonicJS Security] ${warning}`);
|
|
38
|
+
}
|
|
39
|
+
if (isProduction) {
|
|
40
|
+
const hasCritical = !env.JWT_SECRET || env.JWT_SECRET.includes("change-in-production");
|
|
41
|
+
if (hasCritical) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"[SonicJS Security] CRITICAL: Production deployment is missing a secure JWT_SECRET. Set it via `wrangler secret put JWT_SECRET` before deploying."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function bootstrapMiddleware(config = {}) {
|
|
49
|
+
return async (c, next) => {
|
|
50
|
+
if (bootstrapComplete) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
const path = c.req.path;
|
|
54
|
+
if (path.startsWith("/images/") || path.startsWith("/assets/") || path === "/health" || path.endsWith(".js") || path.endsWith(".css") || path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".ico")) {
|
|
55
|
+
return next();
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
console.log("[Bootstrap] Starting system initialization...");
|
|
59
|
+
console.log("[Bootstrap] Running database migrations...");
|
|
60
|
+
const migrationService = new chunkVGSZWZP3_cjs.MigrationService(c.env.DB);
|
|
61
|
+
await migrationService.runPendingMigrations();
|
|
62
|
+
console.log("[Bootstrap] Syncing collection configurations...");
|
|
63
|
+
try {
|
|
64
|
+
await chunkMPT5PA6U_cjs.syncCollections(c.env.DB);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("[Bootstrap] Error syncing collections:", error);
|
|
67
|
+
}
|
|
68
|
+
if (!config.plugins?.disableAll) {
|
|
69
|
+
console.log("[Bootstrap] Bootstrapping core plugins...");
|
|
70
|
+
const bootstrapService = new chunkMPT5PA6U_cjs.PluginBootstrapService(c.env.DB);
|
|
71
|
+
const needsBootstrap = await bootstrapService.isBootstrapNeeded();
|
|
72
|
+
if (needsBootstrap) {
|
|
73
|
+
await bootstrapService.bootstrapCorePlugins();
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
console.log("[Bootstrap] Plugin bootstrap skipped (disableAll is true)");
|
|
77
|
+
}
|
|
78
|
+
bootstrapComplete = true;
|
|
79
|
+
console.log("[Bootstrap] System initialization completed");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("[Bootstrap] Error during system initialization:", error);
|
|
82
|
+
}
|
|
83
|
+
verifySecurityConfig(c.env);
|
|
84
|
+
return next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
|
|
88
|
+
var AuthManager = class {
|
|
89
|
+
static async generateToken(userId, email, role, secret) {
|
|
90
|
+
const payload = {
|
|
91
|
+
userId,
|
|
92
|
+
email,
|
|
93
|
+
role,
|
|
94
|
+
exp: Math.floor(Date.now() / 1e3) + 60 * 60 * 24,
|
|
95
|
+
// 24 hours
|
|
96
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
97
|
+
};
|
|
98
|
+
return await jwt.sign(payload, secret || JWT_SECRET_FALLBACK, "HS256");
|
|
99
|
+
}
|
|
100
|
+
static async verifyToken(token, secret) {
|
|
101
|
+
try {
|
|
102
|
+
const payload = await jwt.verify(token, secret || JWT_SECRET_FALLBACK, "HS256");
|
|
103
|
+
if (payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return payload;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error("Token verification failed:", error);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
static async hashPassword(password) {
|
|
113
|
+
const iterations = 1e5;
|
|
114
|
+
const salt = new Uint8Array(16);
|
|
115
|
+
crypto.getRandomValues(salt);
|
|
116
|
+
const encoder = new TextEncoder();
|
|
117
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
118
|
+
"raw",
|
|
119
|
+
encoder.encode(password),
|
|
120
|
+
"PBKDF2",
|
|
121
|
+
false,
|
|
122
|
+
["deriveBits"]
|
|
123
|
+
);
|
|
124
|
+
const hashBuffer = await crypto.subtle.deriveBits(
|
|
125
|
+
{
|
|
126
|
+
name: "PBKDF2",
|
|
127
|
+
salt,
|
|
128
|
+
iterations,
|
|
129
|
+
hash: "SHA-256"
|
|
130
|
+
},
|
|
131
|
+
keyMaterial,
|
|
132
|
+
256
|
|
133
|
+
);
|
|
134
|
+
const saltHex = Array.from(salt).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
135
|
+
const hashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
136
|
+
return `pbkdf2:${iterations}:${saltHex}:${hashHex}`;
|
|
137
|
+
}
|
|
138
|
+
static async hashPasswordLegacy(password) {
|
|
139
|
+
const encoder = new TextEncoder();
|
|
140
|
+
const data = encoder.encode(password + "salt-change-in-production");
|
|
141
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
142
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
143
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
144
|
+
}
|
|
145
|
+
static async verifyPassword(password, storedHash) {
|
|
146
|
+
if (storedHash.startsWith("pbkdf2:")) {
|
|
147
|
+
const parts = storedHash.split(":");
|
|
148
|
+
if (parts.length !== 4) return false;
|
|
149
|
+
const iterationsStr = parts[1];
|
|
150
|
+
const saltHex = parts[2];
|
|
151
|
+
const expectedHashHex = parts[3];
|
|
152
|
+
const iterations = parseInt(iterationsStr, 10);
|
|
153
|
+
const saltBytes = saltHex.match(/.{2}/g);
|
|
154
|
+
if (!saltBytes) return false;
|
|
155
|
+
const salt = new Uint8Array(saltBytes.map((byte) => parseInt(byte, 16)));
|
|
156
|
+
const encoder = new TextEncoder();
|
|
157
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
158
|
+
"raw",
|
|
159
|
+
encoder.encode(password),
|
|
160
|
+
"PBKDF2",
|
|
161
|
+
false,
|
|
162
|
+
["deriveBits"]
|
|
163
|
+
);
|
|
164
|
+
const hashBuffer = await crypto.subtle.deriveBits(
|
|
165
|
+
{
|
|
166
|
+
name: "PBKDF2",
|
|
167
|
+
salt,
|
|
168
|
+
iterations,
|
|
169
|
+
hash: "SHA-256"
|
|
170
|
+
},
|
|
171
|
+
keyMaterial,
|
|
172
|
+
256
|
|
173
|
+
);
|
|
174
|
+
const actualHashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
175
|
+
if (actualHashHex.length !== expectedHashHex.length) return false;
|
|
176
|
+
let result2 = 0;
|
|
177
|
+
for (let i = 0; i < actualHashHex.length; i++) {
|
|
178
|
+
result2 |= actualHashHex.charCodeAt(i) ^ expectedHashHex.charCodeAt(i);
|
|
179
|
+
}
|
|
180
|
+
return result2 === 0;
|
|
181
|
+
}
|
|
182
|
+
const legacyHash = await this.hashPasswordLegacy(password);
|
|
183
|
+
if (legacyHash.length !== storedHash.length) return false;
|
|
184
|
+
let result = 0;
|
|
185
|
+
for (let i = 0; i < legacyHash.length; i++) {
|
|
186
|
+
result |= legacyHash.charCodeAt(i) ^ storedHash.charCodeAt(i);
|
|
187
|
+
}
|
|
188
|
+
return result === 0;
|
|
189
|
+
}
|
|
190
|
+
static isLegacyHash(storedHash) {
|
|
191
|
+
return !storedHash.startsWith("pbkdf2:");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Set authentication cookie - useful for plugins implementing alternative auth methods
|
|
195
|
+
* @param c - Hono context
|
|
196
|
+
* @param token - JWT token to set in cookie
|
|
197
|
+
* @param options - Optional cookie configuration
|
|
198
|
+
*/
|
|
199
|
+
static setAuthCookie(c, token, options) {
|
|
200
|
+
cookie.setCookie(c, "auth_token", token, {
|
|
201
|
+
httpOnly: options?.httpOnly ?? true,
|
|
202
|
+
secure: options?.secure ?? true,
|
|
203
|
+
sameSite: options?.sameSite ?? "Strict",
|
|
204
|
+
maxAge: options?.maxAge ?? 60 * 60 * 24
|
|
205
|
+
// 24 hours default
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
var requireAuth = () => {
|
|
210
|
+
return async (c, next) => {
|
|
211
|
+
try {
|
|
212
|
+
let token = c.req.header("Authorization")?.replace("Bearer ", "");
|
|
213
|
+
if (!token) {
|
|
214
|
+
token = cookie.getCookie(c, "auth_token");
|
|
215
|
+
}
|
|
216
|
+
if (!token) {
|
|
217
|
+
const acceptHeader = c.req.header("Accept") || "";
|
|
218
|
+
if (acceptHeader.includes("text/html")) {
|
|
219
|
+
return c.redirect("/auth/login?error=Please login to access the admin area");
|
|
220
|
+
}
|
|
221
|
+
return c.json({ error: "Authentication required" }, 401);
|
|
222
|
+
}
|
|
223
|
+
const kv = c.env?.KV;
|
|
224
|
+
let payload = null;
|
|
225
|
+
if (kv) {
|
|
226
|
+
const cacheKey = `auth:${token.substring(0, 20)}`;
|
|
227
|
+
const cached = await kv.get(cacheKey, "json");
|
|
228
|
+
if (cached) {
|
|
229
|
+
payload = cached;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!payload) {
|
|
233
|
+
const jwtSecret = c.env?.JWT_SECRET;
|
|
234
|
+
payload = await AuthManager.verifyToken(token, jwtSecret);
|
|
235
|
+
if (payload && kv) {
|
|
236
|
+
const cacheKey = `auth:${token.substring(0, 20)}`;
|
|
237
|
+
await kv.put(cacheKey, JSON.stringify(payload), { expirationTtl: 300 });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!payload) {
|
|
241
|
+
const acceptHeader = c.req.header("Accept") || "";
|
|
242
|
+
if (acceptHeader.includes("text/html")) {
|
|
243
|
+
return c.redirect("/auth/login?error=Your session has expired, please login again");
|
|
244
|
+
}
|
|
245
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
246
|
+
}
|
|
247
|
+
c.set("user", payload);
|
|
248
|
+
return await next();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error("Auth middleware error:", error);
|
|
251
|
+
const acceptHeader = c.req.header("Accept") || "";
|
|
252
|
+
if (acceptHeader.includes("text/html")) {
|
|
253
|
+
return c.redirect("/auth/login?error=Authentication failed, please login again");
|
|
254
|
+
}
|
|
255
|
+
return c.json({ error: "Authentication failed" }, 401);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
var requireRole = (requiredRole) => {
|
|
260
|
+
return async (c, next) => {
|
|
261
|
+
const user = c.get("user");
|
|
262
|
+
if (!user) {
|
|
263
|
+
const acceptHeader = c.req.header("Accept") || "";
|
|
264
|
+
if (acceptHeader.includes("text/html")) {
|
|
265
|
+
return c.redirect("/auth/login?error=Please login to access the admin area");
|
|
266
|
+
}
|
|
267
|
+
return c.json({ error: "Authentication required" }, 401);
|
|
268
|
+
}
|
|
269
|
+
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
|
|
270
|
+
if (!roles.includes(user.role)) {
|
|
271
|
+
const acceptHeader = c.req.header("Accept") || "";
|
|
272
|
+
if (acceptHeader.includes("text/html")) {
|
|
273
|
+
return c.redirect("/auth/login?error=You do not have permission to access this area");
|
|
274
|
+
}
|
|
275
|
+
return c.json({ error: "Insufficient permissions" }, 403);
|
|
276
|
+
}
|
|
277
|
+
return await next();
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
var optionalAuth = () => {
|
|
281
|
+
return async (c, next) => {
|
|
282
|
+
try {
|
|
283
|
+
let token = c.req.header("Authorization")?.replace("Bearer ", "");
|
|
284
|
+
if (!token) {
|
|
285
|
+
token = cookie.getCookie(c, "auth_token");
|
|
286
|
+
}
|
|
287
|
+
if (token) {
|
|
288
|
+
const jwtSecret = c.env?.JWT_SECRET;
|
|
289
|
+
const payload = await AuthManager.verifyToken(token, jwtSecret);
|
|
290
|
+
if (payload) {
|
|
291
|
+
c.set("user", payload);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return await next();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error("Optional auth error:", error);
|
|
297
|
+
return await next();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/middleware/metrics.ts
|
|
303
|
+
var metricsMiddleware = () => {
|
|
304
|
+
return async (c, next) => {
|
|
305
|
+
const path = new URL(c.req.url).pathname;
|
|
306
|
+
if (path !== "/admin/dashboard/api/metrics") {
|
|
307
|
+
chunkRCQ2HIQD_cjs.metricsTracker.recordRequest();
|
|
308
|
+
}
|
|
309
|
+
await next();
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
var JWT_SECRET_FALLBACK2 = "your-super-secret-jwt-key-change-in-production";
|
|
313
|
+
function arrayBufferToBase64Url(buffer) {
|
|
314
|
+
const bytes = new Uint8Array(buffer);
|
|
315
|
+
let binary = "";
|
|
316
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
317
|
+
binary += String.fromCharCode(bytes[i]);
|
|
318
|
+
}
|
|
319
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
320
|
+
}
|
|
321
|
+
async function getHmacKey(secret) {
|
|
322
|
+
const encoder = new TextEncoder();
|
|
323
|
+
return crypto.subtle.importKey(
|
|
324
|
+
"raw",
|
|
325
|
+
encoder.encode(secret),
|
|
326
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
327
|
+
false,
|
|
328
|
+
["sign", "verify"]
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
async function generateCsrfToken(secret) {
|
|
332
|
+
const nonceBytes = new Uint8Array(32);
|
|
333
|
+
crypto.getRandomValues(nonceBytes);
|
|
334
|
+
const nonce = arrayBufferToBase64Url(nonceBytes.buffer);
|
|
335
|
+
const key = await getHmacKey(secret);
|
|
336
|
+
const encoder = new TextEncoder();
|
|
337
|
+
const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(nonce));
|
|
338
|
+
const signature = arrayBufferToBase64Url(signatureBuffer);
|
|
339
|
+
return `${nonce}.${signature}`;
|
|
340
|
+
}
|
|
341
|
+
async function validateCsrfToken(token, secret) {
|
|
342
|
+
if (!token || typeof token !== "string") return false;
|
|
343
|
+
const dotIndex = token.indexOf(".");
|
|
344
|
+
if (dotIndex === -1) return false;
|
|
345
|
+
const nonce = token.substring(0, dotIndex);
|
|
346
|
+
const signature = token.substring(dotIndex + 1);
|
|
347
|
+
if (!nonce || !signature) return false;
|
|
348
|
+
try {
|
|
349
|
+
const key = await getHmacKey(secret);
|
|
350
|
+
const encoder = new TextEncoder();
|
|
351
|
+
const sigPadded = signature.replace(/-/g, "+").replace(/_/g, "/");
|
|
352
|
+
const sigBinary = atob(sigPadded);
|
|
353
|
+
const sigBytes = new Uint8Array(sigBinary.length);
|
|
354
|
+
for (let i = 0; i < sigBinary.length; i++) {
|
|
355
|
+
sigBytes[i] = sigBinary.charCodeAt(i);
|
|
356
|
+
}
|
|
357
|
+
return await crypto.subtle.verify("HMAC", key, sigBytes.buffer, encoder.encode(nonce));
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
var DEFAULT_EXEMPT_PATHS = [
|
|
363
|
+
"/auth/login",
|
|
364
|
+
"/auth/register",
|
|
365
|
+
"/auth/seed-admin",
|
|
366
|
+
"/auth/accept-invitation",
|
|
367
|
+
"/auth/reset-password",
|
|
368
|
+
"/auth/request-password-reset"
|
|
369
|
+
];
|
|
370
|
+
function isExemptPath(path, extraExemptPaths = []) {
|
|
371
|
+
if (path.startsWith("/forms/") || path.startsWith("/api/forms/") || path === "/forms" || path === "/api/forms") {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (path.startsWith("/api/search")) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const allExempt = [...DEFAULT_EXEMPT_PATHS, ...extraExemptPaths];
|
|
378
|
+
for (const exempt of allExempt) {
|
|
379
|
+
if (path === exempt || path.startsWith(exempt + "/")) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
function csrfProtection(options = {}) {
|
|
386
|
+
return async (c, next) => {
|
|
387
|
+
const method = c.req.method.toUpperCase();
|
|
388
|
+
const path = new URL(c.req.url).pathname;
|
|
389
|
+
const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK2;
|
|
390
|
+
if (c.env?.ENVIRONMENT === "production" && !c.env?.JWT_SECRET) {
|
|
391
|
+
console.warn(
|
|
392
|
+
"[CSRF] WARNING: JWT_SECRET is not set in production. CSRF tokens are signed with the fallback key, which is insecure."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
396
|
+
await ensureCsrfCookie(c, secret);
|
|
397
|
+
await next();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (isExemptPath(path, options.exemptPaths)) {
|
|
401
|
+
await next();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const authCookie = cookie.getCookie(c, "auth_token");
|
|
405
|
+
if (!authCookie) {
|
|
406
|
+
await next();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const cookieToken = cookie.getCookie(c, "csrf_token");
|
|
410
|
+
let headerToken = c.req.header("X-CSRF-Token");
|
|
411
|
+
if (!headerToken) {
|
|
412
|
+
const contentType = c.req.header("Content-Type") || "";
|
|
413
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
414
|
+
try {
|
|
415
|
+
const body = await c.req.parseBody();
|
|
416
|
+
headerToken = body["_csrf"];
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!cookieToken || !headerToken) {
|
|
422
|
+
return csrfError(c, "CSRF token missing");
|
|
423
|
+
}
|
|
424
|
+
if (cookieToken !== headerToken) {
|
|
425
|
+
return csrfError(c, "CSRF token mismatch");
|
|
426
|
+
}
|
|
427
|
+
const isValid = await validateCsrfToken(cookieToken, secret);
|
|
428
|
+
if (!isValid) {
|
|
429
|
+
return csrfError(c, "CSRF token invalid");
|
|
430
|
+
}
|
|
431
|
+
await next();
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async function ensureCsrfCookie(c, secret) {
|
|
435
|
+
const existing = cookie.getCookie(c, "csrf_token");
|
|
436
|
+
if (existing) {
|
|
437
|
+
const isValid = await validateCsrfToken(existing, secret);
|
|
438
|
+
if (isValid) {
|
|
439
|
+
c.set("csrfToken", existing);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const token = await generateCsrfToken(secret);
|
|
444
|
+
c.set("csrfToken", token);
|
|
445
|
+
const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
|
|
446
|
+
cookie.setCookie(c, "csrf_token", token, {
|
|
447
|
+
httpOnly: false,
|
|
448
|
+
// JS must read this cookie
|
|
449
|
+
secure: !isDev,
|
|
450
|
+
sameSite: "Strict",
|
|
451
|
+
path: "/",
|
|
452
|
+
maxAge: 86400
|
|
453
|
+
// 24 hours — browser-side expiry
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function csrfError(c, message) {
|
|
457
|
+
const accept = c.req.header("Accept") || "";
|
|
458
|
+
if (accept.includes("text/html")) {
|
|
459
|
+
return c.html(
|
|
460
|
+
`<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>403 Forbidden</h1><p>${message}</p></body></html>`,
|
|
461
|
+
403
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return c.json({ error: message, status: 403 }, 403);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/middleware/rate-limit.ts
|
|
468
|
+
function rateLimit(options) {
|
|
469
|
+
const { max, windowMs, keyPrefix } = options;
|
|
470
|
+
return async (c, next) => {
|
|
471
|
+
const kv = c.env?.CACHE_KV;
|
|
472
|
+
if (!kv) {
|
|
473
|
+
return await next();
|
|
474
|
+
}
|
|
475
|
+
const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown";
|
|
476
|
+
const key = `ratelimit:${keyPrefix}:${ip}`;
|
|
477
|
+
try {
|
|
478
|
+
const now = Date.now();
|
|
479
|
+
const stored = await kv.get(key, "json");
|
|
480
|
+
let entry;
|
|
481
|
+
if (stored && stored.resetAt > now) {
|
|
482
|
+
entry = stored;
|
|
483
|
+
} else {
|
|
484
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
485
|
+
}
|
|
486
|
+
entry.count++;
|
|
487
|
+
const ttlSeconds = Math.ceil((entry.resetAt - now) / 1e3);
|
|
488
|
+
if (entry.count > max) {
|
|
489
|
+
await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 1) });
|
|
490
|
+
const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
|
|
491
|
+
c.header("Retry-After", String(retryAfter));
|
|
492
|
+
c.header("X-RateLimit-Limit", String(max));
|
|
493
|
+
c.header("X-RateLimit-Remaining", "0");
|
|
494
|
+
c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
|
|
495
|
+
return c.json({ error: "Too many requests. Please try again later." }, 429);
|
|
496
|
+
}
|
|
497
|
+
await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 1) });
|
|
498
|
+
c.header("X-RateLimit-Limit", String(max));
|
|
499
|
+
c.header("X-RateLimit-Remaining", String(max - entry.count));
|
|
500
|
+
c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
|
|
501
|
+
return await next();
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error("Rate limiter error (non-fatal):", error);
|
|
504
|
+
return await next();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/middleware/security-headers.ts
|
|
510
|
+
var securityHeadersMiddleware = () => {
|
|
511
|
+
return async (c, next) => {
|
|
512
|
+
await next();
|
|
513
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
514
|
+
c.header("X-Frame-Options", "SAMEORIGIN");
|
|
515
|
+
c.header("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
516
|
+
c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
517
|
+
const environment = c.env?.ENVIRONMENT;
|
|
518
|
+
if (environment !== "development") {
|
|
519
|
+
c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/middleware/index.ts
|
|
525
|
+
var loggingMiddleware = () => async (_c, next) => await next();
|
|
526
|
+
var detailedLoggingMiddleware = () => async (_c, next) => await next();
|
|
527
|
+
var securityLoggingMiddleware = () => async (_c, next) => await next();
|
|
528
|
+
var performanceLoggingMiddleware = () => async (_c, next) => await next();
|
|
529
|
+
var cacheHeaders = () => async (_c, next) => await next();
|
|
530
|
+
var compressionMiddleware = async (_c, next) => await next();
|
|
531
|
+
var PermissionManager = {};
|
|
532
|
+
var requirePermission = () => async (_c, next) => await next();
|
|
533
|
+
var requireAnyPermission = () => async (_c, next) => await next();
|
|
534
|
+
var logActivity = () => {
|
|
535
|
+
};
|
|
536
|
+
var requireActivePlugin = () => async (_c, next) => await next();
|
|
537
|
+
var requireActivePlugins = () => async (_c, next) => await next();
|
|
538
|
+
var getActivePlugins = () => [];
|
|
539
|
+
var isPluginActive = () => false;
|
|
540
|
+
|
|
541
|
+
exports.AuthManager = AuthManager;
|
|
542
|
+
exports.PermissionManager = PermissionManager;
|
|
543
|
+
exports.bootstrapMiddleware = bootstrapMiddleware;
|
|
544
|
+
exports.cacheHeaders = cacheHeaders;
|
|
545
|
+
exports.compressionMiddleware = compressionMiddleware;
|
|
546
|
+
exports.csrfProtection = csrfProtection;
|
|
547
|
+
exports.detailedLoggingMiddleware = detailedLoggingMiddleware;
|
|
548
|
+
exports.generateCsrfToken = generateCsrfToken;
|
|
549
|
+
exports.getActivePlugins = getActivePlugins;
|
|
550
|
+
exports.isPluginActive = isPluginActive;
|
|
551
|
+
exports.logActivity = logActivity;
|
|
552
|
+
exports.loggingMiddleware = loggingMiddleware;
|
|
553
|
+
exports.metricsMiddleware = metricsMiddleware;
|
|
554
|
+
exports.optionalAuth = optionalAuth;
|
|
555
|
+
exports.performanceLoggingMiddleware = performanceLoggingMiddleware;
|
|
556
|
+
exports.rateLimit = rateLimit;
|
|
557
|
+
exports.requireActivePlugin = requireActivePlugin;
|
|
558
|
+
exports.requireActivePlugins = requireActivePlugins;
|
|
559
|
+
exports.requireAnyPermission = requireAnyPermission;
|
|
560
|
+
exports.requireAuth = requireAuth;
|
|
561
|
+
exports.requirePermission = requirePermission;
|
|
562
|
+
exports.requireRole = requireRole;
|
|
563
|
+
exports.securityHeadersMiddleware = securityHeadersMiddleware;
|
|
564
|
+
exports.securityLoggingMiddleware = securityLoggingMiddleware;
|
|
565
|
+
exports.validateCsrfToken = validateCsrfToken;
|
|
566
|
+
exports.verifySecurityConfig = verifySecurityConfig;
|
|
567
|
+
//# sourceMappingURL=chunk-ZWKCL46S.cjs.map
|
|
568
|
+
//# sourceMappingURL=chunk-ZWKCL46S.cjs.map
|