@sonicjs-cms/core 2.8.1 → 2.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/{app-CYEm1ytG.d.cts → app-DnQ26Lho.d.cts} +3 -0
  2. package/dist/{app-CYEm1ytG.d.ts → app-DnQ26Lho.d.ts} +3 -0
  3. package/dist/{chunk-S6K2H2TS.cjs → chunk-3G7XX4UI.cjs} +9 -9
  4. package/dist/{chunk-S6K2H2TS.cjs.map → chunk-3G7XX4UI.cjs.map} +1 -1
  5. package/dist/{chunk-KAT3OKHE.js → chunk-5XAI2XUF.js} +33 -37
  6. package/dist/chunk-5XAI2XUF.js.map +1 -0
  7. package/dist/{chunk-H7AMQWVI.js → chunk-74XCYEI7.js} +3 -3
  8. package/dist/{chunk-H7AMQWVI.js.map → chunk-74XCYEI7.js.map} +1 -1
  9. package/dist/{chunk-FZRZYQYU.js → chunk-CH5UHZVM.js} +2604 -2364
  10. package/dist/chunk-CH5UHZVM.js.map +1 -0
  11. package/dist/{chunk-7Q2XPM2U.js → chunk-GTFMI24U.js} +21 -2
  12. package/dist/chunk-GTFMI24U.js.map +1 -0
  13. package/dist/{chunk-SKLRRFJJ.cjs → chunk-HXHVU5GM.cjs} +21 -2
  14. package/dist/chunk-HXHVU5GM.cjs.map +1 -0
  15. package/dist/{chunk-WDQZYCQO.cjs → chunk-JDIM5AG7.cjs} +32 -39
  16. package/dist/chunk-JDIM5AG7.cjs.map +1 -0
  17. package/dist/{chunk-VCH6HXVP.js → chunk-JJS7JZCH.js} +58 -4
  18. package/dist/chunk-JJS7JZCH.js.map +1 -0
  19. package/dist/chunk-K4Q4SFJJ.cjs +568 -0
  20. package/dist/chunk-K4Q4SFJJ.cjs.map +1 -0
  21. package/dist/{chunk-SHCYIZAN.cjs → chunk-LTKV7AE5.cjs} +58 -4
  22. package/dist/chunk-LTKV7AE5.cjs.map +1 -0
  23. package/dist/chunk-MNWKYY5E.cjs +44 -0
  24. package/dist/chunk-MNWKYY5E.cjs.map +1 -0
  25. package/dist/{chunk-JVRRG36J.cjs → chunk-R4WR3VTN.cjs} +2393 -2153
  26. package/dist/chunk-R4WR3VTN.cjs.map +1 -0
  27. package/dist/chunk-TQABQWOP.js +39 -0
  28. package/dist/chunk-TQABQWOP.js.map +1 -0
  29. package/dist/chunk-Y3VMEGY2.js +541 -0
  30. package/dist/chunk-Y3VMEGY2.js.map +1 -0
  31. package/dist/{collection-config-BF95LgQb.d.cts → collection-config-i8EaAF7z.d.cts} +2 -1
  32. package/dist/{collection-config-BF95LgQb.d.ts → collection-config-i8EaAF7z.d.ts} +2 -1
  33. package/dist/{filter-bar.template-By4jeiw_.d.cts → filter-bar.template-Daw8ZDoq.d.cts} +1 -0
  34. package/dist/{filter-bar.template-By4jeiw_.d.ts → filter-bar.template-Daw8ZDoq.d.ts} +1 -0
  35. package/dist/index.cjs +112 -111
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +6 -6
  38. package/dist/index.d.ts +6 -6
  39. package/dist/index.js +16 -15
  40. package/dist/index.js.map +1 -1
  41. package/dist/middleware.cjs +43 -23
  42. package/dist/middleware.d.cts +86 -6
  43. package/dist/middleware.d.ts +86 -6
  44. package/dist/middleware.js +2 -2
  45. package/dist/migrations-7X4RPH7O.cjs +13 -0
  46. package/dist/{migrations-76NR5BVF.cjs.map → migrations-7X4RPH7O.cjs.map} +1 -1
  47. package/dist/migrations-KHWFJ2HN.js +4 -0
  48. package/dist/{migrations-2NTJ44OR.js.map → migrations-KHWFJ2HN.js.map} +1 -1
  49. package/dist/{plugin-bootstrap-C7Mj00Ud.d.ts → plugin-bootstrap-CJozpgmI.d.cts} +1 -1
  50. package/dist/{plugin-bootstrap-DKB5f8-E.d.cts → plugin-bootstrap-DU5VmuHZ.d.ts} +1 -1
  51. package/dist/routes.cjs +29 -28
  52. package/dist/routes.d.cts +1 -1
  53. package/dist/routes.d.ts +1 -1
  54. package/dist/routes.js +6 -5
  55. package/dist/services.cjs +2 -2
  56. package/dist/services.d.cts +2 -2
  57. package/dist/services.d.ts +2 -2
  58. package/dist/services.js +1 -1
  59. package/dist/templates.cjs +20 -19
  60. package/dist/templates.d.cts +1 -1
  61. package/dist/templates.d.ts +1 -1
  62. package/dist/templates.js +3 -2
  63. package/dist/types.d.cts +1 -1
  64. package/dist/types.d.ts +1 -1
  65. package/dist/utils.cjs +24 -23
  66. package/dist/utils.d.cts +2 -2
  67. package/dist/utils.d.ts +2 -2
  68. package/dist/utils.js +2 -1
  69. package/dist/{version-vktVAxhe.d.cts → version-C_CXrN_T.d.cts} +5 -0
  70. package/dist/{version-vktVAxhe.d.ts → version-C_CXrN_T.d.ts} +5 -0
  71. package/migrations/032_user_profiles.sql +36 -0
  72. package/package.json +2 -2
  73. package/dist/chunk-7Q2XPM2U.js.map +0 -1
  74. package/dist/chunk-FZRZYQYU.js.map +0 -1
  75. package/dist/chunk-GIWIJNBH.cjs +0 -243
  76. package/dist/chunk-GIWIJNBH.cjs.map +0 -1
  77. package/dist/chunk-JVRRG36J.cjs.map +0 -1
  78. package/dist/chunk-KAT3OKHE.js.map +0 -1
  79. package/dist/chunk-QWTS6NSP.js +0 -221
  80. package/dist/chunk-QWTS6NSP.js.map +0 -1
  81. package/dist/chunk-SHCYIZAN.cjs.map +0 -1
  82. package/dist/chunk-SKLRRFJJ.cjs.map +0 -1
  83. package/dist/chunk-VCH6HXVP.js.map +0 -1
  84. package/dist/chunk-WDQZYCQO.cjs.map +0 -1
  85. package/dist/migrations-2NTJ44OR.js +0 -4
  86. package/dist/migrations-76NR5BVF.cjs +0 -13
@@ -0,0 +1,39 @@
1
+ // src/utils/sanitize.ts
2
+ function escapeHtml(text) {
3
+ if (typeof text !== "string") {
4
+ return "";
5
+ }
6
+ const map = {
7
+ "&": "&",
8
+ "<": "&lt;",
9
+ ">": "&gt;",
10
+ '"': "&quot;",
11
+ "'": "&#039;"
12
+ };
13
+ return text.replace(/[&<>"']/g, (char) => map[char] || char);
14
+ }
15
+ function sanitizeInput(input) {
16
+ if (!input) {
17
+ return "";
18
+ }
19
+ return escapeHtml(String(input).trim());
20
+ }
21
+ function sanitizeRichText(html) {
22
+ if (typeof html !== "string") {
23
+ return "";
24
+ }
25
+ return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/(href|src|action)\s*=\s*"javascript:[^"]*"/gi, '$1=""').replace(/(href|src|action)\s*=\s*'javascript:[^']*'/gi, "$1=''");
26
+ }
27
+ function sanitizeObject(obj, fields) {
28
+ const sanitized = { ...obj };
29
+ for (const field of fields) {
30
+ if (typeof obj[field] === "string") {
31
+ sanitized[field] = sanitizeInput(obj[field]);
32
+ }
33
+ }
34
+ return sanitized;
35
+ }
36
+
37
+ export { escapeHtml, sanitizeInput, sanitizeObject, sanitizeRichText };
38
+ //# sourceMappingURL=chunk-TQABQWOP.js.map
39
+ //# sourceMappingURL=chunk-TQABQWOP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/sanitize.ts"],"names":[],"mappings":";AASO,SAAS,WAAW,IAAA,EAAsB;AAC/C,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,OAAO,EAAA;AAAA,EACT;AAEA,EAAA,MAAM,GAAA,GAA8B;AAAA,IAClC,GAAA,EAAK,OAAA;AAAA,IACL,GAAA,EAAK,MAAA;AAAA,IACL,GAAA,EAAK,MAAA;AAAA,IACL,GAAA,EAAK,QAAA;AAAA,IACL,GAAA,EAAK;AAAA,GACP;AAEA,EAAA,OAAO,IAAA,CAAK,QAAQ,UAAA,EAAY,CAAC,SAAS,GAAA,CAAI,IAAI,KAAK,IAAI,CAAA;AAC7D;AAQO,SAAS,cAAc,KAAA,EAA0C;AACtE,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,EAAA;AAAA,EACT;AACA,EAAA,OAAO,UAAA,CAAW,MAAA,CAAO,KAAK,CAAA,CAAE,MAAM,CAAA;AACxC;AAQO,SAAS,iBAAiB,IAAA,EAAsB;AACrD,EAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,IAAA,OAAO,EAAA;AAAA,EACT;AAEA,EAAA,OAAO,IAAA,CAEJ,OAAA,CAAQ,qDAAA,EAAuD,EAAE,EAEjE,OAAA,CAAQ,8CAAA,EAAgD,EAAE,CAAA,CAE1D,QAAQ,8CAAA,EAAgD,OAAO,CAAA,CAC/D,OAAA,CAAQ,gDAAgD,OAAO,CAAA;AACpE;AAQO,SAAS,cAAA,CACd,KACA,MAAA,EACG;AACH,EAAA,MAAM,SAAA,GAAY,EAAE,GAAG,GAAA,EAAI;AAE3B,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,IAAI,OAAO,GAAA,CAAI,KAAK,CAAA,KAAM,QAAA,EAAU;AAClC,MAAA,SAAA,CAAU,KAAK,CAAA,GAAI,aAAA,CAAc,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,IAC7C;AAAA,EACF;AAEA,EAAA,OAAO,SAAA;AACT","file":"chunk-TQABQWOP.js","sourcesContent":["/**\n * HTML sanitization utilities for preventing XSS attacks\n */\n\n/**\n * Escapes HTML special characters to prevent XSS attacks\n * @param text - The text to escape\n * @returns The escaped text safe for HTML output\n */\nexport function escapeHtml(text: string): string {\n if (typeof text !== 'string') {\n return ''\n }\n\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#039;'\n }\n\n return text.replace(/[&<>\"']/g, (char) => map[char] || char)\n}\n\n/**\n * Sanitizes user input by escaping HTML special characters\n * This should be used for all user-provided text fields to prevent XSS\n * @param input - The input string to sanitize\n * @returns The sanitized string\n */\nexport function sanitizeInput(input: string | null | undefined): string {\n if (!input) {\n return ''\n }\n return escapeHtml(String(input).trim())\n}\n\n/**\n * Sanitizes rich text HTML by stripping dangerous elements while preserving\n * legitimate formatting. Removes script tags, event handlers, and javascript: URLs.\n * @param html - The rich text HTML to sanitize\n * @returns Sanitized HTML safe for rendering\n */\nexport function sanitizeRichText(html: string): string {\n if (typeof html !== 'string') {\n return ''\n }\n\n return html\n // Remove script tags and their contents\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n // Remove event handler attributes (on*)\n .replace(/\\s+on\\w+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '')\n // Remove javascript: URLs in href/src/action attributes\n .replace(/(href|src|action)\\s*=\\s*\"javascript:[^\"]*\"/gi, '$1=\"\"')\n .replace(/(href|src|action)\\s*=\\s*'javascript:[^']*'/gi, \"$1=''\")\n}\n\n/**\n * Sanitizes an object's string properties\n * @param obj - Object with string properties to sanitize\n * @param fields - Array of field names to sanitize\n * @returns New object with sanitized fields\n */\nexport function sanitizeObject<T extends Record<string, any>>(\n obj: T,\n fields: (keyof T)[]\n): T {\n const sanitized = { ...obj }\n\n for (const field of fields) {\n if (typeof obj[field] === 'string') {\n sanitized[field] = sanitizeInput(obj[field]) as T[keyof T]\n }\n }\n\n return sanitized\n}\n"]}
@@ -0,0 +1,541 @@
1
+ import { syncCollections, PluginBootstrapService } from './chunk-YFJJU26H.js';
2
+ import { MigrationService } from './chunk-GTFMI24U.js';
3
+ import { metricsTracker } from './chunk-FICTAGD4.js';
4
+ import { sign, verify } from 'hono/jwt';
5
+ import { setCookie, getCookie } from 'hono/cookie';
6
+
7
+ // src/middleware/bootstrap.ts
8
+ var bootstrapComplete = false;
9
+ function verifySecurityConfig(env) {
10
+ const warnings = [];
11
+ if (!env.JWT_SECRET) {
12
+ warnings.push(
13
+ "JWT_SECRET is not set \u2014 using hardcoded fallback. Set via `wrangler secret put JWT_SECRET`"
14
+ );
15
+ } else if (env.JWT_SECRET.includes("change-in-production")) {
16
+ warnings.push(
17
+ "JWT_SECRET contains the default value \u2014 tokens are forgeable. Generate a strong random secret"
18
+ );
19
+ }
20
+ if (!env.CORS_ORIGINS) {
21
+ warnings.push(
22
+ "CORS_ORIGINS is not set \u2014 all cross-origin API requests will be rejected"
23
+ );
24
+ }
25
+ if (!env.ENVIRONMENT) {
26
+ warnings.push(
27
+ 'ENVIRONMENT is not set \u2014 HSTS header will not be applied. Set to "production" or "development"'
28
+ );
29
+ }
30
+ if (warnings.length === 0) {
31
+ return;
32
+ }
33
+ const isProduction = env.ENVIRONMENT === "production";
34
+ for (const warning of warnings) {
35
+ console.warn(`[SonicJS Security] ${warning}`);
36
+ }
37
+ if (isProduction) {
38
+ const hasCritical = !env.JWT_SECRET || env.JWT_SECRET.includes("change-in-production");
39
+ if (hasCritical) {
40
+ throw new Error(
41
+ "[SonicJS Security] CRITICAL: Production deployment is missing a secure JWT_SECRET. Set it via `wrangler secret put JWT_SECRET` before deploying."
42
+ );
43
+ }
44
+ }
45
+ }
46
+ function bootstrapMiddleware(config = {}) {
47
+ return async (c, next) => {
48
+ if (bootstrapComplete) {
49
+ return next();
50
+ }
51
+ const path = c.req.path;
52
+ if (path.startsWith("/images/") || path.startsWith("/assets/") || path === "/health" || path.endsWith(".js") || path.endsWith(".css") || path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".ico")) {
53
+ return next();
54
+ }
55
+ try {
56
+ console.log("[Bootstrap] Starting system initialization...");
57
+ console.log("[Bootstrap] Running database migrations...");
58
+ const migrationService = new MigrationService(c.env.DB);
59
+ await migrationService.runPendingMigrations();
60
+ console.log("[Bootstrap] Syncing collection configurations...");
61
+ try {
62
+ await syncCollections(c.env.DB);
63
+ } catch (error) {
64
+ console.error("[Bootstrap] Error syncing collections:", error);
65
+ }
66
+ if (!config.plugins?.disableAll) {
67
+ console.log("[Bootstrap] Bootstrapping core plugins...");
68
+ const bootstrapService = new PluginBootstrapService(c.env.DB);
69
+ const needsBootstrap = await bootstrapService.isBootstrapNeeded();
70
+ if (needsBootstrap) {
71
+ await bootstrapService.bootstrapCorePlugins();
72
+ }
73
+ } else {
74
+ console.log("[Bootstrap] Plugin bootstrap skipped (disableAll is true)");
75
+ }
76
+ bootstrapComplete = true;
77
+ console.log("[Bootstrap] System initialization completed");
78
+ } catch (error) {
79
+ console.error("[Bootstrap] Error during system initialization:", error);
80
+ }
81
+ verifySecurityConfig(c.env);
82
+ return next();
83
+ };
84
+ }
85
+ var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
86
+ var AuthManager = class {
87
+ static async generateToken(userId, email, role, secret) {
88
+ const payload = {
89
+ userId,
90
+ email,
91
+ role,
92
+ exp: Math.floor(Date.now() / 1e3) + 60 * 60 * 24,
93
+ // 24 hours
94
+ iat: Math.floor(Date.now() / 1e3)
95
+ };
96
+ return await sign(payload, secret || JWT_SECRET_FALLBACK, "HS256");
97
+ }
98
+ static async verifyToken(token, secret) {
99
+ try {
100
+ const payload = await verify(token, secret || JWT_SECRET_FALLBACK, "HS256");
101
+ if (payload.exp < Math.floor(Date.now() / 1e3)) {
102
+ return null;
103
+ }
104
+ return payload;
105
+ } catch (error) {
106
+ console.error("Token verification failed:", error);
107
+ return null;
108
+ }
109
+ }
110
+ static async hashPassword(password) {
111
+ const iterations = 1e5;
112
+ const salt = new Uint8Array(16);
113
+ crypto.getRandomValues(salt);
114
+ const encoder = new TextEncoder();
115
+ const keyMaterial = await crypto.subtle.importKey(
116
+ "raw",
117
+ encoder.encode(password),
118
+ "PBKDF2",
119
+ false,
120
+ ["deriveBits"]
121
+ );
122
+ const hashBuffer = await crypto.subtle.deriveBits(
123
+ {
124
+ name: "PBKDF2",
125
+ salt,
126
+ iterations,
127
+ hash: "SHA-256"
128
+ },
129
+ keyMaterial,
130
+ 256
131
+ );
132
+ const saltHex = Array.from(salt).map((b) => b.toString(16).padStart(2, "0")).join("");
133
+ const hashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
134
+ return `pbkdf2:${iterations}:${saltHex}:${hashHex}`;
135
+ }
136
+ static async hashPasswordLegacy(password) {
137
+ const encoder = new TextEncoder();
138
+ const data = encoder.encode(password + "salt-change-in-production");
139
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
140
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
141
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
142
+ }
143
+ static async verifyPassword(password, storedHash) {
144
+ if (storedHash.startsWith("pbkdf2:")) {
145
+ const parts = storedHash.split(":");
146
+ if (parts.length !== 4) return false;
147
+ const iterationsStr = parts[1];
148
+ const saltHex = parts[2];
149
+ const expectedHashHex = parts[3];
150
+ const iterations = parseInt(iterationsStr, 10);
151
+ const saltBytes = saltHex.match(/.{2}/g);
152
+ if (!saltBytes) return false;
153
+ const salt = new Uint8Array(saltBytes.map((byte) => parseInt(byte, 16)));
154
+ const encoder = new TextEncoder();
155
+ const keyMaterial = await crypto.subtle.importKey(
156
+ "raw",
157
+ encoder.encode(password),
158
+ "PBKDF2",
159
+ false,
160
+ ["deriveBits"]
161
+ );
162
+ const hashBuffer = await crypto.subtle.deriveBits(
163
+ {
164
+ name: "PBKDF2",
165
+ salt,
166
+ iterations,
167
+ hash: "SHA-256"
168
+ },
169
+ keyMaterial,
170
+ 256
171
+ );
172
+ const actualHashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
173
+ if (actualHashHex.length !== expectedHashHex.length) return false;
174
+ let result2 = 0;
175
+ for (let i = 0; i < actualHashHex.length; i++) {
176
+ result2 |= actualHashHex.charCodeAt(i) ^ expectedHashHex.charCodeAt(i);
177
+ }
178
+ return result2 === 0;
179
+ }
180
+ const legacyHash = await this.hashPasswordLegacy(password);
181
+ if (legacyHash.length !== storedHash.length) return false;
182
+ let result = 0;
183
+ for (let i = 0; i < legacyHash.length; i++) {
184
+ result |= legacyHash.charCodeAt(i) ^ storedHash.charCodeAt(i);
185
+ }
186
+ return result === 0;
187
+ }
188
+ static isLegacyHash(storedHash) {
189
+ return !storedHash.startsWith("pbkdf2:");
190
+ }
191
+ /**
192
+ * Set authentication cookie - useful for plugins implementing alternative auth methods
193
+ * @param c - Hono context
194
+ * @param token - JWT token to set in cookie
195
+ * @param options - Optional cookie configuration
196
+ */
197
+ static setAuthCookie(c, token, options) {
198
+ setCookie(c, "auth_token", token, {
199
+ httpOnly: options?.httpOnly ?? true,
200
+ secure: options?.secure ?? true,
201
+ sameSite: options?.sameSite ?? "Strict",
202
+ maxAge: options?.maxAge ?? 60 * 60 * 24
203
+ // 24 hours default
204
+ });
205
+ }
206
+ };
207
+ var requireAuth = () => {
208
+ return async (c, next) => {
209
+ try {
210
+ let token = c.req.header("Authorization")?.replace("Bearer ", "");
211
+ if (!token) {
212
+ token = getCookie(c, "auth_token");
213
+ }
214
+ if (!token) {
215
+ const acceptHeader = c.req.header("Accept") || "";
216
+ if (acceptHeader.includes("text/html")) {
217
+ return c.redirect("/auth/login?error=Please login to access the admin area");
218
+ }
219
+ return c.json({ error: "Authentication required" }, 401);
220
+ }
221
+ const kv = c.env?.KV;
222
+ let payload = null;
223
+ if (kv) {
224
+ const cacheKey = `auth:${token.substring(0, 20)}`;
225
+ const cached = await kv.get(cacheKey, "json");
226
+ if (cached) {
227
+ payload = cached;
228
+ }
229
+ }
230
+ if (!payload) {
231
+ const jwtSecret = c.env?.JWT_SECRET;
232
+ payload = await AuthManager.verifyToken(token, jwtSecret);
233
+ if (payload && kv) {
234
+ const cacheKey = `auth:${token.substring(0, 20)}`;
235
+ await kv.put(cacheKey, JSON.stringify(payload), { expirationTtl: 300 });
236
+ }
237
+ }
238
+ if (!payload) {
239
+ const acceptHeader = c.req.header("Accept") || "";
240
+ if (acceptHeader.includes("text/html")) {
241
+ return c.redirect("/auth/login?error=Your session has expired, please login again");
242
+ }
243
+ return c.json({ error: "Invalid or expired token" }, 401);
244
+ }
245
+ c.set("user", payload);
246
+ return await next();
247
+ } catch (error) {
248
+ console.error("Auth middleware error:", error);
249
+ const acceptHeader = c.req.header("Accept") || "";
250
+ if (acceptHeader.includes("text/html")) {
251
+ return c.redirect("/auth/login?error=Authentication failed, please login again");
252
+ }
253
+ return c.json({ error: "Authentication failed" }, 401);
254
+ }
255
+ };
256
+ };
257
+ var requireRole = (requiredRole) => {
258
+ return async (c, next) => {
259
+ const user = c.get("user");
260
+ if (!user) {
261
+ const acceptHeader = c.req.header("Accept") || "";
262
+ if (acceptHeader.includes("text/html")) {
263
+ return c.redirect("/auth/login?error=Please login to access the admin area");
264
+ }
265
+ return c.json({ error: "Authentication required" }, 401);
266
+ }
267
+ const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
268
+ if (!roles.includes(user.role)) {
269
+ const acceptHeader = c.req.header("Accept") || "";
270
+ if (acceptHeader.includes("text/html")) {
271
+ return c.redirect("/auth/login?error=You do not have permission to access this area");
272
+ }
273
+ return c.json({ error: "Insufficient permissions" }, 403);
274
+ }
275
+ return await next();
276
+ };
277
+ };
278
+ var optionalAuth = () => {
279
+ return async (c, next) => {
280
+ try {
281
+ let token = c.req.header("Authorization")?.replace("Bearer ", "");
282
+ if (!token) {
283
+ token = getCookie(c, "auth_token");
284
+ }
285
+ if (token) {
286
+ const jwtSecret = c.env?.JWT_SECRET;
287
+ const payload = await AuthManager.verifyToken(token, jwtSecret);
288
+ if (payload) {
289
+ c.set("user", payload);
290
+ }
291
+ }
292
+ return await next();
293
+ } catch (error) {
294
+ console.error("Optional auth error:", error);
295
+ return await next();
296
+ }
297
+ };
298
+ };
299
+
300
+ // src/middleware/metrics.ts
301
+ var metricsMiddleware = () => {
302
+ return async (c, next) => {
303
+ const path = new URL(c.req.url).pathname;
304
+ if (path !== "/admin/dashboard/api/metrics") {
305
+ metricsTracker.recordRequest();
306
+ }
307
+ await next();
308
+ };
309
+ };
310
+ var JWT_SECRET_FALLBACK2 = "your-super-secret-jwt-key-change-in-production";
311
+ function arrayBufferToBase64Url(buffer) {
312
+ const bytes = new Uint8Array(buffer);
313
+ let binary = "";
314
+ for (let i = 0; i < bytes.length; i++) {
315
+ binary += String.fromCharCode(bytes[i]);
316
+ }
317
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
318
+ }
319
+ async function getHmacKey(secret) {
320
+ const encoder = new TextEncoder();
321
+ return crypto.subtle.importKey(
322
+ "raw",
323
+ encoder.encode(secret),
324
+ { name: "HMAC", hash: "SHA-256" },
325
+ false,
326
+ ["sign", "verify"]
327
+ );
328
+ }
329
+ async function generateCsrfToken(secret) {
330
+ const nonceBytes = new Uint8Array(32);
331
+ crypto.getRandomValues(nonceBytes);
332
+ const nonce = arrayBufferToBase64Url(nonceBytes.buffer);
333
+ const key = await getHmacKey(secret);
334
+ const encoder = new TextEncoder();
335
+ const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(nonce));
336
+ const signature = arrayBufferToBase64Url(signatureBuffer);
337
+ return `${nonce}.${signature}`;
338
+ }
339
+ async function validateCsrfToken(token, secret) {
340
+ if (!token || typeof token !== "string") return false;
341
+ const dotIndex = token.indexOf(".");
342
+ if (dotIndex === -1) return false;
343
+ const nonce = token.substring(0, dotIndex);
344
+ const signature = token.substring(dotIndex + 1);
345
+ if (!nonce || !signature) return false;
346
+ try {
347
+ const key = await getHmacKey(secret);
348
+ const encoder = new TextEncoder();
349
+ const sigPadded = signature.replace(/-/g, "+").replace(/_/g, "/");
350
+ const sigBinary = atob(sigPadded);
351
+ const sigBytes = new Uint8Array(sigBinary.length);
352
+ for (let i = 0; i < sigBinary.length; i++) {
353
+ sigBytes[i] = sigBinary.charCodeAt(i);
354
+ }
355
+ return await crypto.subtle.verify("HMAC", key, sigBytes.buffer, encoder.encode(nonce));
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+ var DEFAULT_EXEMPT_PATHS = [
361
+ "/auth/login",
362
+ "/auth/register",
363
+ "/auth/seed-admin",
364
+ "/auth/accept-invitation",
365
+ "/auth/reset-password",
366
+ "/auth/request-password-reset"
367
+ ];
368
+ function isExemptPath(path, extraExemptPaths = []) {
369
+ if (path.startsWith("/forms/") || path.startsWith("/api/forms/") || path === "/forms" || path === "/api/forms") {
370
+ return true;
371
+ }
372
+ if (path.startsWith("/api/search")) {
373
+ return true;
374
+ }
375
+ const allExempt = [...DEFAULT_EXEMPT_PATHS, ...extraExemptPaths];
376
+ for (const exempt of allExempt) {
377
+ if (path === exempt || path.startsWith(exempt + "/")) {
378
+ return true;
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+ function csrfProtection(options = {}) {
384
+ return async (c, next) => {
385
+ const method = c.req.method.toUpperCase();
386
+ const path = new URL(c.req.url).pathname;
387
+ const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK2;
388
+ if (c.env?.ENVIRONMENT === "production" && !c.env?.JWT_SECRET) {
389
+ console.warn(
390
+ "[CSRF] WARNING: JWT_SECRET is not set in production. CSRF tokens are signed with the fallback key, which is insecure."
391
+ );
392
+ }
393
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
394
+ await ensureCsrfCookie(c, secret);
395
+ await next();
396
+ return;
397
+ }
398
+ if (isExemptPath(path, options.exemptPaths)) {
399
+ await next();
400
+ return;
401
+ }
402
+ const authCookie = getCookie(c, "auth_token");
403
+ if (!authCookie) {
404
+ await next();
405
+ return;
406
+ }
407
+ const cookieToken = getCookie(c, "csrf_token");
408
+ let headerToken = c.req.header("X-CSRF-Token");
409
+ if (!headerToken) {
410
+ const contentType = c.req.header("Content-Type") || "";
411
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
412
+ try {
413
+ const body = await c.req.parseBody();
414
+ headerToken = body["_csrf"];
415
+ } catch {
416
+ }
417
+ }
418
+ }
419
+ if (!cookieToken || !headerToken) {
420
+ return csrfError(c, "CSRF token missing");
421
+ }
422
+ if (cookieToken !== headerToken) {
423
+ return csrfError(c, "CSRF token mismatch");
424
+ }
425
+ const isValid = await validateCsrfToken(cookieToken, secret);
426
+ if (!isValid) {
427
+ return csrfError(c, "CSRF token invalid");
428
+ }
429
+ await next();
430
+ };
431
+ }
432
+ async function ensureCsrfCookie(c, secret) {
433
+ const existing = getCookie(c, "csrf_token");
434
+ if (existing) {
435
+ const isValid = await validateCsrfToken(existing, secret);
436
+ if (isValid) {
437
+ c.set("csrfToken", existing);
438
+ return;
439
+ }
440
+ }
441
+ const token = await generateCsrfToken(secret);
442
+ c.set("csrfToken", token);
443
+ const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
444
+ setCookie(c, "csrf_token", token, {
445
+ httpOnly: false,
446
+ // JS must read this cookie
447
+ secure: !isDev,
448
+ sameSite: "Strict",
449
+ path: "/",
450
+ maxAge: 86400
451
+ // 24 hours — browser-side expiry
452
+ });
453
+ }
454
+ function csrfError(c, message) {
455
+ const accept = c.req.header("Accept") || "";
456
+ if (accept.includes("text/html")) {
457
+ return c.html(
458
+ `<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>403 Forbidden</h1><p>${message}</p></body></html>`,
459
+ 403
460
+ );
461
+ }
462
+ return c.json({ error: message, status: 403 }, 403);
463
+ }
464
+
465
+ // src/middleware/rate-limit.ts
466
+ function rateLimit(options) {
467
+ const { max, windowMs, keyPrefix } = options;
468
+ return async (c, next) => {
469
+ const kv = c.env?.CACHE_KV;
470
+ if (!kv) {
471
+ return await next();
472
+ }
473
+ const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown";
474
+ const key = `ratelimit:${keyPrefix}:${ip}`;
475
+ try {
476
+ const now = Date.now();
477
+ const stored = await kv.get(key, "json");
478
+ let entry;
479
+ if (stored && stored.resetAt > now) {
480
+ entry = stored;
481
+ } else {
482
+ entry = { count: 0, resetAt: now + windowMs };
483
+ }
484
+ entry.count++;
485
+ const ttlSeconds = Math.ceil((entry.resetAt - now) / 1e3);
486
+ if (entry.count > max) {
487
+ await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 1) });
488
+ const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
489
+ c.header("Retry-After", String(retryAfter));
490
+ c.header("X-RateLimit-Limit", String(max));
491
+ c.header("X-RateLimit-Remaining", "0");
492
+ c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
493
+ return c.json({ error: "Too many requests. Please try again later." }, 429);
494
+ }
495
+ await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 1) });
496
+ c.header("X-RateLimit-Limit", String(max));
497
+ c.header("X-RateLimit-Remaining", String(max - entry.count));
498
+ c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
499
+ return await next();
500
+ } catch (error) {
501
+ console.error("Rate limiter error (non-fatal):", error);
502
+ return await next();
503
+ }
504
+ };
505
+ }
506
+
507
+ // src/middleware/security-headers.ts
508
+ var securityHeadersMiddleware = () => {
509
+ return async (c, next) => {
510
+ await next();
511
+ c.header("X-Content-Type-Options", "nosniff");
512
+ c.header("X-Frame-Options", "SAMEORIGIN");
513
+ c.header("Referrer-Policy", "strict-origin-when-cross-origin");
514
+ c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
515
+ const environment = c.env?.ENVIRONMENT;
516
+ if (environment !== "development") {
517
+ c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
518
+ }
519
+ };
520
+ };
521
+
522
+ // src/middleware/index.ts
523
+ var loggingMiddleware = () => async (_c, next) => await next();
524
+ var detailedLoggingMiddleware = () => async (_c, next) => await next();
525
+ var securityLoggingMiddleware = () => async (_c, next) => await next();
526
+ var performanceLoggingMiddleware = () => async (_c, next) => await next();
527
+ var cacheHeaders = () => async (_c, next) => await next();
528
+ var compressionMiddleware = async (_c, next) => await next();
529
+ var PermissionManager = {};
530
+ var requirePermission = () => async (_c, next) => await next();
531
+ var requireAnyPermission = () => async (_c, next) => await next();
532
+ var logActivity = () => {
533
+ };
534
+ var requireActivePlugin = () => async (_c, next) => await next();
535
+ var requireActivePlugins = () => async (_c, next) => await next();
536
+ var getActivePlugins = () => [];
537
+ var isPluginActive = () => false;
538
+
539
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, csrfProtection, detailedLoggingMiddleware, generateCsrfToken, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, metricsMiddleware, optionalAuth, performanceLoggingMiddleware, rateLimit, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware, securityLoggingMiddleware, validateCsrfToken, verifySecurityConfig };
540
+ //# sourceMappingURL=chunk-Y3VMEGY2.js.map
541
+ //# sourceMappingURL=chunk-Y3VMEGY2.js.map