archicore 0.3.1 → 0.3.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/README.md +48 -4
- package/dist/cli/commands/interactive.js +83 -23
- package/dist/cli/commands/projects.js +3 -3
- package/dist/cli/ui/prompt.d.ts +4 -0
- package/dist/cli/ui/prompt.js +22 -0
- package/dist/cli/utils/config.js +2 -2
- package/dist/cli/utils/upload-utils.js +65 -18
- package/dist/code-index/ast-parser.d.ts +4 -0
- package/dist/code-index/ast-parser.js +42 -0
- package/dist/code-index/index.d.ts +21 -1
- package/dist/code-index/index.js +45 -1
- package/dist/code-index/source-map-extractor.d.ts +71 -0
- package/dist/code-index/source-map-extractor.js +194 -0
- package/dist/gitlab/gitlab-service.d.ts +162 -0
- package/dist/gitlab/gitlab-service.js +652 -0
- package/dist/gitlab/index.d.ts +8 -0
- package/dist/gitlab/index.js +8 -0
- package/dist/server/config/passport.d.ts +14 -0
- package/dist/server/config/passport.js +86 -0
- package/dist/server/index.js +52 -10
- package/dist/server/middleware/api-auth.d.ts +2 -2
- package/dist/server/middleware/api-auth.js +21 -2
- package/dist/server/middleware/csrf.d.ts +23 -0
- package/dist/server/middleware/csrf.js +96 -0
- package/dist/server/routes/auth.d.ts +2 -2
- package/dist/server/routes/auth.js +204 -5
- package/dist/server/routes/device-auth.js +2 -2
- package/dist/server/routes/gitlab.d.ts +12 -0
- package/dist/server/routes/gitlab.js +528 -0
- package/dist/server/routes/oauth.d.ts +6 -0
- package/dist/server/routes/oauth.js +198 -0
- package/dist/server/services/audit-service.d.ts +1 -1
- package/dist/server/services/auth-service.d.ts +13 -1
- package/dist/server/services/auth-service.js +108 -7
- package/dist/server/services/email-service.d.ts +63 -0
- package/dist/server/services/email-service.js +586 -0
- package/dist/server/utils/disposable-email-domains.d.ts +14 -0
- package/dist/server/utils/disposable-email-domains.js +192 -0
- package/dist/types/api.d.ts +98 -0
- package/dist/types/gitlab.d.ts +245 -0
- package/dist/types/gitlab.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passport OAuth Configuration
|
|
3
|
+
* Google and GitHub authentication strategies
|
|
4
|
+
*/
|
|
5
|
+
import passport from 'passport';
|
|
6
|
+
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
|
7
|
+
import { Strategy as GitHubStrategy } from 'passport-github2';
|
|
8
|
+
import { Logger } from '../../utils/logger.js';
|
|
9
|
+
// Google OAuth Strategy
|
|
10
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
11
|
+
passport.use(new GoogleStrategy({
|
|
12
|
+
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
13
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
14
|
+
callbackURL: process.env.GOOGLE_CALLBACK_URL || '/api/auth/oauth/google/callback',
|
|
15
|
+
scope: ['profile', 'email'],
|
|
16
|
+
}, async (_accessToken, _refreshToken, profile, done) => {
|
|
17
|
+
try {
|
|
18
|
+
Logger.info(`[OAuth] Google authentication for user: ${profile.emails?.[0]?.value}`);
|
|
19
|
+
const oauthProfile = {
|
|
20
|
+
id: profile.id,
|
|
21
|
+
email: profile.emails?.[0]?.value || '',
|
|
22
|
+
displayName: profile.displayName,
|
|
23
|
+
avatar: profile.photos?.[0]?.value,
|
|
24
|
+
provider: 'google',
|
|
25
|
+
};
|
|
26
|
+
// Validate email
|
|
27
|
+
if (!oauthProfile.email) {
|
|
28
|
+
return done(new Error('No email provided by Google'));
|
|
29
|
+
}
|
|
30
|
+
done(null, oauthProfile);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
Logger.error('[OAuth] Google authentication error:', error);
|
|
34
|
+
done(error);
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
Logger.info('[OAuth] Google strategy configured');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
Logger.warn('[OAuth] Google OAuth not configured - missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET');
|
|
41
|
+
}
|
|
42
|
+
// GitHub OAuth Strategy
|
|
43
|
+
if (process.env.GITHUB_OAUTH_CLIENT_ID && process.env.GITHUB_OAUTH_CLIENT_SECRET) {
|
|
44
|
+
passport.use(new GitHubStrategy({
|
|
45
|
+
clientID: process.env.GITHUB_OAUTH_CLIENT_ID,
|
|
46
|
+
clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
|
|
47
|
+
callbackURL: process.env.GITHUB_OAUTH_CALLBACK_URL || '/api/auth/oauth/github/callback',
|
|
48
|
+
scope: ['user:email'],
|
|
49
|
+
}, async (_accessToken, _refreshToken, profile, done) => {
|
|
50
|
+
try {
|
|
51
|
+
Logger.info(`[OAuth] GitHub authentication for user: ${profile.username}`);
|
|
52
|
+
// GitHub might not provide email in profile, check emails array
|
|
53
|
+
const email = profile.emails?.[0]?.value || profile._json?.email || '';
|
|
54
|
+
const oauthProfile = {
|
|
55
|
+
id: profile.id,
|
|
56
|
+
email,
|
|
57
|
+
displayName: profile.displayName || profile.username || '',
|
|
58
|
+
avatar: profile.photos?.[0]?.value || profile._json?.avatar_url,
|
|
59
|
+
provider: 'github',
|
|
60
|
+
};
|
|
61
|
+
// Validate email
|
|
62
|
+
if (!oauthProfile.email) {
|
|
63
|
+
return done(new Error('No email provided by GitHub. Please make your email public in GitHub settings.'));
|
|
64
|
+
}
|
|
65
|
+
done(null, oauthProfile);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
Logger.error('[OAuth] GitHub authentication error:', error);
|
|
69
|
+
done(error);
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
Logger.info('[OAuth] GitHub strategy configured');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
Logger.warn('[OAuth] GitHub OAuth not configured - missing GITHUB_OAUTH_CLIENT_ID or GITHUB_OAUTH_CLIENT_SECRET');
|
|
76
|
+
}
|
|
77
|
+
// Serialize user to session (not used in stateless JWT, but required by passport)
|
|
78
|
+
passport.serializeUser((user, done) => {
|
|
79
|
+
done(null, user);
|
|
80
|
+
});
|
|
81
|
+
// Deserialize user from session
|
|
82
|
+
passport.deserializeUser((user, done) => {
|
|
83
|
+
done(null, user);
|
|
84
|
+
});
|
|
85
|
+
export default passport;
|
|
86
|
+
//# sourceMappingURL=passport.js.map
|
package/dist/server/index.js
CHANGED
|
@@ -22,14 +22,20 @@ import { Logger } from '../utils/logger.js';
|
|
|
22
22
|
import { apiRouter } from './routes/api.js';
|
|
23
23
|
import { uploadRouter } from './routes/upload.js';
|
|
24
24
|
import { authRouter } from './routes/auth.js';
|
|
25
|
+
import { oauthRouter } from './routes/oauth.js';
|
|
25
26
|
import { adminRouter } from './routes/admin.js';
|
|
26
27
|
import { developerRouter } from './routes/developer.js';
|
|
27
28
|
import { githubRouter } from './routes/github.js';
|
|
29
|
+
import { gitlabRouter } from './routes/gitlab.js';
|
|
28
30
|
import deviceAuthRouter from './routes/device-auth.js';
|
|
29
31
|
import { reportIssueRouter } from './routes/report-issue.js';
|
|
30
32
|
import { cache } from './services/cache.js';
|
|
31
33
|
import { db } from './services/database.js';
|
|
32
34
|
import { AuthService } from './services/auth-service.js';
|
|
35
|
+
import { securityConfig } from './config.js';
|
|
36
|
+
import passport from './config/passport.js';
|
|
37
|
+
import cookieParser from 'cookie-parser';
|
|
38
|
+
import { csrfProtection, csrfTokenEndpoint } from './middleware/csrf.js';
|
|
33
39
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
40
|
const __dirname = path.dirname(__filename);
|
|
35
41
|
// CORS whitelist - настройте под свои домены
|
|
@@ -153,17 +159,19 @@ export class ArchiCoreServer {
|
|
|
153
159
|
this.setupErrorHandling();
|
|
154
160
|
}
|
|
155
161
|
setupMiddleware() {
|
|
156
|
-
// Security headers (helmet) -
|
|
157
|
-
// Set HELMET_ENABLED=
|
|
158
|
-
const helmetEnabled = process.env.HELMET_ENABLED
|
|
162
|
+
// Security headers (helmet) - enabled by default for security
|
|
163
|
+
// Set HELMET_ENABLED=false to disable (not recommended)
|
|
164
|
+
const helmetEnabled = process.env.HELMET_ENABLED !== 'false';
|
|
159
165
|
if (helmetEnabled) {
|
|
160
166
|
this.app.use(helmet({
|
|
161
167
|
contentSecurityPolicy: {
|
|
162
168
|
directives: {
|
|
163
169
|
defaultSrc: ["'self'"],
|
|
164
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
165
|
-
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
170
|
+
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|
171
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
|
|
172
|
+
scriptSrcAttr: ["'unsafe-inline'", "'unsafe-hashes'"],
|
|
166
173
|
imgSrc: ["'self'", "data:", "https:"],
|
|
174
|
+
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
167
175
|
connectSrc: ["'self'", "https://api.jina.ai", "https://api.openai.com", "https://api.anthropic.com"],
|
|
168
176
|
},
|
|
169
177
|
},
|
|
@@ -196,8 +204,8 @@ export class ArchiCoreServer {
|
|
|
196
204
|
const globalLimiter = createRateLimiter(this.config.rateLimitWindowMs || 60 * 1000, this.config.rateLimitMax || 100, 'Too many requests, please try again later');
|
|
197
205
|
this.app.use(globalLimiter);
|
|
198
206
|
// CORS configuration
|
|
199
|
-
// Set CORS_RESTRICT=
|
|
200
|
-
const restrictCors = process.env.CORS_RESTRICT
|
|
207
|
+
// Set CORS_RESTRICT=false in .env to disable whitelist mode (not recommended for production)
|
|
208
|
+
const restrictCors = process.env.CORS_RESTRICT !== 'false';
|
|
201
209
|
const allowedOrigins = this.config.corsOrigins || DEFAULT_CORS_ORIGINS;
|
|
202
210
|
this.app.use(cors({
|
|
203
211
|
origin: restrictCors
|
|
@@ -224,10 +232,14 @@ export class ArchiCoreServer {
|
|
|
224
232
|
}
|
|
225
233
|
: true, // Allow all origins when CORS_RESTRICT is not set
|
|
226
234
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
227
|
-
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'],
|
|
235
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID', 'x-csrf-token'],
|
|
228
236
|
credentials: true,
|
|
229
237
|
maxAge: 86400, // Cache preflight for 24 hours
|
|
230
238
|
}));
|
|
239
|
+
// Cookie parser for OAuth session
|
|
240
|
+
this.app.use(cookieParser());
|
|
241
|
+
// Passport initialization for OAuth
|
|
242
|
+
this.app.use(passport.initialize());
|
|
231
243
|
// JSON парсинг - лимит должен покрывать максимальный тариф (admin = 1GB)
|
|
232
244
|
// Проверка по тарифу пользователя происходит в бизнес-логике после парсинга
|
|
233
245
|
this.app.use(express.json({ limit: '1gb' }));
|
|
@@ -251,8 +263,32 @@ export class ArchiCoreServer {
|
|
|
251
263
|
}));
|
|
252
264
|
}
|
|
253
265
|
setupRoutes() {
|
|
254
|
-
//
|
|
266
|
+
// Strict rate limiting for auth endpoints to prevent brute force attacks
|
|
267
|
+
const authRateLimiter = createRateLimiter(15 * 60 * 1000, // 15 minutes
|
|
268
|
+
20, // Max 20 attempts per 15 minutes
|
|
269
|
+
'Too many authentication attempts, please try again later');
|
|
270
|
+
const verificationRateLimiter = createRateLimiter(60 * 60 * 1000, // 1 hour
|
|
271
|
+
5, // Max 5 verification requests per hour
|
|
272
|
+
'Too many verification code requests, please try again later');
|
|
273
|
+
// CSRF token endpoint (no protection needed for GET)
|
|
274
|
+
this.app.get('/api/auth/csrf-token', csrfTokenEndpoint);
|
|
275
|
+
// CSRF protection for state-changing auth operations
|
|
276
|
+
// Ignore paths that use API keys or device auth flow
|
|
277
|
+
const csrfMiddleware = csrfProtection({
|
|
278
|
+
ignorePaths: [
|
|
279
|
+
'/api/auth/device',
|
|
280
|
+
'/api/auth/oauth',
|
|
281
|
+
'/api/developer',
|
|
282
|
+
'/api/v1'
|
|
283
|
+
]
|
|
284
|
+
});
|
|
285
|
+
// Auth routes with rate limiting and CSRF protection
|
|
286
|
+
this.app.use('/api/auth/login', authRateLimiter, csrfMiddleware);
|
|
287
|
+
this.app.use('/api/auth/register', authRateLimiter, csrfMiddleware);
|
|
288
|
+
this.app.use('/api/auth/send-verification-code', verificationRateLimiter, csrfMiddleware);
|
|
255
289
|
this.app.use('/api/auth', authRouter);
|
|
290
|
+
// OAuth routes (Google, GitHub)
|
|
291
|
+
this.app.use('/api/auth/oauth', oauthRouter);
|
|
256
292
|
// Device auth for CLI
|
|
257
293
|
this.app.use('/api/auth/device', deviceAuthRouter);
|
|
258
294
|
// Admin routes
|
|
@@ -261,6 +297,8 @@ export class ArchiCoreServer {
|
|
|
261
297
|
this.app.use('/api/developer', developerRouter);
|
|
262
298
|
// GitHub integration routes
|
|
263
299
|
this.app.use('/api/github', githubRouter);
|
|
300
|
+
// GitLab integration routes
|
|
301
|
+
this.app.use('/api/gitlab', gitlabRouter);
|
|
264
302
|
// API маршруты
|
|
265
303
|
this.app.use('/api', apiRouter);
|
|
266
304
|
// Upload маршруты
|
|
@@ -405,7 +443,11 @@ const isMainModule = process.argv[1]?.includes('server');
|
|
|
405
443
|
if (isMainModule) {
|
|
406
444
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
407
445
|
const host = process.env.HOST || '0.0.0.0';
|
|
408
|
-
const server = new ArchiCoreServer({
|
|
446
|
+
const server = new ArchiCoreServer({
|
|
447
|
+
port,
|
|
448
|
+
host,
|
|
449
|
+
corsOrigins: securityConfig.cors.allowedOrigins
|
|
450
|
+
});
|
|
409
451
|
server.start().catch((err) => {
|
|
410
452
|
Logger.error('Failed to start server:', err);
|
|
411
453
|
process.exit(1);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Аутентификация через API ключи, rate limiting, проверка permissions
|
|
5
5
|
*/
|
|
6
6
|
import { Request, Response, NextFunction } from 'express';
|
|
7
|
-
import { ApiPermission, ApiRequestContext } from '../../types/api.js';
|
|
7
|
+
import { ApiPermission, ApiRequestContext, OperationCategory } from '../../types/api.js';
|
|
8
8
|
declare global {
|
|
9
9
|
namespace Express {
|
|
10
10
|
interface Request {
|
|
@@ -31,7 +31,7 @@ export declare function checkBalance(estimatedTokens: number): (req: Request, re
|
|
|
31
31
|
/**
|
|
32
32
|
* Wrapper для автоматического трекинга использования токенов
|
|
33
33
|
*/
|
|
34
|
-
export declare function trackUsage(operation:
|
|
34
|
+
export declare function trackUsage(operation: OperationCategory): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
35
35
|
/**
|
|
36
36
|
* Error handler для API ошибок
|
|
37
37
|
*/
|
|
@@ -152,6 +152,24 @@ export function checkBalance(estimatedTokens) {
|
|
|
152
152
|
next();
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Map operation category to ApiOperation for internal processing
|
|
157
|
+
*/
|
|
158
|
+
function mapOperationToApiOperation(operation) {
|
|
159
|
+
const mapping = {
|
|
160
|
+
'index': 'index',
|
|
161
|
+
'analyze': 'analyze_full',
|
|
162
|
+
'search': 'search_semantic',
|
|
163
|
+
'ask': 'ask_architect',
|
|
164
|
+
'export': 'export',
|
|
165
|
+
'refactoring': 'refactoring',
|
|
166
|
+
'metrics': 'metrics',
|
|
167
|
+
'rules': 'rules_check',
|
|
168
|
+
'dead_code': 'dead_code',
|
|
169
|
+
'duplication': 'duplication'
|
|
170
|
+
};
|
|
171
|
+
return mapping[operation] || 'index';
|
|
172
|
+
}
|
|
155
173
|
/**
|
|
156
174
|
* Wrapper для автоматического трекинга использования токенов
|
|
157
175
|
*/
|
|
@@ -164,18 +182,19 @@ export function trackUsage(operation) {
|
|
|
164
182
|
// Сохраняем оригинальный json метод
|
|
165
183
|
const originalJson = res.json.bind(res);
|
|
166
184
|
const startTime = Date.now();
|
|
185
|
+
const apiOperation = mapOperationToApiOperation(operation);
|
|
167
186
|
// Перехватываем ответ
|
|
168
187
|
res.json = function (body) {
|
|
169
188
|
// Подсчитываем токены на основе размера ответа
|
|
170
189
|
const responseSize = JSON.stringify(body).length;
|
|
171
190
|
const inputSize = JSON.stringify(req.body || {}).length;
|
|
172
|
-
const tokens = tokenService.calculateTokens(
|
|
191
|
+
const tokens = tokenService.calculateTokens(apiOperation, {
|
|
173
192
|
inputText: JSON.stringify(req.body || {}),
|
|
174
193
|
outputText: JSON.stringify(body),
|
|
175
194
|
contentSizeKb: Math.ceil((inputSize + responseSize) / 1024)
|
|
176
195
|
});
|
|
177
196
|
// Записываем использование асинхронно (не блокируем ответ)
|
|
178
|
-
tokenService.recordUsage(req.apiContext.apiKey.id, req.apiContext.userId,
|
|
197
|
+
tokenService.recordUsage(req.apiContext.apiKey.id, req.apiContext.userId, apiOperation, tokens, req.params.id, // projectId if present
|
|
179
198
|
{
|
|
180
199
|
endpoint: req.path,
|
|
181
200
|
method: req.method,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF Protection Middleware
|
|
3
|
+
*
|
|
4
|
+
* Implements double-submit cookie pattern for CSRF protection.
|
|
5
|
+
* This is a modern alternative to the deprecated csurf package.
|
|
6
|
+
*/
|
|
7
|
+
import { Request, Response, NextFunction } from 'express';
|
|
8
|
+
/**
|
|
9
|
+
* CSRF Protection Middleware
|
|
10
|
+
*
|
|
11
|
+
* For GET requests: Sets a CSRF cookie and provides the token in response
|
|
12
|
+
* For state-changing requests (POST, PUT, DELETE, PATCH): Validates the token
|
|
13
|
+
*/
|
|
14
|
+
export declare function csrfProtection(options?: {
|
|
15
|
+
ignoreMethods?: string[];
|
|
16
|
+
ignorePaths?: string[];
|
|
17
|
+
}): (req: Request, res: Response, next: NextFunction) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Endpoint to get CSRF token
|
|
20
|
+
* GET /api/auth/csrf-token
|
|
21
|
+
*/
|
|
22
|
+
export declare function csrfTokenEndpoint(req: Request, res: Response): void;
|
|
23
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF Protection Middleware
|
|
3
|
+
*
|
|
4
|
+
* Implements double-submit cookie pattern for CSRF protection.
|
|
5
|
+
* This is a modern alternative to the deprecated csurf package.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
const CSRF_COOKIE_NAME = 'archicore_csrf';
|
|
9
|
+
const CSRF_HEADER_NAME = 'x-csrf-token';
|
|
10
|
+
const CSRF_TOKEN_LENGTH = 32;
|
|
11
|
+
/**
|
|
12
|
+
* Generate a cryptographically secure CSRF token
|
|
13
|
+
*/
|
|
14
|
+
function generateCsrfToken() {
|
|
15
|
+
return crypto.randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate that the token from header matches the token from cookie
|
|
19
|
+
*/
|
|
20
|
+
function validateCsrfToken(cookieToken, headerToken) {
|
|
21
|
+
if (!cookieToken || !headerToken) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
25
|
+
try {
|
|
26
|
+
return crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* CSRF Protection Middleware
|
|
34
|
+
*
|
|
35
|
+
* For GET requests: Sets a CSRF cookie and provides the token in response
|
|
36
|
+
* For state-changing requests (POST, PUT, DELETE, PATCH): Validates the token
|
|
37
|
+
*/
|
|
38
|
+
export function csrfProtection(options = {}) {
|
|
39
|
+
const ignoreMethods = options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS'];
|
|
40
|
+
const ignorePaths = options.ignorePaths || [];
|
|
41
|
+
return (req, res, next) => {
|
|
42
|
+
// Skip CSRF for ignored paths
|
|
43
|
+
if (ignorePaths.some(path => req.path.startsWith(path))) {
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
// Skip CSRF for API key authenticated requests (they use different auth)
|
|
47
|
+
if (req.headers['x-api-key'] || req.headers.authorization?.startsWith('Bearer arc_')) {
|
|
48
|
+
return next();
|
|
49
|
+
}
|
|
50
|
+
// For safe methods, just ensure token exists
|
|
51
|
+
if (ignoreMethods.includes(req.method)) {
|
|
52
|
+
// Generate token if not present
|
|
53
|
+
if (!req.cookies[CSRF_COOKIE_NAME]) {
|
|
54
|
+
const token = generateCsrfToken();
|
|
55
|
+
res.cookie(CSRF_COOKIE_NAME, token, {
|
|
56
|
+
httpOnly: false, // Must be readable by JS
|
|
57
|
+
secure: process.env.NODE_ENV === 'production',
|
|
58
|
+
sameSite: 'strict',
|
|
59
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
60
|
+
path: '/'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
// For state-changing methods, validate the token
|
|
66
|
+
const cookieToken = req.cookies[CSRF_COOKIE_NAME];
|
|
67
|
+
const headerToken = req.headers[CSRF_HEADER_NAME];
|
|
68
|
+
if (!validateCsrfToken(cookieToken, headerToken)) {
|
|
69
|
+
res.status(403).json({
|
|
70
|
+
error: 'CSRF token validation failed',
|
|
71
|
+
message: 'Invalid or missing CSRF token. Please refresh the page and try again.'
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
next();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Endpoint to get CSRF token
|
|
80
|
+
* GET /api/auth/csrf-token
|
|
81
|
+
*/
|
|
82
|
+
export function csrfTokenEndpoint(req, res) {
|
|
83
|
+
let token = req.cookies[CSRF_COOKIE_NAME];
|
|
84
|
+
if (!token) {
|
|
85
|
+
token = generateCsrfToken();
|
|
86
|
+
res.cookie(CSRF_COOKIE_NAME, token, {
|
|
87
|
+
httpOnly: false,
|
|
88
|
+
secure: process.env.NODE_ENV === 'production',
|
|
89
|
+
sameSite: 'strict',
|
|
90
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
91
|
+
path: '/'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
res.json({ csrfToken: token });
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=csrf.js.map
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Authentication API Routes for ArchiCore
|
|
3
3
|
*/
|
|
4
4
|
import { Request, Response, NextFunction } from 'express';
|
|
5
|
-
import { User } from '../../types/user.js';
|
|
5
|
+
import { User as ArchiCoreUser } from '../../types/user.js';
|
|
6
6
|
export declare const authRouter: import("express-serve-static-core").Router;
|
|
7
7
|
declare global {
|
|
8
8
|
namespace Express {
|
|
9
9
|
interface Request {
|
|
10
|
-
user?:
|
|
10
|
+
user?: ArchiCoreUser;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|