archicore 0.3.0 → 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.
Files changed (42) hide show
  1. package/README.md +2267 -374
  2. package/dist/cli/commands/interactive.js +83 -23
  3. package/dist/cli/commands/projects.js +3 -3
  4. package/dist/cli/ui/prompt.d.ts +4 -0
  5. package/dist/cli/ui/prompt.js +22 -0
  6. package/dist/cli/utils/config.js +2 -2
  7. package/dist/cli/utils/upload-utils.js +65 -18
  8. package/dist/code-index/ast-parser.d.ts +4 -0
  9. package/dist/code-index/ast-parser.js +42 -0
  10. package/dist/code-index/index.d.ts +21 -1
  11. package/dist/code-index/index.js +45 -1
  12. package/dist/code-index/source-map-extractor.d.ts +71 -0
  13. package/dist/code-index/source-map-extractor.js +194 -0
  14. package/dist/gitlab/gitlab-service.d.ts +162 -0
  15. package/dist/gitlab/gitlab-service.js +652 -0
  16. package/dist/gitlab/index.d.ts +8 -0
  17. package/dist/gitlab/index.js +8 -0
  18. package/dist/server/config/passport.d.ts +14 -0
  19. package/dist/server/config/passport.js +86 -0
  20. package/dist/server/index.js +52 -10
  21. package/dist/server/middleware/api-auth.d.ts +2 -2
  22. package/dist/server/middleware/api-auth.js +21 -2
  23. package/dist/server/middleware/csrf.d.ts +23 -0
  24. package/dist/server/middleware/csrf.js +96 -0
  25. package/dist/server/routes/auth.d.ts +2 -2
  26. package/dist/server/routes/auth.js +204 -5
  27. package/dist/server/routes/device-auth.js +2 -2
  28. package/dist/server/routes/gitlab.d.ts +12 -0
  29. package/dist/server/routes/gitlab.js +528 -0
  30. package/dist/server/routes/oauth.d.ts +6 -0
  31. package/dist/server/routes/oauth.js +198 -0
  32. package/dist/server/services/audit-service.d.ts +1 -1
  33. package/dist/server/services/auth-service.d.ts +13 -1
  34. package/dist/server/services/auth-service.js +108 -7
  35. package/dist/server/services/email-service.d.ts +63 -0
  36. package/dist/server/services/email-service.js +586 -0
  37. package/dist/server/utils/disposable-email-domains.d.ts +14 -0
  38. package/dist/server/utils/disposable-email-domains.js +192 -0
  39. package/dist/types/api.d.ts +98 -0
  40. package/dist/types/gitlab.d.ts +245 -0
  41. package/dist/types/gitlab.js +11 -0
  42. package/package.json +12 -4
@@ -0,0 +1,652 @@
1
+ /**
2
+ * GitLab Integration Service
3
+ *
4
+ * Manages GitLab connections with support for:
5
+ * - Multiple GitLab instances (self-hosted + gitlab.com)
6
+ * - Personal Access Token (PAT) authentication
7
+ * - Private repositories access
8
+ * - Branch listing and selection
9
+ * - Webhooks for auto-analysis
10
+ * - Merge Request analysis
11
+ */
12
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
13
+ import { readFile, writeFile, mkdir } from 'fs/promises';
14
+ import { existsSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { v4 as uuidv4 } from 'uuid';
17
+ import { Logger } from '../utils/logger.js';
18
+ // Storage paths
19
+ const DATA_DIR = process.env.ARCHICORE_DATA_DIR || '.archicore';
20
+ const INSTANCES_FILE = 'gitlab-instances.json';
21
+ const REPOSITORIES_FILE = 'gitlab-repositories.json';
22
+ // Encryption key for tokens
23
+ const ENCRYPTION_KEY = process.env.GITLAB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY || 'archicore-gitlab-key-32bytes!!';
24
+ // Webhook base URL - construct from PUBLIC_URL or API_URL
25
+ const getWebhookUrl = () => {
26
+ // Use explicit GitLab webhook URL if set
27
+ if (process.env.GITLAB_WEBHOOK_URL) {
28
+ return process.env.GITLAB_WEBHOOK_URL;
29
+ }
30
+ // Otherwise construct from API_URL or PUBLIC_URL
31
+ const baseUrl = process.env.API_URL || process.env.PUBLIC_URL || 'http://localhost:3000';
32
+ return `${baseUrl}/api/gitlab/webhook`;
33
+ };
34
+ const WEBHOOK_BASE_URL = getWebhookUrl();
35
+ // Singleton instance
36
+ let instance = null;
37
+ export class GitLabService {
38
+ instances = [];
39
+ repositories = [];
40
+ initialized = false;
41
+ dataDir = DATA_DIR;
42
+ constructor(config) {
43
+ // Singleton pattern
44
+ if (instance) {
45
+ return instance;
46
+ }
47
+ instance = this;
48
+ this.dataDir = config?.dataDir || DATA_DIR;
49
+ }
50
+ // ===== INITIALIZATION =====
51
+ async ensureInitialized() {
52
+ if (this.initialized)
53
+ return;
54
+ try {
55
+ await this.loadInstances();
56
+ await this.loadRepositories();
57
+ this.initialized = true;
58
+ }
59
+ catch (error) {
60
+ Logger.error('GitLab service initialization failed:', error);
61
+ throw error;
62
+ }
63
+ }
64
+ async loadInstances() {
65
+ const filePath = join(this.dataDir, INSTANCES_FILE);
66
+ if (existsSync(filePath)) {
67
+ try {
68
+ const content = await readFile(filePath, 'utf-8');
69
+ const data = JSON.parse(content);
70
+ this.instances = data.instances || [];
71
+ Logger.info(`Loaded ${this.instances.length} GitLab instances`);
72
+ }
73
+ catch (error) {
74
+ Logger.warn('Could not load GitLab instances:', error);
75
+ this.instances = [];
76
+ }
77
+ }
78
+ }
79
+ async saveInstances() {
80
+ try {
81
+ if (!existsSync(this.dataDir)) {
82
+ await mkdir(this.dataDir, { recursive: true });
83
+ }
84
+ const filePath = join(this.dataDir, INSTANCES_FILE);
85
+ await writeFile(filePath, JSON.stringify({ instances: this.instances }, null, 2));
86
+ }
87
+ catch (error) {
88
+ Logger.error('Could not save GitLab instances:', error);
89
+ }
90
+ }
91
+ async loadRepositories() {
92
+ const filePath = join(this.dataDir, REPOSITORIES_FILE);
93
+ if (existsSync(filePath)) {
94
+ try {
95
+ const content = await readFile(filePath, 'utf-8');
96
+ const data = JSON.parse(content);
97
+ this.repositories = data.repositories || [];
98
+ Logger.info(`Loaded ${this.repositories.length} connected GitLab repositories`);
99
+ }
100
+ catch (error) {
101
+ Logger.warn('Could not load GitLab repositories:', error);
102
+ this.repositories = [];
103
+ }
104
+ }
105
+ }
106
+ async saveRepositories() {
107
+ try {
108
+ if (!existsSync(this.dataDir)) {
109
+ await mkdir(this.dataDir, { recursive: true });
110
+ }
111
+ const filePath = join(this.dataDir, REPOSITORIES_FILE);
112
+ await writeFile(filePath, JSON.stringify({ repositories: this.repositories }, null, 2));
113
+ }
114
+ catch (error) {
115
+ Logger.error('Could not save GitLab repositories:', error);
116
+ }
117
+ }
118
+ // ===== ENCRYPTION =====
119
+ encrypt(text) {
120
+ const iv = randomBytes(16);
121
+ const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
122
+ const cipher = createCipheriv('aes-256-cbc', key, iv);
123
+ let encrypted = cipher.update(text, 'utf8', 'hex');
124
+ encrypted += cipher.final('hex');
125
+ return iv.toString('hex') + ':' + encrypted;
126
+ }
127
+ decrypt(encrypted) {
128
+ try {
129
+ const parts = encrypted.split(':');
130
+ if (parts.length !== 2)
131
+ return encrypted; // Not encrypted
132
+ const iv = Buffer.from(parts[0], 'hex');
133
+ const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
134
+ const decipher = createDecipheriv('aes-256-cbc', key, iv);
135
+ let decrypted = decipher.update(parts[1], 'hex', 'utf8');
136
+ decrypted += decipher.final('utf8');
137
+ return decrypted;
138
+ }
139
+ catch {
140
+ return encrypted; // Return as-is if decryption fails
141
+ }
142
+ }
143
+ // ===== INSTANCE MANAGEMENT =====
144
+ /**
145
+ * Add a new GitLab instance connection
146
+ */
147
+ async addInstance(userId, instanceUrl, accessToken, options = {}) {
148
+ await this.ensureInitialized();
149
+ // Normalize URL (remove trailing slash)
150
+ const normalizedUrl = instanceUrl.replace(/\/+$/, '');
151
+ // Validate token by fetching user info
152
+ const user = await this.fetchGitLabUser(normalizedUrl, accessToken, options.rejectUnauthorizedSSL);
153
+ // Check if instance already exists for this user
154
+ const existingIndex = this.instances.findIndex(i => i.userId === userId && i.instanceUrl === normalizedUrl);
155
+ const instanceData = {
156
+ id: existingIndex >= 0 ? this.instances[existingIndex].id : uuidv4(),
157
+ name: options.name || this.getInstanceName(normalizedUrl),
158
+ instanceUrl: normalizedUrl,
159
+ userId,
160
+ gitlabUserId: user.id,
161
+ gitlabUsername: user.username,
162
+ accessToken: this.encrypt(accessToken),
163
+ tokenScopes: await this.getTokenScopes(normalizedUrl, accessToken, options.rejectUnauthorizedSSL),
164
+ rejectUnauthorizedSSL: options.rejectUnauthorizedSSL ?? true,
165
+ createdAt: existingIndex >= 0 ? this.instances[existingIndex].createdAt : new Date(),
166
+ lastUsedAt: new Date()
167
+ };
168
+ if (existingIndex >= 0) {
169
+ this.instances[existingIndex] = instanceData;
170
+ Logger.info(`Updated GitLab instance: ${instanceData.name} for user ${userId}`);
171
+ }
172
+ else {
173
+ this.instances.push(instanceData);
174
+ Logger.info(`Added GitLab instance: ${instanceData.name} for user ${userId}`);
175
+ }
176
+ await this.saveInstances();
177
+ return instanceData;
178
+ }
179
+ /**
180
+ * Get instance display name from URL
181
+ */
182
+ getInstanceName(url) {
183
+ try {
184
+ const hostname = new URL(url).hostname;
185
+ if (hostname === 'gitlab.com')
186
+ return 'GitLab.com';
187
+ return hostname;
188
+ }
189
+ catch {
190
+ return url;
191
+ }
192
+ }
193
+ /**
194
+ * Get all GitLab instances for a user
195
+ */
196
+ async getInstances(userId) {
197
+ await this.ensureInitialized();
198
+ return this.instances
199
+ .filter(i => i.userId === userId)
200
+ .map(i => ({ ...i, accessToken: '[ENCRYPTED]' })); // Don't expose tokens
201
+ }
202
+ /**
203
+ * Get a specific instance by ID
204
+ */
205
+ async getInstance(instanceId) {
206
+ await this.ensureInitialized();
207
+ return this.instances.find(i => i.id === instanceId) || null;
208
+ }
209
+ /**
210
+ * Remove a GitLab instance connection
211
+ */
212
+ async removeInstance(userId, instanceId) {
213
+ await this.ensureInitialized();
214
+ const index = this.instances.findIndex(i => i.id === instanceId && i.userId === userId);
215
+ if (index === -1)
216
+ return false;
217
+ // Also remove all connected repositories for this instance
218
+ this.repositories = this.repositories.filter(r => r.instanceId !== instanceId);
219
+ this.instances.splice(index, 1);
220
+ await this.saveInstances();
221
+ await this.saveRepositories();
222
+ Logger.info(`Removed GitLab instance: ${instanceId}`);
223
+ return true;
224
+ }
225
+ // ===== GITLAB API HELPERS =====
226
+ /**
227
+ * Make authenticated request to GitLab API
228
+ */
229
+ async gitlabFetch(instanceUrl, accessToken, endpoint, options = {}) {
230
+ const url = `${instanceUrl}/api/v4${endpoint}`;
231
+ const fetchOptions = {
232
+ method: options.method || 'GET',
233
+ headers: {
234
+ 'PRIVATE-TOKEN': accessToken,
235
+ 'Content-Type': 'application/json',
236
+ 'Accept': 'application/json'
237
+ }
238
+ };
239
+ if (options.body) {
240
+ fetchOptions.body = JSON.stringify(options.body);
241
+ }
242
+ // Note: For self-signed certificates, Node.js fetch doesn't support
243
+ // rejectUnauthorized directly. Users may need to set NODE_TLS_REJECT_UNAUTHORIZED=0
244
+ // or use a custom agent in production.
245
+ const response = await fetch(url, fetchOptions);
246
+ if (!response.ok) {
247
+ const errorText = await response.text();
248
+ let errorMessage = `GitLab API error: ${response.status}`;
249
+ try {
250
+ const errorJson = JSON.parse(errorText);
251
+ errorMessage = errorJson.message || errorJson.error || errorMessage;
252
+ }
253
+ catch {
254
+ errorMessage = errorText || errorMessage;
255
+ }
256
+ throw new Error(errorMessage);
257
+ }
258
+ return response.json();
259
+ }
260
+ /**
261
+ * Fetch current GitLab user info
262
+ */
263
+ async fetchGitLabUser(instanceUrl, accessToken, rejectUnauthorizedSSL) {
264
+ return this.gitlabFetch(instanceUrl, accessToken, '/user', { rejectUnauthorizedSSL });
265
+ }
266
+ /**
267
+ * Get token scopes (by testing API endpoints)
268
+ */
269
+ async getTokenScopes(instanceUrl, accessToken, rejectUnauthorizedSSL) {
270
+ const scopes = [];
271
+ // Test read_user scope
272
+ try {
273
+ await this.gitlabFetch(instanceUrl, accessToken, '/user', { rejectUnauthorizedSSL });
274
+ scopes.push('read_user');
275
+ }
276
+ catch { /* no access */ }
277
+ // Test read_api/api scope (list projects)
278
+ try {
279
+ await this.gitlabFetch(instanceUrl, accessToken, '/projects?per_page=1', { rejectUnauthorizedSSL });
280
+ scopes.push('read_api');
281
+ }
282
+ catch { /* no access */ }
283
+ // Test read_repository scope
284
+ try {
285
+ const projects = await this.gitlabFetch(instanceUrl, accessToken, '/projects?per_page=1&membership=true', { rejectUnauthorizedSSL });
286
+ if (projects.length > 0) {
287
+ try {
288
+ await this.gitlabFetch(instanceUrl, accessToken, `/projects/${projects[0].id}/repository/branches?per_page=1`, { rejectUnauthorizedSSL });
289
+ scopes.push('read_repository');
290
+ }
291
+ catch { /* no access */ }
292
+ }
293
+ }
294
+ catch { /* no access */ }
295
+ // Test write_repository scope (we don't actually write, just assume if api scope)
296
+ if (scopes.includes('read_api')) {
297
+ scopes.push('api'); // Full API access likely
298
+ }
299
+ return scopes;
300
+ }
301
+ // ===== REPOSITORY MANAGEMENT =====
302
+ /**
303
+ * List all accessible projects from a GitLab instance
304
+ */
305
+ async listProjects(userId, instanceId, options = {}) {
306
+ await this.ensureInitialized();
307
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
308
+ if (!instance) {
309
+ throw new Error('GitLab instance not found');
310
+ }
311
+ const accessToken = this.decrypt(instance.accessToken);
312
+ const params = new URLSearchParams();
313
+ if (options.search)
314
+ params.set('search', options.search);
315
+ if (options.membership !== false)
316
+ params.set('membership', 'true');
317
+ if (options.owned)
318
+ params.set('owned', 'true');
319
+ params.set('order_by', 'last_activity_at');
320
+ params.set('sort', 'desc');
321
+ params.set('per_page', String(options.perPage || 50));
322
+ params.set('page', String(options.page || 1));
323
+ const endpoint = `/projects?${params.toString()}`;
324
+ return this.gitlabFetch(instance.instanceUrl, accessToken, endpoint, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
325
+ }
326
+ /**
327
+ * Get a specific project by ID
328
+ */
329
+ async getProject(userId, instanceId, projectId) {
330
+ await this.ensureInitialized();
331
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
332
+ if (!instance) {
333
+ throw new Error('GitLab instance not found');
334
+ }
335
+ const accessToken = this.decrypt(instance.accessToken);
336
+ const encodedId = encodeURIComponent(String(projectId));
337
+ return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
338
+ }
339
+ /**
340
+ * List branches for a project
341
+ */
342
+ async listBranches(userId, instanceId, projectId) {
343
+ await this.ensureInitialized();
344
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
345
+ if (!instance) {
346
+ throw new Error('GitLab instance not found');
347
+ }
348
+ const accessToken = this.decrypt(instance.accessToken);
349
+ const encodedId = encodeURIComponent(String(projectId));
350
+ return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/repository/branches?per_page=100`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
351
+ }
352
+ /**
353
+ * Connect a GitLab repository to ArchiCore
354
+ */
355
+ async connectRepository(userId, instanceId, projectId, options = {}) {
356
+ await this.ensureInitialized();
357
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
358
+ if (!instance) {
359
+ throw new Error('GitLab instance not found');
360
+ }
361
+ // Check if already connected
362
+ const existing = this.repositories.find(r => r.instanceId === instanceId && r.gitlabProjectId === projectId);
363
+ if (existing && !options.forceReconnect) {
364
+ return existing;
365
+ }
366
+ // Fetch project info
367
+ const project = await this.getProject(userId, instanceId, projectId);
368
+ const repoData = {
369
+ id: existing?.id || uuidv4(),
370
+ instanceId,
371
+ gitlabProjectId: project.id,
372
+ fullPath: project.path_with_namespace,
373
+ name: project.name,
374
+ owner: project.namespace.path,
375
+ visibility: project.visibility,
376
+ defaultBranch: project.default_branch,
377
+ autoAnalyze: options.autoAnalyze ?? true,
378
+ analyzeMRs: options.analyzeMRs ?? true,
379
+ status: 'pending',
380
+ createdAt: existing?.createdAt || new Date(),
381
+ updatedAt: new Date()
382
+ };
383
+ // Remove existing if force reconnect
384
+ if (existing) {
385
+ const index = this.repositories.findIndex(r => r.id === existing.id);
386
+ if (index >= 0) {
387
+ this.repositories.splice(index, 1);
388
+ }
389
+ }
390
+ this.repositories.push(repoData);
391
+ await this.saveRepositories();
392
+ Logger.info(`Connected GitLab repository: ${project.path_with_namespace}`);
393
+ return repoData;
394
+ }
395
+ /**
396
+ * Get all connected repositories for a user
397
+ */
398
+ async getConnectedRepositories(userId) {
399
+ await this.ensureInitialized();
400
+ // Get user's instances
401
+ const userInstanceIds = this.instances
402
+ .filter(i => i.userId === userId)
403
+ .map(i => i.id);
404
+ return this.repositories.filter(r => userInstanceIds.includes(r.instanceId));
405
+ }
406
+ /**
407
+ * Get a specific connected repository
408
+ */
409
+ async getConnectedRepository(repoId) {
410
+ await this.ensureInitialized();
411
+ return this.repositories.find(r => r.id === repoId) || null;
412
+ }
413
+ /**
414
+ * Disconnect a repository
415
+ */
416
+ async disconnectRepository(userId, repoId) {
417
+ await this.ensureInitialized();
418
+ const repo = this.repositories.find(r => r.id === repoId);
419
+ if (!repo)
420
+ return false;
421
+ // Verify ownership
422
+ const instance = this.instances.find(i => i.id === repo.instanceId);
423
+ if (!instance || instance.userId !== userId)
424
+ return false;
425
+ // Remove webhook if exists
426
+ if (repo.webhookId) {
427
+ try {
428
+ await this.deleteWebhook(userId, repo.instanceId, repo.gitlabProjectId, repo.webhookId);
429
+ }
430
+ catch (error) {
431
+ Logger.warn(`Failed to delete webhook for ${repo.fullPath}:`, error);
432
+ }
433
+ }
434
+ const index = this.repositories.findIndex(r => r.id === repoId);
435
+ if (index >= 0) {
436
+ this.repositories.splice(index, 1);
437
+ await this.saveRepositories();
438
+ Logger.info(`Disconnected GitLab repository: ${repo.fullPath}`);
439
+ return true;
440
+ }
441
+ return false;
442
+ }
443
+ /**
444
+ * Update repository's ArchiCore project ID
445
+ */
446
+ async updateRepositoryProjectId(repoId, projectId) {
447
+ await this.ensureInitialized();
448
+ const repo = this.repositories.find(r => r.id === repoId);
449
+ if (repo) {
450
+ repo.projectId = projectId;
451
+ repo.updatedAt = new Date();
452
+ await this.saveRepositories();
453
+ }
454
+ }
455
+ /**
456
+ * Update repository status
457
+ */
458
+ async updateRepositoryStatus(repoId, status, error) {
459
+ await this.ensureInitialized();
460
+ const repo = this.repositories.find(r => r.id === repoId);
461
+ if (repo) {
462
+ repo.status = status;
463
+ repo.lastError = error;
464
+ repo.updatedAt = new Date();
465
+ await this.saveRepositories();
466
+ }
467
+ }
468
+ /**
469
+ * Update last analyzed timestamp
470
+ */
471
+ async updateLastAnalyzed(repoId) {
472
+ await this.ensureInitialized();
473
+ const repo = this.repositories.find(r => r.id === repoId);
474
+ if (repo) {
475
+ repo.lastAnalyzedAt = new Date();
476
+ repo.updatedAt = new Date();
477
+ await this.saveRepositories();
478
+ }
479
+ }
480
+ // ===== REPOSITORY CONTENT =====
481
+ /**
482
+ * Download repository archive
483
+ */
484
+ async downloadRepository(userId, instanceId, projectId, ref) {
485
+ await this.ensureInitialized();
486
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
487
+ if (!instance) {
488
+ throw new Error('GitLab instance not found');
489
+ }
490
+ const accessToken = this.decrypt(instance.accessToken);
491
+ const encodedId = encodeURIComponent(String(projectId));
492
+ const sha = ref || 'HEAD';
493
+ const url = `${instance.instanceUrl}/api/v4/projects/${encodedId}/repository/archive.zip?sha=${encodeURIComponent(sha)}`;
494
+ const response = await fetch(url, {
495
+ headers: {
496
+ 'PRIVATE-TOKEN': accessToken
497
+ }
498
+ });
499
+ if (!response.ok) {
500
+ throw new Error(`Failed to download repository: ${response.status}`);
501
+ }
502
+ const arrayBuffer = await response.arrayBuffer();
503
+ return Buffer.from(arrayBuffer);
504
+ }
505
+ /**
506
+ * Get file content from repository
507
+ */
508
+ async getFileContent(userId, instanceId, projectId, filePath, ref) {
509
+ await this.ensureInitialized();
510
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
511
+ if (!instance) {
512
+ throw new Error('GitLab instance not found');
513
+ }
514
+ const accessToken = this.decrypt(instance.accessToken);
515
+ const encodedId = encodeURIComponent(String(projectId));
516
+ const encodedPath = encodeURIComponent(filePath);
517
+ const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
518
+ const file = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/repository/files/${encodedPath}${refParam}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
519
+ if (file.encoding === 'base64') {
520
+ return Buffer.from(file.content, 'base64').toString('utf-8');
521
+ }
522
+ return file.content;
523
+ }
524
+ // ===== MERGE REQUESTS =====
525
+ /**
526
+ * Get merge request details
527
+ */
528
+ async getMergeRequest(userId, instanceId, projectId, mrIid) {
529
+ await this.ensureInitialized();
530
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
531
+ if (!instance) {
532
+ throw new Error('GitLab instance not found');
533
+ }
534
+ const accessToken = this.decrypt(instance.accessToken);
535
+ const encodedId = encodeURIComponent(String(projectId));
536
+ return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
537
+ }
538
+ /**
539
+ * Get merge request changes (diff)
540
+ */
541
+ async getMergeRequestChanges(userId, instanceId, projectId, mrIid) {
542
+ await this.ensureInitialized();
543
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
544
+ if (!instance) {
545
+ throw new Error('GitLab instance not found');
546
+ }
547
+ const accessToken = this.decrypt(instance.accessToken);
548
+ const encodedId = encodeURIComponent(String(projectId));
549
+ const response = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}/changes`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
550
+ return response.changes;
551
+ }
552
+ /**
553
+ * Post a comment on merge request
554
+ */
555
+ async postMRComment(userId, instanceId, projectId, mrIid, body) {
556
+ await this.ensureInitialized();
557
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
558
+ if (!instance) {
559
+ throw new Error('GitLab instance not found');
560
+ }
561
+ const accessToken = this.decrypt(instance.accessToken);
562
+ const encodedId = encodeURIComponent(String(projectId));
563
+ return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}/notes`, {
564
+ method: 'POST',
565
+ body: { body },
566
+ rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL
567
+ });
568
+ }
569
+ // ===== WEBHOOKS =====
570
+ /**
571
+ * Create webhook for repository
572
+ */
573
+ async createWebhook(userId, instanceId, projectId) {
574
+ await this.ensureInitialized();
575
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
576
+ if (!instance) {
577
+ throw new Error('GitLab instance not found');
578
+ }
579
+ const accessToken = this.decrypt(instance.accessToken);
580
+ const secret = randomBytes(32).toString('hex');
581
+ const encodedId = encodeURIComponent(String(projectId));
582
+ const webhook = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/hooks`, {
583
+ method: 'POST',
584
+ body: {
585
+ url: WEBHOOK_BASE_URL,
586
+ token: secret,
587
+ push_events: true,
588
+ merge_requests_events: true,
589
+ enable_ssl_verification: instance.rejectUnauthorizedSSL
590
+ },
591
+ rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL
592
+ });
593
+ return { id: webhook.id, secret };
594
+ }
595
+ /**
596
+ * Delete webhook
597
+ */
598
+ async deleteWebhook(userId, instanceId, projectId, hookId) {
599
+ await this.ensureInitialized();
600
+ const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
601
+ if (!instance) {
602
+ throw new Error('GitLab instance not found');
603
+ }
604
+ const accessToken = this.decrypt(instance.accessToken);
605
+ const encodedId = encodeURIComponent(String(projectId));
606
+ await fetch(`${instance.instanceUrl}/api/v4/projects/${encodedId}/hooks/${hookId}`, {
607
+ method: 'DELETE',
608
+ headers: { 'PRIVATE-TOKEN': accessToken }
609
+ });
610
+ }
611
+ /**
612
+ * Verify webhook token
613
+ */
614
+ verifyWebhookToken(token, expectedToken) {
615
+ return token === expectedToken;
616
+ }
617
+ /**
618
+ * Find repository by webhook (for incoming webhooks)
619
+ */
620
+ async findRepositoryByWebhook(fullPath) {
621
+ await this.ensureInitialized();
622
+ return this.repositories.find(r => r.fullPath === fullPath) || null;
623
+ }
624
+ /**
625
+ * Get instance for repository (used in webhook handling)
626
+ */
627
+ async getInstanceForRepository(repo) {
628
+ await this.ensureInitialized();
629
+ return this.instances.find(i => i.id === repo.instanceId) || null;
630
+ }
631
+ /**
632
+ * Get decrypted webhook secret
633
+ */
634
+ getWebhookSecret(encryptedSecret) {
635
+ return this.decrypt(encryptedSecret);
636
+ }
637
+ /**
638
+ * Disconnect repository by ArchiCore project ID
639
+ */
640
+ async disconnectRepositoryByProjectId(projectId) {
641
+ await this.ensureInitialized();
642
+ const repo = this.repositories.find(r => r.projectId === projectId);
643
+ if (!repo)
644
+ return false;
645
+ const instance = this.instances.find(i => i.id === repo.instanceId);
646
+ if (!instance)
647
+ return false;
648
+ return this.disconnectRepository(instance.userId, repo.id);
649
+ }
650
+ }
651
+ export default GitLabService;
652
+ //# sourceMappingURL=gitlab-service.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * GitLab Integration Module
3
+ *
4
+ * Exports GitLab service and types for integration with ArchiCore
5
+ */
6
+ export { GitLabService, default } from './gitlab-service.js';
7
+ export * from '../types/gitlab.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * GitLab Integration Module
3
+ *
4
+ * Exports GitLab service and types for integration with ArchiCore
5
+ */
6
+ export { GitLabService, default } from './gitlab-service.js';
7
+ export * from '../types/gitlab.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Passport OAuth Configuration
3
+ * Google and GitHub authentication strategies
4
+ */
5
+ import passport from 'passport';
6
+ export interface OAuthProfile {
7
+ id: string;
8
+ email: string;
9
+ displayName: string;
10
+ avatar?: string;
11
+ provider: 'google' | 'github';
12
+ }
13
+ export default passport;
14
+ //# sourceMappingURL=passport.d.ts.map