@africode/core 5.0.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/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode CSRF Protection Middleware
|
|
3
|
+
*
|
|
4
|
+
* Double-submit cookie pattern for CSRF protection.
|
|
5
|
+
* Framework-level middleware — secure by default.
|
|
6
|
+
*
|
|
7
|
+
* How it works:
|
|
8
|
+
* 1. Server generates a random CSRF token and sets it as a cookie
|
|
9
|
+
* 2. Client reads the cookie and sends the token in a header (X-CSRF-Token)
|
|
10
|
+
* 3. Server compares cookie token with header token
|
|
11
|
+
* 4. If they match → request is from the legitimate client
|
|
12
|
+
* 5. If they don't match → request is cross-site (blocked)
|
|
13
|
+
*
|
|
14
|
+
* Why double-submit cookie:
|
|
15
|
+
* - No server-side session storage needed for the token
|
|
16
|
+
* - Works with stateless APIs
|
|
17
|
+
* - Compatible with single-page apps and server-rendered pages
|
|
18
|
+
*
|
|
19
|
+
* @module core/server/csrf
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { CsrfError } from '../errors.js';
|
|
23
|
+
import { emitSecurityViolation } from '../config.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a cryptographically secure CSRF token
|
|
27
|
+
* @returns {string} 32-byte hex token
|
|
28
|
+
*/
|
|
29
|
+
export function generateCsrfToken() {
|
|
30
|
+
const bytes = new Uint8Array(32);
|
|
31
|
+
crypto.getRandomValues(bytes);
|
|
32
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create the CSRF cookie header string
|
|
37
|
+
* @param {string} token - The CSRF token
|
|
38
|
+
* @param {Object} [opts] - Options
|
|
39
|
+
* @param {boolean} [opts.secure=false] - Set Secure flag (HTTPS only)
|
|
40
|
+
* @param {string} [opts.sameSite='Strict'] - SameSite policy
|
|
41
|
+
* @param {number} [opts.maxAge=86400] - Cookie max age in seconds (default: 24h)
|
|
42
|
+
* @returns {string} Set-Cookie header value
|
|
43
|
+
*/
|
|
44
|
+
export function createCsrfCookie(token, opts = {}) {
|
|
45
|
+
const { secure = false, sameSite = 'Strict', maxAge = 86400 } = opts;
|
|
46
|
+
return `csrf_token=${token}; Path=/; Max-Age=${maxAge}; SameSite=${sameSite}${secure ? '; Secure' : ''}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract the CSRF token from the request cookie
|
|
51
|
+
* @param {Request} request
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
function getCsrfFromCookie(request) {
|
|
55
|
+
const cookieHeader = request.headers.get('Cookie');
|
|
56
|
+
if (!cookieHeader) {return null;}
|
|
57
|
+
|
|
58
|
+
const match = cookieHeader.match(/csrf_token=([^;]+)/);
|
|
59
|
+
return match ? match[1] : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract the CSRF token from the request header
|
|
64
|
+
* @param {Request} request
|
|
65
|
+
* @returns {string|null}
|
|
66
|
+
*/
|
|
67
|
+
function getCsrfFromHeader(request) {
|
|
68
|
+
return request.headers.get('X-CSRF-Token') || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* CSRF Protection Middleware
|
|
73
|
+
*
|
|
74
|
+
* Validates that the CSRF token in the cookie matches the one in the header.
|
|
75
|
+
* Only applies to state-changing methods (POST, PUT, PATCH, DELETE).
|
|
76
|
+
* GET, HEAD, OPTIONS are safe methods and are not checked.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} [config] - Configuration
|
|
79
|
+
* @param {string[]} [config.safeMethods=['GET', 'HEAD', 'OPTIONS']] - Methods that skip CSRF check
|
|
80
|
+
* @param {string[]} [config.excludePaths=[]] - Paths that skip CSRF check (e.g. public APIs)
|
|
81
|
+
* @param {string} [config.headerName='X-CSRF-Token'] - Header name for the token
|
|
82
|
+
* @param {string} [config.cookieName='csrf_token'] - Cookie name for the token
|
|
83
|
+
* @param {Function} [config.onFailure] - Custom failure handler (receives request, returns Response)
|
|
84
|
+
*
|
|
85
|
+
* @returns {Function} Middleware function: (request) => Response | null
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // In your server setup:
|
|
89
|
+
* const csrf = createCsrfMiddleware();
|
|
90
|
+
*
|
|
91
|
+
* // In your request handler:
|
|
92
|
+
* const csrfError = csrf(request);
|
|
93
|
+
* if (csrfError) return csrfError;
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // With configuration:
|
|
97
|
+
* const csrf = createCsrfMiddleware({
|
|
98
|
+
* excludePaths: ['/api/webhooks'],
|
|
99
|
+
* onFailure: (req) => new Response('Custom CSRF error', { status: 403 })
|
|
100
|
+
* });
|
|
101
|
+
*/
|
|
102
|
+
export function createCsrfMiddleware(config = {}) {
|
|
103
|
+
const {
|
|
104
|
+
safeMethods = ['GET', 'HEAD', 'OPTIONS'],
|
|
105
|
+
excludePaths = [],
|
|
106
|
+
headerName = 'X-CSRF-Token',
|
|
107
|
+
cookieName = 'csrf_token',
|
|
108
|
+
onFailure = null
|
|
109
|
+
} = config;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {Request} request
|
|
113
|
+
* @returns {Response|null} Returns Response if CSRF fails, null if passes
|
|
114
|
+
*/
|
|
115
|
+
return function csrfMiddleware(request) {
|
|
116
|
+
const method = request.method.toUpperCase();
|
|
117
|
+
|
|
118
|
+
// Safe methods don't need CSRF protection
|
|
119
|
+
if (safeMethods.includes(method)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check excluded paths
|
|
124
|
+
const url = new URL(request.url);
|
|
125
|
+
if (excludePaths.some(path => url.pathname.startsWith(path))) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get tokens
|
|
130
|
+
const cookieToken = getCsrfFromCookie(request);
|
|
131
|
+
const headerToken = request.headers.get(headerName);
|
|
132
|
+
|
|
133
|
+
// Both must exist
|
|
134
|
+
if (!cookieToken || !headerToken) {
|
|
135
|
+
const error = new CsrfError('missing');
|
|
136
|
+
emitSecurityViolation(error);
|
|
137
|
+
if (onFailure) {return onFailure(request);}
|
|
138
|
+
return error.toResponse();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Constant-time comparison to prevent timing attacks
|
|
142
|
+
if (!timingSafeEqual(cookieToken, headerToken)) {
|
|
143
|
+
const error = new CsrfError('mismatch');
|
|
144
|
+
emitSecurityViolation(error);
|
|
145
|
+
if (onFailure) {return onFailure(request);}
|
|
146
|
+
return error.toResponse();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// CSRF check passed
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Constant-time string comparison to prevent timing attacks.
|
|
156
|
+
* Both strings must be the same length for timing safety.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} a
|
|
159
|
+
* @param {string} b
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
function timingSafeEqual(a, b) {
|
|
163
|
+
if (a.length !== b.length) {return false;}
|
|
164
|
+
|
|
165
|
+
const encoder = new TextEncoder();
|
|
166
|
+
const bufA = encoder.encode(a);
|
|
167
|
+
const bufB = encoder.encode(b);
|
|
168
|
+
|
|
169
|
+
// Use constant-time XOR comparison
|
|
170
|
+
let result = 0;
|
|
171
|
+
for (let i = 0; i < bufA.length; i++) {
|
|
172
|
+
result |= bufA[i] ^ bufB[i];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result === 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Helper to inject CSRF token into a page response.
|
|
180
|
+
* Sets the cookie and adds a meta tag for client-side access.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} html - The HTML response body
|
|
183
|
+
* @param {string} token - The CSRF token
|
|
184
|
+
* @returns {{ html: string, cookie: string }} Modified HTML and cookie header
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const token = generateCsrfToken();
|
|
188
|
+
* const { html, cookie } = injectCsrfToken(pageHtml, token);
|
|
189
|
+
* return new Response(html, {
|
|
190
|
+
* headers: {
|
|
191
|
+
* 'Content-Type': 'text/html',
|
|
192
|
+
* 'Set-Cookie': cookie
|
|
193
|
+
* }
|
|
194
|
+
* });
|
|
195
|
+
*/
|
|
196
|
+
export function injectCsrfToken(html, token) {
|
|
197
|
+
const metaTag = `<meta name="csrf-token" content="${token}">`;
|
|
198
|
+
const modifiedHtml = html.replace('</head>', ` ${metaTag}\n</head>`);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
html: modifiedHtml,
|
|
202
|
+
cookie: createCsrfCookie(token)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default { generateCsrfToken, createCsrfCookie, createCsrfMiddleware, injectCsrfToken };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Database Layer
|
|
3
|
+
* Zero-config SQLite integration powered by Bun
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Database } from "bun:sqlite";
|
|
7
|
+
|
|
8
|
+
// Auto-create db file
|
|
9
|
+
const db = new Database("africode.sqlite", { create: true });
|
|
10
|
+
|
|
11
|
+
// Initialize Schema
|
|
12
|
+
db.run(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
email TEXT UNIQUE,
|
|
16
|
+
password_hash TEXT,
|
|
17
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
18
|
+
);
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
db.run(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
user_id INTEGER,
|
|
25
|
+
expires_at DATETIME,
|
|
26
|
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
export default db;
|
|
31
|
+
|
|
32
|
+
// Helper query function
|
|
33
|
+
export const query = {
|
|
34
|
+
getUserByEmail: (email) => db.query("SELECT * FROM users WHERE email = ?").get(email),
|
|
35
|
+
createUser: (email, hash) => db.run("INSERT INTO users (email, password_hash) VALUES (?, ?)", [email, hash]),
|
|
36
|
+
createSession: (sid, uid, exp) => db.run("INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)", [sid, uid, exp]),
|
|
37
|
+
getSession: (sid) => db.query("SELECT * FROM sessions WHERE id = ?").get(sid),
|
|
38
|
+
deleteSession: (sid) => db.run("DELETE FROM sessions WHERE id = ?", [sid])
|
|
39
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Middleware Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Chainable middleware system for cross-cutting concerns:
|
|
5
|
+
* - CORS handling
|
|
6
|
+
* - Request logging
|
|
7
|
+
* - Authentication checks
|
|
8
|
+
* - Rate limiting
|
|
9
|
+
* - Error handling
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```javascript
|
|
13
|
+
* const app = new MiddlewareApp();
|
|
14
|
+
* app.use(cors({ origin: 'https://example.com' }));
|
|
15
|
+
* app.use(logging());
|
|
16
|
+
* app.use(authRequired());
|
|
17
|
+
* app.listen(3000);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export class MiddlewareApp {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.middlewares = [];
|
|
24
|
+
this.errorHandlers = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a middleware function
|
|
29
|
+
* Middleware receives (request, response, next) and can:
|
|
30
|
+
* - Modify request/response
|
|
31
|
+
* - Call next() to continue
|
|
32
|
+
* - Return a response to short-circuit
|
|
33
|
+
*/
|
|
34
|
+
use(middleware) {
|
|
35
|
+
if (typeof middleware !== 'function') {
|
|
36
|
+
throw new Error('Middleware must be a function');
|
|
37
|
+
}
|
|
38
|
+
this.middlewares.push(middleware);
|
|
39
|
+
return this; // Enable chaining
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register error handling middleware
|
|
44
|
+
*/
|
|
45
|
+
onError(handler) {
|
|
46
|
+
if (typeof handler !== 'function') {
|
|
47
|
+
throw new Error('Error handler must be a function');
|
|
48
|
+
}
|
|
49
|
+
this.errorHandlers.push(handler);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute middleware chain
|
|
55
|
+
*/
|
|
56
|
+
async executeMiddlewares(request, response) {
|
|
57
|
+
let index = 0;
|
|
58
|
+
|
|
59
|
+
const next = async () => {
|
|
60
|
+
if (index >= this.middlewares.length) {
|
|
61
|
+
return; // End of chain
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const middleware = this.middlewares[index++];
|
|
65
|
+
try {
|
|
66
|
+
await middleware(request, response, next);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Call error handlers
|
|
69
|
+
for (const handler of this.errorHandlers) {
|
|
70
|
+
await handler(error, request, response);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start HTTP server
|
|
80
|
+
*/
|
|
81
|
+
listen(port = 3000, callback) {
|
|
82
|
+
const server = Bun.serve({
|
|
83
|
+
port,
|
|
84
|
+
fetch: async (request) => {
|
|
85
|
+
const response = new Response();
|
|
86
|
+
await this.executeMiddlewares(request, response);
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(`[Middleware] Server listening on port ${port}`);
|
|
92
|
+
if (callback) callback();
|
|
93
|
+
return server;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Built-in Middleware Functions
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* CORS Middleware
|
|
103
|
+
* Handles Cross-Origin Resource Sharing
|
|
104
|
+
*/
|
|
105
|
+
export function cors(options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
origin = '*',
|
|
108
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
109
|
+
allowedHeaders = ['Content-Type', 'Authorization'],
|
|
110
|
+
credentials = false,
|
|
111
|
+
maxAge = 3600
|
|
112
|
+
} = options;
|
|
113
|
+
|
|
114
|
+
return async (request, response, next) => {
|
|
115
|
+
const requestOrigin = request.headers.get('origin');
|
|
116
|
+
|
|
117
|
+
// Check if origin is allowed
|
|
118
|
+
let allowOrigin = false;
|
|
119
|
+
if (origin === '*') {
|
|
120
|
+
allowOrigin = true;
|
|
121
|
+
} else if (Array.isArray(origin)) {
|
|
122
|
+
allowOrigin = origin.includes(requestOrigin);
|
|
123
|
+
} else if (typeof origin === 'function') {
|
|
124
|
+
allowOrigin = origin(requestOrigin);
|
|
125
|
+
} else {
|
|
126
|
+
allowOrigin = origin === requestOrigin;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (allowOrigin) {
|
|
130
|
+
response.headers.set('Access-Control-Allow-Origin', requestOrigin || '*');
|
|
131
|
+
response.headers.set('Access-Control-Allow-Methods', methods.join(', '));
|
|
132
|
+
response.headers.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
133
|
+
response.headers.set('Access-Control-Max-Age', maxAge.toString());
|
|
134
|
+
|
|
135
|
+
if (credentials) {
|
|
136
|
+
response.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle preflight requests
|
|
141
|
+
if (request.method === 'OPTIONS') {
|
|
142
|
+
return new Response(null, { status: 204 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await next();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Request Logging Middleware
|
|
151
|
+
*/
|
|
152
|
+
export function logging(options = {}) {
|
|
153
|
+
const {
|
|
154
|
+
format = 'combined',
|
|
155
|
+
logFn = console.log
|
|
156
|
+
} = options;
|
|
157
|
+
|
|
158
|
+
return async (request, response, next) => {
|
|
159
|
+
const startTime = Date.now();
|
|
160
|
+
const method = request.method;
|
|
161
|
+
const url = new URL(request.url);
|
|
162
|
+
const pathname = url.pathname;
|
|
163
|
+
|
|
164
|
+
await next();
|
|
165
|
+
|
|
166
|
+
const duration = Date.now() - startTime;
|
|
167
|
+
const ip = request.headers.get('x-forwarded-for') || '127.0.0.1';
|
|
168
|
+
|
|
169
|
+
if (format === 'json') {
|
|
170
|
+
logFn(JSON.stringify({
|
|
171
|
+
ip,
|
|
172
|
+
method,
|
|
173
|
+
pathname,
|
|
174
|
+
duration,
|
|
175
|
+
timestamp: new Date().toISOString()
|
|
176
|
+
}));
|
|
177
|
+
} else if (format === 'combined') {
|
|
178
|
+
logFn(`[${new Date().toISOString()}] ${ip} - ${method} ${pathname} - ${duration}ms`);
|
|
179
|
+
} else {
|
|
180
|
+
logFn(`[${new Date().toISOString()}] ${method} ${pathname} - ${duration}ms`);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Authentication Required Middleware
|
|
187
|
+
*/
|
|
188
|
+
export function authRequired() {
|
|
189
|
+
return async (request, response, next) => {
|
|
190
|
+
// Extract session from cookies
|
|
191
|
+
const cookieHeader = request.headers.get('Cookie');
|
|
192
|
+
if (!cookieHeader) {
|
|
193
|
+
return new Response(
|
|
194
|
+
JSON.stringify({ error: 'Unauthorized' }),
|
|
195
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sessionMatch = cookieHeader.match(/session_id=([^;]+)/);
|
|
200
|
+
if (!sessionMatch) {
|
|
201
|
+
return new Response(
|
|
202
|
+
JSON.stringify({ error: 'Unauthorized' }),
|
|
203
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Attach session ID to request for later use
|
|
208
|
+
request.sessionId = sessionMatch[1];
|
|
209
|
+
|
|
210
|
+
await next();
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Rate Limiting Middleware
|
|
216
|
+
*/
|
|
217
|
+
export function rateLimit(options = {}) {
|
|
218
|
+
const {
|
|
219
|
+
maxRequests = 100,
|
|
220
|
+
windowMs = 60000, // 1 minute
|
|
221
|
+
keyGenerator = (request) => request.headers.get('x-forwarded-for') || '127.0.0.1'
|
|
222
|
+
} = options;
|
|
223
|
+
|
|
224
|
+
const store = new Map();
|
|
225
|
+
|
|
226
|
+
return async (request, response, next) => {
|
|
227
|
+
const key = keyGenerator(request);
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
const record = store.get(key);
|
|
230
|
+
|
|
231
|
+
// Clean up old records
|
|
232
|
+
if (record && now - record.resetTime > windowMs) {
|
|
233
|
+
store.delete(key);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if limit exceeded
|
|
237
|
+
const current = store.get(key) || { count: 0, resetTime: now };
|
|
238
|
+
if (current.count >= maxRequests) {
|
|
239
|
+
return new Response(
|
|
240
|
+
JSON.stringify({ error: 'Rate limit exceeded' }),
|
|
241
|
+
{ status: 429, headers: { 'Content-Type': 'application/json' } }
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
current.count++;
|
|
246
|
+
store.set(key, current);
|
|
247
|
+
|
|
248
|
+
// Add rate limit headers
|
|
249
|
+
response.headers.set('X-RateLimit-Limit', maxRequests.toString());
|
|
250
|
+
response.headers.set('X-RateLimit-Remaining', (maxRequests - current.count).toString());
|
|
251
|
+
response.headers.set('X-RateLimit-Reset', (current.resetTime + windowMs).toString());
|
|
252
|
+
|
|
253
|
+
await next();
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Error Handling Middleware
|
|
259
|
+
*/
|
|
260
|
+
export function errorHandler() {
|
|
261
|
+
return async (error, request, response) => {
|
|
262
|
+
console.error('[Error]', error);
|
|
263
|
+
|
|
264
|
+
const statusCode = error.status || 500;
|
|
265
|
+
const message = error.message || 'Internal Server Error';
|
|
266
|
+
|
|
267
|
+
response.status = statusCode;
|
|
268
|
+
response.headers.set('Content-Type', 'application/json');
|
|
269
|
+
|
|
270
|
+
return new Response(
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
error: message,
|
|
273
|
+
status: statusCode,
|
|
274
|
+
timestamp: new Date().toISOString()
|
|
275
|
+
}),
|
|
276
|
+
{ status: statusCode, headers: { 'Content-Type': 'application/json' } }
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Content Type Validation Middleware
|
|
283
|
+
*/
|
|
284
|
+
export function validateContentType(acceptedTypes = ['application/json']) {
|
|
285
|
+
return async (request, response, next) => {
|
|
286
|
+
if (request.method === 'POST' || request.method === 'PUT') {
|
|
287
|
+
const contentType = request.headers.get('Content-Type')?.split(';')[0];
|
|
288
|
+
if (!acceptedTypes.includes(contentType)) {
|
|
289
|
+
return new Response(
|
|
290
|
+
JSON.stringify({ error: 'Unsupported Content-Type' }),
|
|
291
|
+
{ status: 415, headers: { 'Content-Type': 'application/json' } }
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
await next();
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Security Headers Middleware
|
|
301
|
+
*/
|
|
302
|
+
export function securityHeaders() {
|
|
303
|
+
return async (request, response, next) => {
|
|
304
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
305
|
+
response.headers.set('X-Frame-Options', 'DENY');
|
|
306
|
+
response.headers.set('X-XSS-Protection', '1; mode=block');
|
|
307
|
+
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
308
|
+
response.headers.set('Content-Security-Policy', "default-src 'self'");
|
|
309
|
+
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
310
|
+
|
|
311
|
+
await next();
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export default {
|
|
316
|
+
MiddlewareApp,
|
|
317
|
+
cors,
|
|
318
|
+
logging,
|
|
319
|
+
authRequired,
|
|
320
|
+
rateLimit,
|
|
321
|
+
errorHandler,
|
|
322
|
+
validateContentType,
|
|
323
|
+
securityHeaders
|
|
324
|
+
};
|