git-workspace-service 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,2425 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var child_process = require('child_process');
5
+ var util = require('util');
6
+ var fs3 = require('fs/promises');
7
+ var path = require('path');
8
+ var fs = require('fs');
9
+ var os = require('os');
10
+
11
+ function _interopNamespace(e) {
12
+ if (e && e.__esModule) return e;
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
30
+ var fs3__namespace = /*#__PURE__*/_interopNamespace(fs3);
31
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
32
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
33
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
34
+
35
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
36
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
37
+ }) : x)(function(x) {
38
+ if (typeof require !== "undefined") return require.apply(this, arguments);
39
+ throw Error('Dynamic require of "' + x + '" is not supported');
40
+ });
41
+
42
+ // src/utils/branch-naming.ts
43
+ var DEFAULT_BRANCH_PREFIX = "parallax";
44
+ var DEFAULT_OPTIONS = {
45
+ maxSlugLength: 30,
46
+ prefix: DEFAULT_BRANCH_PREFIX
47
+ };
48
+ function generateBranchName(config, options) {
49
+ const opts = { ...DEFAULT_OPTIONS, ...options };
50
+ const role = sanitizeForBranch(config.role);
51
+ const slug = config.slug ? sanitizeForBranch(config.slug, opts.maxSlugLength) : "";
52
+ const parts = [opts.prefix, config.executionId, role];
53
+ if (slug) {
54
+ parts[2] = `${role}-${slug}`;
55
+ }
56
+ return parts.join("/");
57
+ }
58
+ function parseBranchName(branchName, options) {
59
+ const opts = { ...DEFAULT_OPTIONS, ...options };
60
+ if (!branchName.startsWith(`${opts.prefix}/`)) {
61
+ return null;
62
+ }
63
+ const parts = branchName.split("/");
64
+ if (parts.length < 3) {
65
+ return null;
66
+ }
67
+ const [, executionId, roleAndSlug] = parts;
68
+ const dashIndex = roleAndSlug.indexOf("-");
69
+ if (dashIndex === -1) {
70
+ return { executionId, role: roleAndSlug };
71
+ }
72
+ return {
73
+ executionId,
74
+ role: roleAndSlug.substring(0, dashIndex),
75
+ slug: roleAndSlug.substring(dashIndex + 1)
76
+ };
77
+ }
78
+ function isManagedBranch(branchName, options) {
79
+ const opts = { ...DEFAULT_OPTIONS, ...options };
80
+ return branchName.startsWith(`${opts.prefix}/`);
81
+ }
82
+ function filterBranchesByExecution(branches, executionId, options) {
83
+ const opts = { ...DEFAULT_OPTIONS, ...options };
84
+ const prefix = `${opts.prefix}/${executionId}/`;
85
+ return branches.filter((b) => b.startsWith(prefix));
86
+ }
87
+ function createBranchInfo(config, options) {
88
+ return {
89
+ name: generateBranchName(config, options),
90
+ executionId: config.executionId,
91
+ baseBranch: config.baseBranch,
92
+ createdAt: /* @__PURE__ */ new Date()
93
+ };
94
+ }
95
+ function sanitizeForBranch(input, maxLength) {
96
+ let result = input.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
97
+ if (maxLength && result.length > maxLength) {
98
+ result = result.substring(0, maxLength).replace(/-$/, "");
99
+ }
100
+ return result;
101
+ }
102
+ function generateSlug(description, maxLength = 30) {
103
+ const words = description.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2).filter((w) => !STOP_WORDS.has(w)).slice(0, 4);
104
+ return sanitizeForBranch(words.join("-"), maxLength);
105
+ }
106
+ var STOP_WORDS = /* @__PURE__ */ new Set([
107
+ "the",
108
+ "and",
109
+ "for",
110
+ "with",
111
+ "this",
112
+ "that",
113
+ "from",
114
+ "have",
115
+ "has",
116
+ "will",
117
+ "would",
118
+ "could",
119
+ "should",
120
+ "been",
121
+ "being",
122
+ "into",
123
+ "than",
124
+ "then",
125
+ "when",
126
+ "where",
127
+ "which",
128
+ "while",
129
+ "about",
130
+ "after",
131
+ "before"
132
+ ]);
133
+ function createNodeCredentialHelperScript(contextFilePath) {
134
+ return `#!/usr/bin/env node
135
+ const fs = require('fs');
136
+ const readline = require('readline');
137
+
138
+ const contextFile = '${contextFilePath}';
139
+
140
+ async function main() {
141
+ // Parse input from git
142
+ const rl = readline.createInterface({ input: process.stdin });
143
+ const request = {};
144
+
145
+ for await (const line of rl) {
146
+ if (!line.trim()) break;
147
+ const [key, value] = line.split('=');
148
+ if (key && value !== undefined) {
149
+ request[key.trim()] = value.trim();
150
+ }
151
+ }
152
+
153
+ // Read context file
154
+ try {
155
+ const context = JSON.parse(fs.readFileSync(contextFile, 'utf8'));
156
+
157
+ // Check expiration
158
+ if (new Date(context.expiresAt) < new Date()) {
159
+ console.error('Credential expired');
160
+ process.exit(1);
161
+ }
162
+
163
+ // Output credentials
164
+ console.log('username=x-access-token');
165
+ console.log('password=' + context.token);
166
+ console.log('');
167
+ } catch (error) {
168
+ console.error('Failed to read credentials:', error.message);
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ main();
174
+ `;
175
+ }
176
+ function createShellCredentialHelperScript(contextFilePath) {
177
+ return `#!/bin/sh
178
+ # Git Credential Helper
179
+ # Reads credentials from: ${contextFilePath}
180
+
181
+ # Read the context file
182
+ if [ ! -f "${contextFilePath}" ]; then
183
+ echo "Credential context file not found" >&2
184
+ exit 1
185
+ fi
186
+
187
+ # Parse JSON and extract token (using basic shell tools)
188
+ TOKEN=$(cat "${contextFilePath}" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
189
+ EXPIRES=$(cat "${contextFilePath}" | grep -o '"expiresAt":"[^"]*"' | cut -d'"' -f4)
190
+
191
+ # Check if we got a token
192
+ if [ -z "$TOKEN" ]; then
193
+ echo "No token found in credential context" >&2
194
+ exit 1
195
+ fi
196
+
197
+ # Output credentials in git credential format
198
+ echo "username=x-access-token"
199
+ echo "password=$TOKEN"
200
+ echo ""
201
+ `;
202
+ }
203
+ async function configureCredentialHelper(workspacePath, context) {
204
+ const helperDir = path__namespace.join(workspacePath, ".git-workspace");
205
+ await fs__namespace.promises.mkdir(helperDir, { recursive: true });
206
+ const contextFilePath = path__namespace.join(helperDir, "credential-context.json");
207
+ await fs__namespace.promises.writeFile(
208
+ contextFilePath,
209
+ JSON.stringify(context, null, 2),
210
+ { mode: 384 }
211
+ // Read/write only for owner
212
+ );
213
+ const helperScriptPath = path__namespace.join(helperDir, "git-credential-helper");
214
+ const helperScript = createShellCredentialHelperScript(contextFilePath);
215
+ await fs__namespace.promises.writeFile(helperScriptPath, helperScript, { mode: 448 });
216
+ return helperScriptPath;
217
+ }
218
+ async function updateCredentials(workspacePath, newToken, newExpiresAt) {
219
+ const contextFilePath = path__namespace.join(workspacePath, ".git-workspace", "credential-context.json");
220
+ const existingContent = await fs__namespace.promises.readFile(contextFilePath, "utf8");
221
+ const context = JSON.parse(existingContent);
222
+ context.token = newToken;
223
+ context.expiresAt = newExpiresAt;
224
+ await fs__namespace.promises.writeFile(
225
+ contextFilePath,
226
+ JSON.stringify(context, null, 2),
227
+ { mode: 384 }
228
+ );
229
+ }
230
+ async function cleanupCredentialFiles(workspacePath) {
231
+ const helperDir = path__namespace.join(workspacePath, ".git-workspace");
232
+ try {
233
+ await fs__namespace.promises.rm(helperDir, { recursive: true, force: true });
234
+ } catch {
235
+ }
236
+ }
237
+ function getGitCredentialConfig(helperScriptPath) {
238
+ return [
239
+ // Clear any existing helpers
240
+ `git config credential.helper ''`,
241
+ // Use our custom helper script
242
+ `git config --add credential.helper '!${helperScriptPath}'`,
243
+ // Disable interactive prompts
244
+ `git config credential.interactive false`
245
+ ];
246
+ }
247
+ function outputCredentials(username, password) {
248
+ console.log(`username=${username}`);
249
+ console.log(`password=${password}`);
250
+ console.log("");
251
+ }
252
+
253
+ // src/workspace-service.ts
254
+ var execAsync = util.promisify(child_process.exec);
255
+ var WorkspaceService = class {
256
+ workspaces = /* @__PURE__ */ new Map();
257
+ baseDir;
258
+ branchPrefix;
259
+ credentialService;
260
+ logger;
261
+ eventHandlers = /* @__PURE__ */ new Set();
262
+ constructor(options) {
263
+ this.baseDir = options.config.baseDir;
264
+ this.branchPrefix = options.config.branchPrefix || "parallax";
265
+ this.credentialService = options.credentialService;
266
+ this.logger = options.logger;
267
+ }
268
+ /**
269
+ * Initialize the workspace service
270
+ */
271
+ async initialize() {
272
+ await fs3__namespace.mkdir(this.baseDir, { recursive: true });
273
+ this.log("info", { baseDir: this.baseDir }, "Workspace service initialized");
274
+ }
275
+ /**
276
+ * Register an event handler
277
+ */
278
+ onEvent(handler) {
279
+ this.eventHandlers.add(handler);
280
+ return () => this.eventHandlers.delete(handler);
281
+ }
282
+ /**
283
+ * Provision a new workspace for a task
284
+ */
285
+ async provision(config) {
286
+ const workspaceId = crypto.randomUUID();
287
+ this.log(
288
+ "info",
289
+ {
290
+ workspaceId,
291
+ repo: config.repo,
292
+ executionId: config.execution.id,
293
+ role: config.task.role
294
+ },
295
+ "Provisioning workspace"
296
+ );
297
+ await this.emitEvent({
298
+ type: "workspace:provisioning",
299
+ workspaceId,
300
+ executionId: config.execution.id,
301
+ timestamp: /* @__PURE__ */ new Date()
302
+ });
303
+ const workspacePath = path__namespace.join(this.baseDir, workspaceId);
304
+ await fs3__namespace.mkdir(workspacePath, { recursive: true });
305
+ const credential = await this.credentialService.getCredentials({
306
+ repo: config.repo,
307
+ access: "write",
308
+ context: {
309
+ executionId: config.execution.id,
310
+ taskId: config.task.id,
311
+ userId: config.user?.id,
312
+ reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
313
+ },
314
+ // Pass user-provided credentials if available
315
+ userProvided: config.userCredentials
316
+ });
317
+ await this.emitEvent({
318
+ type: "credential:granted",
319
+ workspaceId,
320
+ credentialId: credential.id,
321
+ executionId: config.execution.id,
322
+ timestamp: /* @__PURE__ */ new Date()
323
+ });
324
+ const branchInfo = createBranchInfo(
325
+ {
326
+ executionId: config.execution.id,
327
+ role: config.task.role,
328
+ slug: config.task.slug,
329
+ baseBranch: config.baseBranch
330
+ },
331
+ { prefix: this.branchPrefix }
332
+ );
333
+ const workspace = {
334
+ id: workspaceId,
335
+ path: workspacePath,
336
+ repo: config.repo,
337
+ branch: branchInfo,
338
+ credential,
339
+ provisionedAt: /* @__PURE__ */ new Date(),
340
+ status: "provisioning"
341
+ };
342
+ this.workspaces.set(workspaceId, workspace);
343
+ try {
344
+ await this.cloneRepo(workspace, credential.token);
345
+ await this.createBranch(workspace);
346
+ await this.configureGit(workspace);
347
+ workspace.status = "ready";
348
+ this.workspaces.set(workspaceId, workspace);
349
+ this.log(
350
+ "info",
351
+ {
352
+ workspaceId,
353
+ path: workspacePath,
354
+ branch: branchInfo.name
355
+ },
356
+ "Workspace provisioned"
357
+ );
358
+ await this.emitEvent({
359
+ type: "workspace:ready",
360
+ workspaceId,
361
+ executionId: config.execution.id,
362
+ timestamp: /* @__PURE__ */ new Date()
363
+ });
364
+ return workspace;
365
+ } catch (error) {
366
+ workspace.status = "error";
367
+ this.workspaces.set(workspaceId, workspace);
368
+ const errorMessage = error instanceof Error ? error.message : String(error);
369
+ this.log("error", { workspaceId, error: errorMessage }, "Failed to provision workspace");
370
+ await this.emitEvent({
371
+ type: "workspace:error",
372
+ workspaceId,
373
+ executionId: config.execution.id,
374
+ timestamp: /* @__PURE__ */ new Date(),
375
+ error: errorMessage
376
+ });
377
+ throw error;
378
+ }
379
+ }
380
+ /**
381
+ * Finalize a workspace (push, create PR, cleanup)
382
+ */
383
+ async finalize(workspaceId, options) {
384
+ const workspace = this.workspaces.get(workspaceId);
385
+ if (!workspace) {
386
+ throw new Error(`Workspace not found: ${workspaceId}`);
387
+ }
388
+ workspace.status = "finalizing";
389
+ this.workspaces.set(workspaceId, workspace);
390
+ this.log(
391
+ "info",
392
+ {
393
+ workspaceId,
394
+ push: options.push,
395
+ createPr: options.createPr
396
+ },
397
+ "Finalizing workspace"
398
+ );
399
+ await this.emitEvent({
400
+ type: "workspace:finalizing",
401
+ workspaceId,
402
+ executionId: workspace.branch.executionId,
403
+ timestamp: /* @__PURE__ */ new Date()
404
+ });
405
+ let pr;
406
+ try {
407
+ if (options.push) {
408
+ await this.pushBranch(workspace);
409
+ }
410
+ if (options.createPr && options.pr) {
411
+ pr = await this.createPullRequest(workspace, options.pr);
412
+ workspace.branch.pullRequest = pr;
413
+ await this.emitEvent({
414
+ type: "pr:created",
415
+ workspaceId,
416
+ executionId: workspace.branch.executionId,
417
+ timestamp: /* @__PURE__ */ new Date(),
418
+ data: {
419
+ prNumber: pr.number,
420
+ prUrl: pr.url
421
+ }
422
+ });
423
+ }
424
+ if (options.cleanup) {
425
+ await this.cleanup(workspaceId);
426
+ } else {
427
+ workspace.status = "ready";
428
+ this.workspaces.set(workspaceId, workspace);
429
+ }
430
+ return pr;
431
+ } catch (error) {
432
+ const errorMessage = error instanceof Error ? error.message : String(error);
433
+ this.log("error", { workspaceId, error: errorMessage }, "Failed to finalize workspace");
434
+ throw error;
435
+ }
436
+ }
437
+ /**
438
+ * Get a workspace by ID
439
+ */
440
+ get(workspaceId) {
441
+ return this.workspaces.get(workspaceId) || null;
442
+ }
443
+ /**
444
+ * Get all workspaces for an execution
445
+ */
446
+ getForExecution(executionId) {
447
+ return Array.from(this.workspaces.values()).filter(
448
+ (w) => w.branch.executionId === executionId
449
+ );
450
+ }
451
+ /**
452
+ * Clean up a workspace
453
+ */
454
+ async cleanup(workspaceId) {
455
+ const workspace = this.workspaces.get(workspaceId);
456
+ if (!workspace) {
457
+ return;
458
+ }
459
+ this.log("info", { workspaceId }, "Cleaning up workspace");
460
+ try {
461
+ await cleanupCredentialFiles(workspace.path);
462
+ } catch (error) {
463
+ const errorMessage = error instanceof Error ? error.message : String(error);
464
+ this.log("warn", { workspaceId, error: errorMessage }, "Failed to clean up credential files");
465
+ }
466
+ await this.credentialService.revokeCredential(workspace.credential.id);
467
+ await this.emitEvent({
468
+ type: "credential:revoked",
469
+ workspaceId,
470
+ credentialId: workspace.credential.id,
471
+ executionId: workspace.branch.executionId,
472
+ timestamp: /* @__PURE__ */ new Date()
473
+ });
474
+ try {
475
+ await fs3__namespace.rm(workspace.path, { recursive: true, force: true });
476
+ } catch (error) {
477
+ const errorMessage = error instanceof Error ? error.message : String(error);
478
+ this.log("warn", { workspaceId, error: errorMessage }, "Failed to remove workspace directory");
479
+ }
480
+ workspace.status = "cleaned_up";
481
+ this.workspaces.set(workspaceId, workspace);
482
+ await this.emitEvent({
483
+ type: "workspace:cleaned_up",
484
+ workspaceId,
485
+ executionId: workspace.branch.executionId,
486
+ timestamp: /* @__PURE__ */ new Date()
487
+ });
488
+ }
489
+ /**
490
+ * Clean up all workspaces for an execution
491
+ */
492
+ async cleanupForExecution(executionId) {
493
+ const workspaces = this.getForExecution(executionId);
494
+ await Promise.all(workspaces.map((w) => this.cleanup(w.id)));
495
+ await this.credentialService.revokeForExecution(executionId);
496
+ }
497
+ // ─────────────────────────────────────────────────────────────
498
+ // Private Methods
499
+ // ─────────────────────────────────────────────────────────────
500
+ async cloneRepo(workspace, token) {
501
+ const cloneUrl = token ? this.buildAuthenticatedUrl(workspace.repo, token) : workspace.repo;
502
+ await this.execInDir(
503
+ workspace.path,
504
+ `git clone --depth 1 --branch ${workspace.branch.baseBranch} ${cloneUrl} .`
505
+ );
506
+ }
507
+ async createBranch(workspace) {
508
+ await this.execInDir(workspace.path, `git checkout -b ${workspace.branch.name}`);
509
+ }
510
+ async configureGit(workspace) {
511
+ await this.execInDir(workspace.path, 'git config user.name "Workspace Agent"');
512
+ await this.execInDir(workspace.path, 'git config user.email "agent@workspace.local"');
513
+ if (!workspace.credential.token) {
514
+ this.log(
515
+ "debug",
516
+ { workspaceId: workspace.id },
517
+ "Using SSH authentication, skipping credential helper"
518
+ );
519
+ return;
520
+ }
521
+ const credentialContext = {
522
+ workspaceId: workspace.id,
523
+ executionId: workspace.branch.executionId,
524
+ repo: workspace.repo,
525
+ token: workspace.credential.token,
526
+ expiresAt: workspace.credential.expiresAt.toISOString()
527
+ };
528
+ const helperScriptPath = await configureCredentialHelper(
529
+ workspace.path,
530
+ credentialContext
531
+ );
532
+ const configCommands = getGitCredentialConfig(helperScriptPath);
533
+ for (const cmd of configCommands) {
534
+ await this.execInDir(workspace.path, cmd);
535
+ }
536
+ this.log(
537
+ "debug",
538
+ { workspaceId: workspace.id, helperPath: helperScriptPath },
539
+ "Git credential helper configured"
540
+ );
541
+ }
542
+ async pushBranch(workspace) {
543
+ await this.execInDir(workspace.path, `git push -u origin ${workspace.branch.name}`);
544
+ }
545
+ async createPullRequest(workspace, config) {
546
+ const repoInfo = this.parseRepo(workspace.repo);
547
+ if (!repoInfo) {
548
+ throw new Error(`Invalid repository format: ${workspace.repo}`);
549
+ }
550
+ const provider = this.credentialService.getProvider(workspace.credential.provider);
551
+ if (!provider) {
552
+ throw new Error(`Provider not configured: ${workspace.credential.provider}`);
553
+ }
554
+ const pr = await provider.createPullRequest({
555
+ repo: workspace.repo,
556
+ sourceBranch: workspace.branch.name,
557
+ targetBranch: config.targetBranch,
558
+ title: config.title,
559
+ body: config.body,
560
+ draft: config.draft,
561
+ labels: config.labels,
562
+ reviewers: config.reviewers,
563
+ credential: workspace.credential
564
+ });
565
+ pr.executionId = workspace.branch.executionId;
566
+ this.log(
567
+ "info",
568
+ {
569
+ workspaceId: workspace.id,
570
+ prNumber: pr.number,
571
+ prUrl: pr.url
572
+ },
573
+ "Pull request created"
574
+ );
575
+ return pr;
576
+ }
577
+ parseRepo(repo) {
578
+ const patterns = [/github\.com[/:]([^/]+)\/([^/.]+)/, /^([^/]+)\/([^/]+)$/];
579
+ for (const pattern of patterns) {
580
+ const match = repo.match(pattern);
581
+ if (match) {
582
+ return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+ buildAuthenticatedUrl(repo, token) {
588
+ let url = repo;
589
+ if (url.startsWith("git@github.com:")) {
590
+ url = url.replace("git@github.com:", "https://github.com/");
591
+ }
592
+ if (!url.endsWith(".git")) {
593
+ url = `${url}.git`;
594
+ }
595
+ if (!url.startsWith("https://")) {
596
+ url = `https://${url}`;
597
+ }
598
+ url = url.replace("https://", `https://x-access-token:${token}@`);
599
+ return url;
600
+ }
601
+ async execInDir(dir, command) {
602
+ const safeCommand = command.replace(/x-access-token:[^@]+@/g, "x-access-token:***@");
603
+ this.log("debug", { dir, command: safeCommand }, "Executing git command");
604
+ const { stdout, stderr } = await execAsync(command, { cwd: dir });
605
+ if (stderr && !stderr.includes("Cloning into")) {
606
+ this.log("debug", { stderr: stderr.substring(0, 200) }, "Git stderr");
607
+ }
608
+ return stdout;
609
+ }
610
+ log(level, data, message) {
611
+ if (this.logger) {
612
+ this.logger[level](data, message);
613
+ }
614
+ }
615
+ async emitEvent(event) {
616
+ for (const handler of this.eventHandlers) {
617
+ try {
618
+ await handler(event);
619
+ } catch (error) {
620
+ const errorMessage = error instanceof Error ? error.message : String(error);
621
+ this.log("warn", { event: event.type, error: errorMessage }, "Event handler error");
622
+ }
623
+ }
624
+ }
625
+ };
626
+
627
+ // src/oauth/device-flow.ts
628
+ var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
629
+ var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
630
+ var ERROR_AUTHORIZATION_PENDING = "authorization_pending";
631
+ var ERROR_SLOW_DOWN = "slow_down";
632
+ var ERROR_EXPIRED_TOKEN = "expired_token";
633
+ var ERROR_ACCESS_DENIED = "access_denied";
634
+ var OAuthDeviceFlow = class {
635
+ clientId;
636
+ clientSecret;
637
+ provider;
638
+ permissions;
639
+ promptEmitter;
640
+ timeout;
641
+ logger;
642
+ constructor(config, logger) {
643
+ this.clientId = config.clientId;
644
+ this.clientSecret = config.clientSecret;
645
+ this.provider = config.provider || "github";
646
+ this.permissions = config.permissions || {
647
+ repositories: { type: "all" },
648
+ contents: "write",
649
+ pullRequests: "write",
650
+ issues: "write",
651
+ metadata: "read",
652
+ canDeleteBranch: true,
653
+ canForcePush: false,
654
+ canDeleteRepository: false,
655
+ canAdminister: false
656
+ };
657
+ this.promptEmitter = config.promptEmitter;
658
+ this.timeout = config.timeout || 900;
659
+ this.logger = logger;
660
+ }
661
+ /**
662
+ * Start the device code flow and wait for authorization
663
+ */
664
+ async authorize() {
665
+ this.log("info", {}, "Starting OAuth device code flow");
666
+ const deviceCode = await this.requestDeviceCode();
667
+ const prompt = {
668
+ provider: this.provider,
669
+ verificationUri: deviceCode.verificationUri,
670
+ userCode: deviceCode.userCode,
671
+ expiresIn: deviceCode.expiresIn,
672
+ requestedPermissions: this.permissions
673
+ };
674
+ if (this.promptEmitter) {
675
+ this.promptEmitter.onAuthRequired(prompt);
676
+ } else {
677
+ this.printAuthPrompt(prompt);
678
+ }
679
+ try {
680
+ const token = await this.pollForToken(deviceCode);
681
+ const result = {
682
+ success: true,
683
+ provider: this.provider
684
+ };
685
+ if (this.promptEmitter) {
686
+ this.promptEmitter.onAuthComplete(result);
687
+ }
688
+ this.log("info", {}, "OAuth authorization successful");
689
+ return token;
690
+ } catch (error) {
691
+ const result = {
692
+ success: false,
693
+ provider: this.provider,
694
+ error: error instanceof Error ? error.message : String(error)
695
+ };
696
+ if (this.promptEmitter) {
697
+ this.promptEmitter.onAuthComplete(result);
698
+ }
699
+ throw error;
700
+ }
701
+ }
702
+ /**
703
+ * Request a device code from GitHub
704
+ */
705
+ async requestDeviceCode() {
706
+ const scope = this.permissionsToScopes(this.permissions);
707
+ this.log("debug", { scope }, "Requesting device code");
708
+ const response = await fetch(GITHUB_DEVICE_CODE_URL, {
709
+ method: "POST",
710
+ headers: {
711
+ Accept: "application/json",
712
+ "Content-Type": "application/json"
713
+ },
714
+ body: JSON.stringify({
715
+ client_id: this.clientId,
716
+ scope
717
+ })
718
+ });
719
+ if (!response.ok) {
720
+ const text = await response.text();
721
+ throw new Error(`Failed to request device code: ${response.status} ${text}`);
722
+ }
723
+ const data = await response.json();
724
+ return {
725
+ deviceCode: data.device_code,
726
+ userCode: data.user_code,
727
+ verificationUri: data.verification_uri,
728
+ verificationUriComplete: data.verification_uri_complete,
729
+ expiresIn: data.expires_in,
730
+ interval: data.interval || 5
731
+ };
732
+ }
733
+ /**
734
+ * Poll for the access token until authorized or timeout
735
+ */
736
+ async pollForToken(deviceCode) {
737
+ const startTime = Date.now();
738
+ let interval = deviceCode.interval * 1e3;
739
+ const expiresAt = startTime + deviceCode.expiresIn * 1e3;
740
+ while (Date.now() < expiresAt) {
741
+ if (Date.now() - startTime > this.timeout * 1e3) {
742
+ throw new Error("OAuth flow timed out");
743
+ }
744
+ await this.sleep(interval);
745
+ const secondsRemaining = Math.ceil((expiresAt - Date.now()) / 1e3);
746
+ if (this.promptEmitter?.onAuthPending) {
747
+ this.promptEmitter.onAuthPending(secondsRemaining);
748
+ }
749
+ try {
750
+ const token = await this.exchangeDeviceCode(deviceCode.deviceCode);
751
+ return token;
752
+ } catch (error) {
753
+ if (error instanceof DeviceFlowError) {
754
+ switch (error.code) {
755
+ case ERROR_AUTHORIZATION_PENDING:
756
+ this.log("debug", {}, "Authorization pending, continuing to poll");
757
+ continue;
758
+ case ERROR_SLOW_DOWN:
759
+ interval += 5e3;
760
+ this.log("debug", { interval }, "Slowing down polling");
761
+ continue;
762
+ case ERROR_EXPIRED_TOKEN:
763
+ throw new Error("Device code expired. Please try again.");
764
+ case ERROR_ACCESS_DENIED:
765
+ throw new Error("User denied authorization");
766
+ default:
767
+ throw error;
768
+ }
769
+ }
770
+ throw error;
771
+ }
772
+ }
773
+ throw new Error("Device code expired");
774
+ }
775
+ /**
776
+ * Exchange device code for access token
777
+ */
778
+ async exchangeDeviceCode(deviceCode) {
779
+ const body = {
780
+ client_id: this.clientId,
781
+ device_code: deviceCode,
782
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
783
+ };
784
+ if (this.clientSecret) {
785
+ body.client_secret = this.clientSecret;
786
+ }
787
+ const response = await fetch(GITHUB_TOKEN_URL, {
788
+ method: "POST",
789
+ headers: {
790
+ Accept: "application/json",
791
+ "Content-Type": "application/json"
792
+ },
793
+ body: JSON.stringify(body)
794
+ });
795
+ const data = await response.json();
796
+ if (data.error) {
797
+ throw new DeviceFlowError(data.error, data.error_description);
798
+ }
799
+ const scopes = (data.scope || "").split(/[,\s]+/).filter(Boolean);
800
+ return {
801
+ accessToken: data.access_token,
802
+ tokenType: data.token_type || "bearer",
803
+ scopes,
804
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
805
+ refreshToken: data.refresh_token,
806
+ provider: this.provider,
807
+ permissions: this.permissions,
808
+ createdAt: /* @__PURE__ */ new Date()
809
+ };
810
+ }
811
+ /**
812
+ * Refresh an expired token
813
+ */
814
+ async refreshToken(refreshTokenValue) {
815
+ this.log("info", {}, "Refreshing OAuth token");
816
+ const body = {
817
+ client_id: this.clientId,
818
+ grant_type: "refresh_token",
819
+ refresh_token: refreshTokenValue
820
+ };
821
+ if (this.clientSecret) {
822
+ body.client_secret = this.clientSecret;
823
+ }
824
+ const response = await fetch(GITHUB_TOKEN_URL, {
825
+ method: "POST",
826
+ headers: {
827
+ Accept: "application/json",
828
+ "Content-Type": "application/json"
829
+ },
830
+ body: JSON.stringify(body)
831
+ });
832
+ if (!response.ok) {
833
+ const text = await response.text();
834
+ throw new Error(`Failed to refresh token: ${response.status} ${text}`);
835
+ }
836
+ const data = await response.json();
837
+ if (data.error) {
838
+ throw new Error(`Token refresh failed: ${data.error_description || data.error}`);
839
+ }
840
+ const scopes = (data.scope || "").split(/[,\s]+/).filter(Boolean);
841
+ return {
842
+ accessToken: data.access_token,
843
+ tokenType: data.token_type || "bearer",
844
+ scopes,
845
+ expiresAt: data.expires_in ? new Date(Date.now() + data.expires_in * 1e3) : void 0,
846
+ refreshToken: data.refresh_token || refreshTokenValue,
847
+ provider: this.provider,
848
+ permissions: this.permissions,
849
+ createdAt: /* @__PURE__ */ new Date()
850
+ };
851
+ }
852
+ /**
853
+ * Convert AgentPermissions to GitHub OAuth scopes
854
+ */
855
+ permissionsToScopes(permissions) {
856
+ const scopes = [];
857
+ if (permissions.contents === "write") {
858
+ scopes.push("repo");
859
+ } else if (permissions.contents === "read") {
860
+ scopes.push("public_repo");
861
+ }
862
+ if (permissions.pullRequests !== "none" && !scopes.includes("repo")) {
863
+ scopes.push("public_repo");
864
+ }
865
+ scopes.push("read:user");
866
+ return scopes.join(" ");
867
+ }
868
+ /**
869
+ * Print auth prompt to console (default behavior)
870
+ */
871
+ printAuthPrompt(prompt) {
872
+ console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
873
+ console.log("\u2502 GitHub Authorization Required \u2502");
874
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
875
+ console.log(`\u2502 1. Go to: ${prompt.verificationUri.padEnd(35)}\u2502`);
876
+ console.log(`\u2502 2. Enter code: ${prompt.userCode.padEnd(29)}\u2502`);
877
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
878
+ console.log(`\u2502 \u23F3 Waiting for authorization... \u2502`);
879
+ console.log(`\u2502 Code expires in ${prompt.expiresIn} seconds${" ".repeat(18)}\u2502`);
880
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
881
+ }
882
+ sleep(ms) {
883
+ return new Promise((resolve) => setTimeout(resolve, ms));
884
+ }
885
+ log(level, data, message) {
886
+ if (this.logger) {
887
+ this.logger[level](data, message);
888
+ }
889
+ }
890
+ };
891
+ var DeviceFlowError = class extends Error {
892
+ constructor(code, description) {
893
+ super(description || code);
894
+ this.code = code;
895
+ this.description = description;
896
+ this.name = "DeviceFlowError";
897
+ }
898
+ };
899
+ var TokenStore = class {
900
+ /**
901
+ * Check if a token is expired
902
+ */
903
+ isExpired(token) {
904
+ if (!token.expiresAt) {
905
+ return false;
906
+ }
907
+ const buffer = 5 * 60 * 1e3;
908
+ return Date.now() > token.expiresAt.getTime() - buffer;
909
+ }
910
+ /**
911
+ * Check if a token needs refresh (expired or close to expiry)
912
+ */
913
+ needsRefresh(token) {
914
+ if (!token.expiresAt) {
915
+ return false;
916
+ }
917
+ const buffer = 10 * 60 * 1e3;
918
+ return Date.now() > token.expiresAt.getTime() - buffer;
919
+ }
920
+ };
921
+ var MemoryTokenStore = class extends TokenStore {
922
+ tokens = /* @__PURE__ */ new Map();
923
+ async save(provider, token) {
924
+ this.tokens.set(provider, token);
925
+ }
926
+ async get(provider) {
927
+ return this.tokens.get(provider) || null;
928
+ }
929
+ async clear(provider) {
930
+ if (provider) {
931
+ this.tokens.delete(provider);
932
+ } else {
933
+ this.tokens.clear();
934
+ }
935
+ }
936
+ async list() {
937
+ return Array.from(this.tokens.keys());
938
+ }
939
+ };
940
+ var FileTokenStore = class extends TokenStore {
941
+ directory;
942
+ encryptionKey;
943
+ constructor(options = {}) {
944
+ super();
945
+ this.directory = options.directory || path__namespace.join(os__namespace.homedir(), ".parallax", "tokens");
946
+ if (options.encryptionKey) {
947
+ this.encryptionKey = crypto__namespace.createHash("sha256").update(options.encryptionKey).digest();
948
+ }
949
+ }
950
+ async save(provider, token) {
951
+ await this.ensureDirectory();
952
+ const filePath = this.getTokenPath(provider);
953
+ const serialized = JSON.stringify({
954
+ ...token,
955
+ expiresAt: token.expiresAt?.toISOString(),
956
+ createdAt: token.createdAt.toISOString()
957
+ });
958
+ const data = this.encryptionKey ? this.encrypt(serialized) : serialized;
959
+ await fs3__namespace.writeFile(filePath, data, "utf-8");
960
+ }
961
+ async get(provider) {
962
+ const filePath = this.getTokenPath(provider);
963
+ try {
964
+ const data = await fs3__namespace.readFile(filePath, "utf-8");
965
+ const serialized = this.encryptionKey ? this.decrypt(data) : data;
966
+ const parsed = JSON.parse(serialized);
967
+ return {
968
+ ...parsed,
969
+ expiresAt: parsed.expiresAt ? new Date(parsed.expiresAt) : void 0,
970
+ createdAt: new Date(parsed.createdAt)
971
+ };
972
+ } catch (error) {
973
+ if (error.code === "ENOENT") {
974
+ return null;
975
+ }
976
+ throw error;
977
+ }
978
+ }
979
+ async clear(provider) {
980
+ if (provider) {
981
+ const filePath = this.getTokenPath(provider);
982
+ try {
983
+ await fs3__namespace.unlink(filePath);
984
+ } catch (error) {
985
+ if (error.code !== "ENOENT") {
986
+ throw error;
987
+ }
988
+ }
989
+ } else {
990
+ try {
991
+ const files = await fs3__namespace.readdir(this.directory);
992
+ await Promise.all(
993
+ files.filter((f) => f.endsWith(".token")).map((f) => fs3__namespace.unlink(path__namespace.join(this.directory, f)))
994
+ );
995
+ } catch (error) {
996
+ if (error.code !== "ENOENT") {
997
+ throw error;
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ async list() {
1003
+ try {
1004
+ const files = await fs3__namespace.readdir(this.directory);
1005
+ return files.filter((f) => f.endsWith(".token")).map((f) => f.replace(".token", ""));
1006
+ } catch {
1007
+ return [];
1008
+ }
1009
+ }
1010
+ getTokenPath(provider) {
1011
+ return path__namespace.join(this.directory, `${provider}.token`);
1012
+ }
1013
+ async ensureDirectory() {
1014
+ await fs3__namespace.mkdir(this.directory, { recursive: true, mode: 448 });
1015
+ }
1016
+ encrypt(data) {
1017
+ if (!this.encryptionKey) {
1018
+ return data;
1019
+ }
1020
+ const iv = crypto__namespace.randomBytes(16);
1021
+ const cipher = crypto__namespace.createCipheriv("aes-256-cbc", this.encryptionKey, iv);
1022
+ let encrypted = cipher.update(data, "utf8", "hex");
1023
+ encrypted += cipher.final("hex");
1024
+ return iv.toString("hex") + ":" + encrypted;
1025
+ }
1026
+ decrypt(data) {
1027
+ if (!this.encryptionKey) {
1028
+ return data;
1029
+ }
1030
+ const [ivHex, encrypted] = data.split(":");
1031
+ const iv = Buffer.from(ivHex, "hex");
1032
+ const decipher = crypto__namespace.createDecipheriv("aes-256-cbc", this.encryptionKey, iv);
1033
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
1034
+ decrypted += decipher.final("utf8");
1035
+ return decrypted;
1036
+ }
1037
+ };
1038
+
1039
+ // src/credential-service.ts
1040
+ var CredentialService = class {
1041
+ grants = /* @__PURE__ */ new Map();
1042
+ defaultTtl;
1043
+ maxTtl;
1044
+ providers;
1045
+ grantStore;
1046
+ tokenStore;
1047
+ oauthConfig;
1048
+ logger;
1049
+ constructor(options = {}) {
1050
+ this.defaultTtl = options.defaultTtlSeconds || 3600;
1051
+ this.maxTtl = options.maxTtlSeconds || 3600;
1052
+ this.providers = options.providers || /* @__PURE__ */ new Map();
1053
+ this.grantStore = options.grantStore;
1054
+ this.tokenStore = options.tokenStore || new MemoryTokenStore();
1055
+ this.oauthConfig = options.oauth;
1056
+ this.logger = options.logger;
1057
+ }
1058
+ /**
1059
+ * Get the token store (for external access to cached tokens)
1060
+ */
1061
+ getTokenStore() {
1062
+ return this.tokenStore;
1063
+ }
1064
+ /**
1065
+ * Register a provider adapter
1066
+ */
1067
+ registerProvider(provider) {
1068
+ this.providers.set(provider.name, provider);
1069
+ this.log("info", { provider: provider.name }, "Provider registered");
1070
+ }
1071
+ /**
1072
+ * Get a provider adapter
1073
+ */
1074
+ getProvider(name) {
1075
+ return this.providers.get(name);
1076
+ }
1077
+ /**
1078
+ * Request credentials for a repository
1079
+ */
1080
+ async getCredentials(request) {
1081
+ const provider = this.detectProvider(request.repo);
1082
+ this.log(
1083
+ "info",
1084
+ {
1085
+ repo: request.repo,
1086
+ provider,
1087
+ access: request.access,
1088
+ executionId: request.context.executionId
1089
+ },
1090
+ "Credential request"
1091
+ );
1092
+ const ttlSeconds = Math.min(
1093
+ request.ttlSeconds || this.defaultTtl,
1094
+ this.maxTtl
1095
+ );
1096
+ let credential = null;
1097
+ if (request.userProvided) {
1098
+ credential = this.createUserProvidedCredential(request, ttlSeconds, provider);
1099
+ this.log(
1100
+ "info",
1101
+ { repo: request.repo, type: request.userProvided.type },
1102
+ "Using user-provided credentials"
1103
+ );
1104
+ }
1105
+ if (!credential) {
1106
+ credential = await this.getCachedOAuthCredential(provider, request, ttlSeconds);
1107
+ }
1108
+ if (!credential) {
1109
+ const providerAdapter = this.providers.get(provider);
1110
+ if (providerAdapter) {
1111
+ try {
1112
+ credential = await providerAdapter.getCredentials(request);
1113
+ } catch (error) {
1114
+ this.log(
1115
+ "warn",
1116
+ { repo: request.repo, provider, error },
1117
+ "Failed to get provider credentials"
1118
+ );
1119
+ }
1120
+ }
1121
+ }
1122
+ if (!credential && this.oauthConfig) {
1123
+ credential = await this.getOAuthCredentialViaDeviceFlow(provider, request, ttlSeconds);
1124
+ }
1125
+ if (!credential) {
1126
+ throw new Error(
1127
+ `No credentials available for repository: ${request.repo}. ` + (this.oauthConfig ? "OAuth device flow failed or was cancelled." : "Configure OAuth to enable interactive authentication.")
1128
+ );
1129
+ }
1130
+ const grant = {
1131
+ id: credential.id,
1132
+ type: credential.type,
1133
+ repo: request.repo,
1134
+ provider,
1135
+ grantedTo: {
1136
+ executionId: request.context.executionId,
1137
+ taskId: request.context.taskId,
1138
+ agentId: request.context.agentId
1139
+ },
1140
+ permissions: credential.permissions,
1141
+ createdAt: /* @__PURE__ */ new Date(),
1142
+ expiresAt: credential.expiresAt
1143
+ };
1144
+ this.grants.set(grant.id, grant);
1145
+ if (this.grantStore) {
1146
+ try {
1147
+ await this.grantStore.create({
1148
+ ...grant,
1149
+ reason: request.context.reason
1150
+ });
1151
+ } catch (error) {
1152
+ this.log(
1153
+ "warn",
1154
+ { grantId: grant.id, error },
1155
+ "Failed to persist credential grant"
1156
+ );
1157
+ }
1158
+ }
1159
+ this.log(
1160
+ "info",
1161
+ {
1162
+ grantId: grant.id,
1163
+ repo: request.repo,
1164
+ expiresAt: credential.expiresAt,
1165
+ persisted: !!this.grantStore
1166
+ },
1167
+ "Credential granted"
1168
+ );
1169
+ return credential;
1170
+ }
1171
+ /**
1172
+ * Revoke a credential grant
1173
+ */
1174
+ async revokeCredential(grantId) {
1175
+ const grant = this.grants.get(grantId);
1176
+ if (!grant) {
1177
+ if (this.grantStore) {
1178
+ try {
1179
+ await this.grantStore.revoke(grantId);
1180
+ } catch (error) {
1181
+ this.log("warn", { grantId, error }, "Failed to revoke credential in store");
1182
+ }
1183
+ }
1184
+ return;
1185
+ }
1186
+ grant.revokedAt = /* @__PURE__ */ new Date();
1187
+ this.grants.set(grantId, grant);
1188
+ const provider = this.providers.get(grant.provider);
1189
+ if (provider) {
1190
+ try {
1191
+ await provider.revokeCredential(grantId);
1192
+ } catch (error) {
1193
+ this.log("warn", { grantId, error }, "Failed to revoke credential via provider");
1194
+ }
1195
+ }
1196
+ if (this.grantStore) {
1197
+ try {
1198
+ await this.grantStore.revoke(grantId);
1199
+ } catch (error) {
1200
+ this.log("warn", { grantId, error }, "Failed to revoke credential in store");
1201
+ }
1202
+ }
1203
+ this.log("info", { grantId }, "Credential revoked");
1204
+ }
1205
+ /**
1206
+ * Revoke all credentials for an execution
1207
+ */
1208
+ async revokeForExecution(executionId) {
1209
+ let count = 0;
1210
+ for (const [id, grant] of this.grants) {
1211
+ if (grant.grantedTo.executionId === executionId && !grant.revokedAt) {
1212
+ grant.revokedAt = /* @__PURE__ */ new Date();
1213
+ this.grants.set(id, grant);
1214
+ count++;
1215
+ }
1216
+ }
1217
+ if (this.grantStore) {
1218
+ try {
1219
+ const storeCount = await this.grantStore.revokeForExecution(executionId);
1220
+ count = Math.max(count, storeCount);
1221
+ } catch (error) {
1222
+ this.log("warn", { executionId, error }, "Failed to revoke credentials in store");
1223
+ }
1224
+ }
1225
+ this.log("info", { executionId, revokedCount: count }, "Credentials revoked for execution");
1226
+ return count;
1227
+ }
1228
+ /**
1229
+ * Check if a credential is valid
1230
+ */
1231
+ isValid(grantId) {
1232
+ const grant = this.grants.get(grantId);
1233
+ if (!grant) return false;
1234
+ if (grant.revokedAt) return false;
1235
+ if (/* @__PURE__ */ new Date() > grant.expiresAt) return false;
1236
+ return true;
1237
+ }
1238
+ /**
1239
+ * Get grant info for audit
1240
+ */
1241
+ async getGrant(grantId) {
1242
+ const memoryGrant = this.grants.get(grantId);
1243
+ if (memoryGrant) {
1244
+ return memoryGrant;
1245
+ }
1246
+ if (this.grantStore) {
1247
+ try {
1248
+ return await this.grantStore.findById(grantId);
1249
+ } catch (error) {
1250
+ this.log("warn", { grantId, error }, "Failed to get grant from store");
1251
+ }
1252
+ }
1253
+ return null;
1254
+ }
1255
+ /**
1256
+ * List all grants for an execution
1257
+ */
1258
+ async getGrantsForExecution(executionId) {
1259
+ if (this.grantStore) {
1260
+ try {
1261
+ return await this.grantStore.findByExecutionId(executionId);
1262
+ } catch (error) {
1263
+ this.log("warn", { executionId, error }, "Failed to get grants from store");
1264
+ }
1265
+ }
1266
+ return Array.from(this.grants.values()).filter(
1267
+ (g) => g.grantedTo.executionId === executionId
1268
+ );
1269
+ }
1270
+ // ─────────────────────────────────────────────────────────────
1271
+ // Private Methods
1272
+ // ─────────────────────────────────────────────────────────────
1273
+ detectProvider(repo) {
1274
+ const lowerRepo = repo.toLowerCase();
1275
+ if (lowerRepo.includes("github.com") || lowerRepo.startsWith("github:")) {
1276
+ return "github";
1277
+ }
1278
+ if (lowerRepo.includes("gitlab.com") || lowerRepo.startsWith("gitlab:")) {
1279
+ return "gitlab";
1280
+ }
1281
+ if (lowerRepo.includes("bitbucket.org") || lowerRepo.startsWith("bitbucket:")) {
1282
+ return "bitbucket";
1283
+ }
1284
+ if (lowerRepo.includes("dev.azure.com") || lowerRepo.includes("visualstudio.com")) {
1285
+ return "azure_devops";
1286
+ }
1287
+ return "self_hosted";
1288
+ }
1289
+ /**
1290
+ * Create a GitCredential from user-provided credentials (PAT or OAuth token)
1291
+ */
1292
+ createUserProvidedCredential(request, ttlSeconds, provider) {
1293
+ const userCreds = request.userProvided;
1294
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
1295
+ let credentialType;
1296
+ let token;
1297
+ if (userCreds.type === "ssh") {
1298
+ credentialType = "ssh_key";
1299
+ token = "";
1300
+ } else {
1301
+ credentialType = userCreds.type === "pat" ? "pat" : "oauth";
1302
+ token = userCreds.token;
1303
+ }
1304
+ const permissions = request.access === "write" ? ["contents:read", "contents:write", "pull_requests:write"] : ["contents:read"];
1305
+ return {
1306
+ id: crypto.randomUUID(),
1307
+ type: credentialType,
1308
+ token,
1309
+ repo: request.repo,
1310
+ permissions,
1311
+ expiresAt,
1312
+ provider: userCreds.provider || provider
1313
+ };
1314
+ }
1315
+ /**
1316
+ * Check for a cached OAuth token and create credential from it
1317
+ */
1318
+ async getCachedOAuthCredential(provider, request, ttlSeconds) {
1319
+ try {
1320
+ const cachedToken = await this.tokenStore.get(provider);
1321
+ if (!cachedToken) {
1322
+ return null;
1323
+ }
1324
+ if (this.tokenStore.isExpired(cachedToken)) {
1325
+ if (cachedToken.refreshToken && this.oauthConfig) {
1326
+ const refreshedToken = await this.refreshOAuthToken(provider, cachedToken.refreshToken);
1327
+ if (refreshedToken) {
1328
+ return this.createOAuthCredential(refreshedToken, request, ttlSeconds);
1329
+ }
1330
+ }
1331
+ this.log("info", { provider }, "Cached OAuth token expired");
1332
+ return null;
1333
+ }
1334
+ if (this.tokenStore.needsRefresh(cachedToken) && cachedToken.refreshToken && this.oauthConfig) {
1335
+ const refreshedToken = await this.refreshOAuthToken(provider, cachedToken.refreshToken);
1336
+ if (refreshedToken) {
1337
+ return this.createOAuthCredential(refreshedToken, request, ttlSeconds);
1338
+ }
1339
+ }
1340
+ this.log("info", { provider }, "Using cached OAuth token");
1341
+ return this.createOAuthCredential(cachedToken, request, ttlSeconds);
1342
+ } catch (error) {
1343
+ this.log("warn", { provider, error }, "Failed to get cached OAuth token");
1344
+ return null;
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Initiate interactive OAuth device flow
1349
+ */
1350
+ async getOAuthCredentialViaDeviceFlow(provider, request, ttlSeconds) {
1351
+ if (!this.oauthConfig) {
1352
+ return null;
1353
+ }
1354
+ if (provider !== "github") {
1355
+ this.log("warn", { provider }, "OAuth device flow only supported for GitHub");
1356
+ return null;
1357
+ }
1358
+ this.log("info", { repo: request.repo }, "Starting OAuth device flow for authentication");
1359
+ try {
1360
+ const deviceFlow = new OAuthDeviceFlow({
1361
+ clientId: this.oauthConfig.clientId,
1362
+ clientSecret: this.oauthConfig.clientSecret,
1363
+ provider,
1364
+ permissions: this.oauthConfig.permissions,
1365
+ promptEmitter: this.oauthConfig.promptEmitter
1366
+ });
1367
+ const token = await deviceFlow.authorize();
1368
+ await this.tokenStore.save(provider, token);
1369
+ this.log("info", { provider }, "OAuth device flow completed successfully");
1370
+ return this.createOAuthCredential(token, request, ttlSeconds);
1371
+ } catch (error) {
1372
+ this.log("error", { provider, error }, "OAuth device flow failed");
1373
+ return null;
1374
+ }
1375
+ }
1376
+ /**
1377
+ * Refresh an OAuth token
1378
+ */
1379
+ async refreshOAuthToken(provider, refreshToken) {
1380
+ if (!this.oauthConfig) {
1381
+ return null;
1382
+ }
1383
+ try {
1384
+ const deviceFlow = new OAuthDeviceFlow({
1385
+ clientId: this.oauthConfig.clientId,
1386
+ clientSecret: this.oauthConfig.clientSecret,
1387
+ provider,
1388
+ permissions: this.oauthConfig.permissions
1389
+ });
1390
+ const newToken = await deviceFlow.refreshToken(refreshToken);
1391
+ await this.tokenStore.save(provider, newToken);
1392
+ this.log("info", { provider }, "OAuth token refreshed successfully");
1393
+ return newToken;
1394
+ } catch (error) {
1395
+ this.log("warn", { provider, error }, "Failed to refresh OAuth token");
1396
+ return null;
1397
+ }
1398
+ }
1399
+ /**
1400
+ * Create a GitCredential from an OAuthToken
1401
+ */
1402
+ createOAuthCredential(token, request, ttlSeconds) {
1403
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
1404
+ const permissions = [];
1405
+ if (token.permissions.contents === "read" || token.permissions.contents === "write") {
1406
+ permissions.push("contents:read");
1407
+ }
1408
+ if (token.permissions.contents === "write") {
1409
+ permissions.push("contents:write");
1410
+ }
1411
+ if (token.permissions.pullRequests === "read" || token.permissions.pullRequests === "write") {
1412
+ permissions.push("pull_requests:read");
1413
+ }
1414
+ if (token.permissions.pullRequests === "write") {
1415
+ permissions.push("pull_requests:write");
1416
+ }
1417
+ if (token.permissions.issues === "read" || token.permissions.issues === "write") {
1418
+ permissions.push("issues:read");
1419
+ }
1420
+ if (token.permissions.issues === "write") {
1421
+ permissions.push("issues:write");
1422
+ }
1423
+ return {
1424
+ id: crypto.randomUUID(),
1425
+ type: "oauth",
1426
+ token: token.accessToken,
1427
+ repo: request.repo,
1428
+ permissions,
1429
+ expiresAt,
1430
+ provider: token.provider
1431
+ };
1432
+ }
1433
+ log(level, data, message) {
1434
+ if (this.logger) {
1435
+ this.logger[level](data, message);
1436
+ }
1437
+ }
1438
+ };
1439
+ var octokitCache = null;
1440
+ function loadOctokit() {
1441
+ if (!octokitCache) {
1442
+ try {
1443
+ const { Octokit } = __require("@octokit/rest");
1444
+ const { createAppAuth } = __require("@octokit/auth-app");
1445
+ octokitCache = { Octokit, createAppAuth };
1446
+ } catch {
1447
+ throw new Error(
1448
+ "@octokit/rest and @octokit/auth-app are required for GitHub provider. Install them with: npm install @octokit/rest @octokit/auth-app"
1449
+ );
1450
+ }
1451
+ }
1452
+ return octokitCache;
1453
+ }
1454
+ var GitHubProvider = class {
1455
+ constructor(config, logger) {
1456
+ this.config = config;
1457
+ this.logger = logger;
1458
+ }
1459
+ name = "github";
1460
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1461
+ appOctokit;
1462
+ installations = /* @__PURE__ */ new Map();
1463
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1464
+ installationOctokits = /* @__PURE__ */ new Map();
1465
+ initialized = false;
1466
+ /**
1467
+ * Initialize provider - fetch all installations
1468
+ */
1469
+ async initialize() {
1470
+ if (this.initialized) {
1471
+ return;
1472
+ }
1473
+ const { Octokit, createAppAuth } = loadOctokit();
1474
+ this.log("info", {}, "Initializing GitHub provider");
1475
+ this.appOctokit = new Octokit({
1476
+ authStrategy: createAppAuth,
1477
+ auth: {
1478
+ appId: this.config.appId,
1479
+ privateKey: this.config.privateKey
1480
+ },
1481
+ baseUrl: this.config.baseUrl
1482
+ });
1483
+ try {
1484
+ const { data: installations } = await this.appOctokit.apps.listInstallations();
1485
+ for (const installation of installations) {
1486
+ await this.registerInstallation(installation.id);
1487
+ }
1488
+ this.initialized = true;
1489
+ this.log(
1490
+ "info",
1491
+ { installationCount: installations.length },
1492
+ "GitHub provider initialized"
1493
+ );
1494
+ } catch (error) {
1495
+ this.log("error", { error }, "Failed to initialize GitHub provider");
1496
+ throw error;
1497
+ }
1498
+ }
1499
+ /**
1500
+ * Register an installation (called on webhook or init)
1501
+ */
1502
+ async registerInstallation(installationId) {
1503
+ if (!this.appOctokit) {
1504
+ await this.initialize();
1505
+ }
1506
+ try {
1507
+ const { data: installation } = await this.appOctokit.apps.getInstallation({
1508
+ installation_id: installationId
1509
+ });
1510
+ const octokit = await this.getInstallationOctokit(installationId);
1511
+ const { data: repos } = await octokit.apps.listReposAccessibleToInstallation({
1512
+ per_page: 100
1513
+ });
1514
+ const account = installation.account;
1515
+ const accountLogin = account?.login || account?.slug || "unknown";
1516
+ const accountType = account?.type === "Organization" ? "Organization" : "User";
1517
+ const appInstallation = {
1518
+ installationId,
1519
+ accountLogin,
1520
+ accountType,
1521
+ repositories: repos.repositories?.map((r) => r.full_name) || [],
1522
+ permissions: installation.permissions
1523
+ };
1524
+ this.installations.set(appInstallation.accountLogin, appInstallation);
1525
+ this.log(
1526
+ "info",
1527
+ {
1528
+ installationId,
1529
+ account: appInstallation.accountLogin,
1530
+ repoCount: appInstallation.repositories.length
1531
+ },
1532
+ "Installation registered"
1533
+ );
1534
+ return appInstallation;
1535
+ } catch (error) {
1536
+ this.log("error", { installationId, error }, "Failed to register installation");
1537
+ throw error;
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Get installation for a repository
1542
+ */
1543
+ getInstallationForRepo(owner, repo) {
1544
+ const installation = this.installations.get(owner);
1545
+ if (!installation) {
1546
+ return null;
1547
+ }
1548
+ const fullName = `${owner}/${repo}`;
1549
+ if (!installation.repositories.includes(fullName)) {
1550
+ return null;
1551
+ }
1552
+ return installation;
1553
+ }
1554
+ /**
1555
+ * Get credentials for a repository (implements GitProviderAdapter)
1556
+ */
1557
+ async getCredentials(request) {
1558
+ const repoInfo = this.parseRepo(request.repo);
1559
+ if (!repoInfo) {
1560
+ throw new Error(`Invalid repository format: ${request.repo}`);
1561
+ }
1562
+ return this.getCredentialsForRepo(
1563
+ repoInfo.owner,
1564
+ repoInfo.repo,
1565
+ request.access,
1566
+ request.ttlSeconds
1567
+ );
1568
+ }
1569
+ /**
1570
+ * Get credentials for a repository by owner/repo
1571
+ */
1572
+ async getCredentialsForRepo(owner, repo, access, ttlSeconds = 3600) {
1573
+ if (!this.initialized) {
1574
+ await this.initialize();
1575
+ }
1576
+ const installation = this.getInstallationForRepo(owner, repo);
1577
+ if (!installation) {
1578
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1579
+ }
1580
+ const { createAppAuth } = loadOctokit();
1581
+ const auth = createAppAuth({
1582
+ appId: this.config.appId,
1583
+ privateKey: this.config.privateKey
1584
+ });
1585
+ const { token, expiresAt } = await auth({
1586
+ type: "installation",
1587
+ installationId: installation.installationId,
1588
+ repositoryNames: [repo],
1589
+ permissions: access === "write" ? { contents: "write", pull_requests: "write", metadata: "read" } : { contents: "read", metadata: "read" }
1590
+ });
1591
+ return {
1592
+ id: crypto.randomUUID(),
1593
+ type: "github_app",
1594
+ token,
1595
+ repo: `${owner}/${repo}`,
1596
+ permissions: access === "write" ? ["contents:write", "pull_requests:write", "metadata:read"] : ["contents:read", "metadata:read"],
1597
+ expiresAt: expiresAt ? new Date(expiresAt) : new Date(Date.now() + ttlSeconds * 1e3),
1598
+ provider: "github"
1599
+ };
1600
+ }
1601
+ /**
1602
+ * Revoke a credential (no-op for GitHub App tokens - they expire automatically)
1603
+ */
1604
+ async revokeCredential(_credentialId) {
1605
+ }
1606
+ /**
1607
+ * Create a pull request (implements GitProviderAdapter)
1608
+ */
1609
+ async createPullRequest(options) {
1610
+ const repoInfo = this.parseRepo(options.repo);
1611
+ if (!repoInfo) {
1612
+ throw new Error(`Invalid repository format: ${options.repo}`);
1613
+ }
1614
+ return this.createPullRequestForRepo(repoInfo.owner, repoInfo.repo, {
1615
+ title: options.title,
1616
+ body: options.body,
1617
+ head: options.sourceBranch,
1618
+ base: options.targetBranch,
1619
+ draft: options.draft,
1620
+ labels: options.labels,
1621
+ reviewers: options.reviewers
1622
+ });
1623
+ }
1624
+ /**
1625
+ * Create a pull request by owner/repo
1626
+ */
1627
+ async createPullRequestForRepo(owner, repo, options) {
1628
+ if (!this.initialized) {
1629
+ await this.initialize();
1630
+ }
1631
+ const installation = this.getInstallationForRepo(owner, repo);
1632
+ if (!installation) {
1633
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1634
+ }
1635
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1636
+ const { data: pr } = await octokit.pulls.create({
1637
+ owner,
1638
+ repo,
1639
+ title: options.title,
1640
+ body: options.body,
1641
+ head: options.head,
1642
+ base: options.base,
1643
+ draft: options.draft
1644
+ });
1645
+ if (options.labels && options.labels.length > 0) {
1646
+ await octokit.issues.addLabels({
1647
+ owner,
1648
+ repo,
1649
+ issue_number: pr.number,
1650
+ labels: options.labels
1651
+ });
1652
+ }
1653
+ if (options.reviewers && options.reviewers.length > 0) {
1654
+ await octokit.pulls.requestReviewers({
1655
+ owner,
1656
+ repo,
1657
+ pull_number: pr.number,
1658
+ reviewers: options.reviewers
1659
+ });
1660
+ }
1661
+ this.log(
1662
+ "info",
1663
+ { owner, repo, prNumber: pr.number, title: options.title },
1664
+ "Pull request created"
1665
+ );
1666
+ return {
1667
+ number: pr.number,
1668
+ url: pr.html_url,
1669
+ state: pr.state,
1670
+ sourceBranch: options.head,
1671
+ targetBranch: options.base,
1672
+ title: options.title,
1673
+ executionId: "",
1674
+ // Set by caller
1675
+ createdAt: new Date(pr.created_at)
1676
+ };
1677
+ }
1678
+ /**
1679
+ * Check if a branch exists (implements GitProviderAdapter)
1680
+ */
1681
+ async branchExists(repo, branch, _credential) {
1682
+ const repoInfo = this.parseRepo(repo);
1683
+ if (!repoInfo) {
1684
+ return false;
1685
+ }
1686
+ if (!this.initialized) {
1687
+ await this.initialize();
1688
+ }
1689
+ const installation = this.getInstallationForRepo(repoInfo.owner, repoInfo.repo);
1690
+ if (!installation) {
1691
+ return false;
1692
+ }
1693
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1694
+ try {
1695
+ await octokit.repos.getBranch({
1696
+ owner: repoInfo.owner,
1697
+ repo: repoInfo.repo,
1698
+ branch
1699
+ });
1700
+ return true;
1701
+ } catch {
1702
+ return false;
1703
+ }
1704
+ }
1705
+ /**
1706
+ * Get the default branch for a repository (implements GitProviderAdapter)
1707
+ */
1708
+ async getDefaultBranch(repo, _credential) {
1709
+ const repoInfo = this.parseRepo(repo);
1710
+ if (!repoInfo) {
1711
+ throw new Error(`Invalid repository format: ${repo}`);
1712
+ }
1713
+ if (!this.initialized) {
1714
+ await this.initialize();
1715
+ }
1716
+ const installation = this.getInstallationForRepo(repoInfo.owner, repoInfo.repo);
1717
+ if (!installation) {
1718
+ throw new Error(`No GitHub App installation found for ${repo}`);
1719
+ }
1720
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1721
+ const { data: repoData } = await octokit.repos.get({
1722
+ owner: repoInfo.owner,
1723
+ repo: repoInfo.repo
1724
+ });
1725
+ return repoData.default_branch;
1726
+ }
1727
+ /**
1728
+ * Get pull request status
1729
+ */
1730
+ async getPullRequest(owner, repo, prNumber) {
1731
+ if (!this.initialized) {
1732
+ await this.initialize();
1733
+ }
1734
+ const installation = this.getInstallationForRepo(owner, repo);
1735
+ if (!installation) {
1736
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1737
+ }
1738
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1739
+ const { data: pr } = await octokit.pulls.get({
1740
+ owner,
1741
+ repo,
1742
+ pull_number: prNumber
1743
+ });
1744
+ return {
1745
+ number: pr.number,
1746
+ url: pr.html_url,
1747
+ state: pr.merged ? "merged" : pr.state,
1748
+ sourceBranch: pr.head.ref,
1749
+ targetBranch: pr.base.ref,
1750
+ title: pr.title,
1751
+ executionId: "",
1752
+ // Would need to parse from branch name
1753
+ createdAt: new Date(pr.created_at),
1754
+ mergedAt: pr.merged_at ? new Date(pr.merged_at) : void 0
1755
+ };
1756
+ }
1757
+ /**
1758
+ * Delete a branch
1759
+ */
1760
+ async deleteBranch(owner, repo, branch) {
1761
+ if (!this.initialized) {
1762
+ await this.initialize();
1763
+ }
1764
+ const installation = this.getInstallationForRepo(owner, repo);
1765
+ if (!installation) {
1766
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1767
+ }
1768
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1769
+ await octokit.git.deleteRef({
1770
+ owner,
1771
+ repo,
1772
+ ref: `heads/${branch}`
1773
+ });
1774
+ this.log("info", { owner, repo, branch }, "Branch deleted");
1775
+ }
1776
+ /**
1777
+ * List all managed branches for a repo
1778
+ */
1779
+ async listManagedBranches(owner, repo, prefix = "parallax/") {
1780
+ if (!this.initialized) {
1781
+ await this.initialize();
1782
+ }
1783
+ const installation = this.getInstallationForRepo(owner, repo);
1784
+ if (!installation) {
1785
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1786
+ }
1787
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1788
+ const branches = [];
1789
+ let page = 1;
1790
+ while (true) {
1791
+ const { data } = await octokit.repos.listBranches({
1792
+ owner,
1793
+ repo,
1794
+ per_page: 100,
1795
+ page
1796
+ });
1797
+ if (data.length === 0) break;
1798
+ for (const branch of data) {
1799
+ if (branch.name.startsWith(prefix)) {
1800
+ branches.push(branch.name);
1801
+ }
1802
+ }
1803
+ page++;
1804
+ }
1805
+ return branches;
1806
+ }
1807
+ // ─────────────────────────────────────────────────────────────
1808
+ // Issue Management
1809
+ // ─────────────────────────────────────────────────────────────
1810
+ /**
1811
+ * Create an issue
1812
+ */
1813
+ async createIssue(owner, repo, options) {
1814
+ if (!this.initialized) {
1815
+ await this.initialize();
1816
+ }
1817
+ const installation = this.getInstallationForRepo(owner, repo);
1818
+ if (!installation) {
1819
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1820
+ }
1821
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1822
+ const { data: issue } = await octokit.issues.create({
1823
+ owner,
1824
+ repo,
1825
+ title: options.title,
1826
+ body: options.body,
1827
+ labels: options.labels,
1828
+ assignees: options.assignees,
1829
+ milestone: options.milestone
1830
+ });
1831
+ this.log(
1832
+ "info",
1833
+ { owner, repo, issueNumber: issue.number, title: options.title },
1834
+ "Issue created"
1835
+ );
1836
+ return {
1837
+ number: issue.number,
1838
+ url: issue.html_url,
1839
+ state: issue.state,
1840
+ title: issue.title,
1841
+ body: issue.body || "",
1842
+ labels: issue.labels.map(
1843
+ (l) => typeof l === "string" ? l : l.name || ""
1844
+ ),
1845
+ assignees: issue.assignees?.map((a) => a.login) || [],
1846
+ createdAt: new Date(issue.created_at),
1847
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1848
+ };
1849
+ }
1850
+ /**
1851
+ * Get an issue by number
1852
+ */
1853
+ async getIssue(owner, repo, issueNumber) {
1854
+ if (!this.initialized) {
1855
+ await this.initialize();
1856
+ }
1857
+ const installation = this.getInstallationForRepo(owner, repo);
1858
+ if (!installation) {
1859
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1860
+ }
1861
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1862
+ const { data: issue } = await octokit.issues.get({
1863
+ owner,
1864
+ repo,
1865
+ issue_number: issueNumber
1866
+ });
1867
+ return {
1868
+ number: issue.number,
1869
+ url: issue.html_url,
1870
+ state: issue.state,
1871
+ title: issue.title,
1872
+ body: issue.body || "",
1873
+ labels: issue.labels.map(
1874
+ (l) => typeof l === "string" ? l : l.name || ""
1875
+ ),
1876
+ assignees: issue.assignees?.map((a) => a.login) || [],
1877
+ createdAt: new Date(issue.created_at),
1878
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1879
+ };
1880
+ }
1881
+ /**
1882
+ * List issues with optional filters
1883
+ */
1884
+ async listIssues(owner, repo, options) {
1885
+ if (!this.initialized) {
1886
+ await this.initialize();
1887
+ }
1888
+ const installation = this.getInstallationForRepo(owner, repo);
1889
+ if (!installation) {
1890
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1891
+ }
1892
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1893
+ const { data: issues } = await octokit.issues.listForRepo({
1894
+ owner,
1895
+ repo,
1896
+ state: options?.state || "open",
1897
+ labels: options?.labels?.join(","),
1898
+ assignee: options?.assignee,
1899
+ since: options?.since?.toISOString(),
1900
+ per_page: 100
1901
+ });
1902
+ return issues.filter((issue) => !issue.pull_request).map((issue) => ({
1903
+ number: issue.number,
1904
+ url: issue.html_url,
1905
+ state: issue.state,
1906
+ title: issue.title,
1907
+ body: issue.body || "",
1908
+ labels: issue.labels.map(
1909
+ (l) => typeof l === "string" ? l : l.name || ""
1910
+ ),
1911
+ assignees: issue.assignees?.map((a) => a.login) || [],
1912
+ createdAt: new Date(issue.created_at),
1913
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1914
+ }));
1915
+ }
1916
+ /**
1917
+ * Update an issue (labels, state, assignees, etc.)
1918
+ */
1919
+ async updateIssue(owner, repo, issueNumber, options) {
1920
+ if (!this.initialized) {
1921
+ await this.initialize();
1922
+ }
1923
+ const installation = this.getInstallationForRepo(owner, repo);
1924
+ if (!installation) {
1925
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1926
+ }
1927
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1928
+ const { data: issue } = await octokit.issues.update({
1929
+ owner,
1930
+ repo,
1931
+ issue_number: issueNumber,
1932
+ title: options.title,
1933
+ body: options.body,
1934
+ state: options.state,
1935
+ labels: options.labels,
1936
+ assignees: options.assignees
1937
+ });
1938
+ this.log(
1939
+ "info",
1940
+ { owner, repo, issueNumber, updates: Object.keys(options) },
1941
+ "Issue updated"
1942
+ );
1943
+ return {
1944
+ number: issue.number,
1945
+ url: issue.html_url,
1946
+ state: issue.state,
1947
+ title: issue.title,
1948
+ body: issue.body || "",
1949
+ labels: issue.labels.map(
1950
+ (l) => typeof l === "string" ? l : l.name || ""
1951
+ ),
1952
+ assignees: issue.assignees?.map((a) => a.login) || [],
1953
+ createdAt: new Date(issue.created_at),
1954
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
1955
+ };
1956
+ }
1957
+ /**
1958
+ * Add labels to an issue
1959
+ */
1960
+ async addLabels(owner, repo, issueNumber, labels) {
1961
+ if (!this.initialized) {
1962
+ await this.initialize();
1963
+ }
1964
+ const installation = this.getInstallationForRepo(owner, repo);
1965
+ if (!installation) {
1966
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1967
+ }
1968
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1969
+ await octokit.issues.addLabels({
1970
+ owner,
1971
+ repo,
1972
+ issue_number: issueNumber,
1973
+ labels
1974
+ });
1975
+ this.log("info", { owner, repo, issueNumber, labels }, "Labels added to issue");
1976
+ }
1977
+ /**
1978
+ * Remove a label from an issue
1979
+ */
1980
+ async removeLabel(owner, repo, issueNumber, label) {
1981
+ if (!this.initialized) {
1982
+ await this.initialize();
1983
+ }
1984
+ const installation = this.getInstallationForRepo(owner, repo);
1985
+ if (!installation) {
1986
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
1987
+ }
1988
+ const octokit = await this.getInstallationOctokit(installation.installationId);
1989
+ await octokit.issues.removeLabel({
1990
+ owner,
1991
+ repo,
1992
+ issue_number: issueNumber,
1993
+ name: label
1994
+ });
1995
+ this.log("info", { owner, repo, issueNumber, label }, "Label removed from issue");
1996
+ }
1997
+ /**
1998
+ * Add a comment to an issue
1999
+ */
2000
+ async addComment(owner, repo, issueNumber, options) {
2001
+ if (!this.initialized) {
2002
+ await this.initialize();
2003
+ }
2004
+ const installation = this.getInstallationForRepo(owner, repo);
2005
+ if (!installation) {
2006
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
2007
+ }
2008
+ const octokit = await this.getInstallationOctokit(installation.installationId);
2009
+ const { data: comment } = await octokit.issues.createComment({
2010
+ owner,
2011
+ repo,
2012
+ issue_number: issueNumber,
2013
+ body: options.body
2014
+ });
2015
+ this.log("info", { owner, repo, issueNumber, commentId: comment.id }, "Comment added to issue");
2016
+ return {
2017
+ id: comment.id,
2018
+ url: comment.html_url,
2019
+ body: comment.body || "",
2020
+ author: comment.user?.login || "unknown",
2021
+ createdAt: new Date(comment.created_at)
2022
+ };
2023
+ }
2024
+ /**
2025
+ * List comments on an issue
2026
+ */
2027
+ async listComments(owner, repo, issueNumber) {
2028
+ if (!this.initialized) {
2029
+ await this.initialize();
2030
+ }
2031
+ const installation = this.getInstallationForRepo(owner, repo);
2032
+ if (!installation) {
2033
+ throw new Error(`No GitHub App installation found for ${owner}/${repo}`);
2034
+ }
2035
+ const octokit = await this.getInstallationOctokit(installation.installationId);
2036
+ const { data: comments } = await octokit.issues.listComments({
2037
+ owner,
2038
+ repo,
2039
+ issue_number: issueNumber,
2040
+ per_page: 100
2041
+ });
2042
+ return comments.map((comment) => ({
2043
+ id: comment.id,
2044
+ url: comment.html_url,
2045
+ body: comment.body || "",
2046
+ author: comment.user?.login || "unknown",
2047
+ createdAt: new Date(comment.created_at)
2048
+ }));
2049
+ }
2050
+ /**
2051
+ * Close an issue
2052
+ */
2053
+ async closeIssue(owner, repo, issueNumber) {
2054
+ return this.updateIssue(owner, repo, issueNumber, { state: "closed" });
2055
+ }
2056
+ /**
2057
+ * Reopen an issue
2058
+ */
2059
+ async reopenIssue(owner, repo, issueNumber) {
2060
+ return this.updateIssue(owner, repo, issueNumber, { state: "open" });
2061
+ }
2062
+ // ─────────────────────────────────────────────────────────────
2063
+ // Private Methods
2064
+ // ─────────────────────────────────────────────────────────────
2065
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2066
+ async getInstallationOctokit(installationId) {
2067
+ if (!this.installationOctokits.has(installationId)) {
2068
+ const { Octokit, createAppAuth } = loadOctokit();
2069
+ const octokit = new Octokit({
2070
+ authStrategy: createAppAuth,
2071
+ auth: {
2072
+ appId: this.config.appId,
2073
+ privateKey: this.config.privateKey,
2074
+ installationId
2075
+ },
2076
+ baseUrl: this.config.baseUrl
2077
+ });
2078
+ this.installationOctokits.set(installationId, octokit);
2079
+ }
2080
+ return this.installationOctokits.get(installationId);
2081
+ }
2082
+ parseRepo(repo) {
2083
+ const patterns = [
2084
+ /github\.com[/:]([^/]+)\/([^/.]+)/,
2085
+ /^([^/]+)\/([^/]+)$/
2086
+ ];
2087
+ for (const pattern of patterns) {
2088
+ const match = repo.match(pattern);
2089
+ if (match) {
2090
+ return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
2091
+ }
2092
+ }
2093
+ return null;
2094
+ }
2095
+ log(level, data, message) {
2096
+ if (this.logger) {
2097
+ this.logger[level](data, message);
2098
+ }
2099
+ }
2100
+ };
2101
+
2102
+ // src/providers/github-pat-client.ts
2103
+ var OctokitClass = null;
2104
+ function loadOctokit2() {
2105
+ if (!OctokitClass) {
2106
+ try {
2107
+ const { Octokit } = __require("@octokit/rest");
2108
+ OctokitClass = Octokit;
2109
+ } catch {
2110
+ throw new Error(
2111
+ "@octokit/rest is required for GitHubPatClient. Install it with: npm install @octokit/rest"
2112
+ );
2113
+ }
2114
+ }
2115
+ return OctokitClass;
2116
+ }
2117
+ var GitHubPatClient = class {
2118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2119
+ octokit;
2120
+ logger;
2121
+ constructor(options, logger) {
2122
+ this.logger = logger;
2123
+ const Octokit = loadOctokit2();
2124
+ this.octokit = new Octokit({
2125
+ auth: options.token,
2126
+ baseUrl: options.baseUrl
2127
+ });
2128
+ }
2129
+ // ─────────────────────────────────────────────────────────────
2130
+ // Pull Requests
2131
+ // ─────────────────────────────────────────────────────────────
2132
+ /**
2133
+ * Create a pull request
2134
+ */
2135
+ async createPullRequest(owner, repo, options) {
2136
+ const { data: pr } = await this.octokit.pulls.create({
2137
+ owner,
2138
+ repo,
2139
+ title: options.title,
2140
+ body: options.body,
2141
+ head: options.head,
2142
+ base: options.base,
2143
+ draft: options.draft
2144
+ });
2145
+ if (options.labels && options.labels.length > 0) {
2146
+ await this.octokit.issues.addLabels({
2147
+ owner,
2148
+ repo,
2149
+ issue_number: pr.number,
2150
+ labels: options.labels
2151
+ });
2152
+ }
2153
+ if (options.reviewers && options.reviewers.length > 0) {
2154
+ await this.octokit.pulls.requestReviewers({
2155
+ owner,
2156
+ repo,
2157
+ pull_number: pr.number,
2158
+ reviewers: options.reviewers
2159
+ });
2160
+ }
2161
+ this.log("info", { owner, repo, prNumber: pr.number }, "Pull request created");
2162
+ return {
2163
+ number: pr.number,
2164
+ url: pr.html_url,
2165
+ state: pr.state,
2166
+ sourceBranch: options.head,
2167
+ targetBranch: options.base,
2168
+ title: options.title,
2169
+ executionId: "",
2170
+ createdAt: new Date(pr.created_at)
2171
+ };
2172
+ }
2173
+ /**
2174
+ * Get a pull request
2175
+ */
2176
+ async getPullRequest(owner, repo, prNumber) {
2177
+ const { data: pr } = await this.octokit.pulls.get({
2178
+ owner,
2179
+ repo,
2180
+ pull_number: prNumber
2181
+ });
2182
+ return {
2183
+ number: pr.number,
2184
+ url: pr.html_url,
2185
+ state: pr.merged ? "merged" : pr.state,
2186
+ sourceBranch: pr.head.ref,
2187
+ targetBranch: pr.base.ref,
2188
+ title: pr.title,
2189
+ executionId: "",
2190
+ createdAt: new Date(pr.created_at),
2191
+ mergedAt: pr.merged_at ? new Date(pr.merged_at) : void 0
2192
+ };
2193
+ }
2194
+ // ─────────────────────────────────────────────────────────────
2195
+ // Issues
2196
+ // ─────────────────────────────────────────────────────────────
2197
+ /**
2198
+ * Create an issue
2199
+ */
2200
+ async createIssue(owner, repo, options) {
2201
+ const { data: issue } = await this.octokit.issues.create({
2202
+ owner,
2203
+ repo,
2204
+ title: options.title,
2205
+ body: options.body,
2206
+ labels: options.labels,
2207
+ assignees: options.assignees,
2208
+ milestone: options.milestone
2209
+ });
2210
+ this.log("info", { owner, repo, issueNumber: issue.number }, "Issue created");
2211
+ return this.mapIssue(issue);
2212
+ }
2213
+ /**
2214
+ * Get an issue
2215
+ */
2216
+ async getIssue(owner, repo, issueNumber) {
2217
+ const { data: issue } = await this.octokit.issues.get({
2218
+ owner,
2219
+ repo,
2220
+ issue_number: issueNumber
2221
+ });
2222
+ return this.mapIssue(issue);
2223
+ }
2224
+ /**
2225
+ * List issues
2226
+ */
2227
+ async listIssues(owner, repo, options) {
2228
+ const { data: issues } = await this.octokit.issues.listForRepo({
2229
+ owner,
2230
+ repo,
2231
+ state: options?.state || "open",
2232
+ labels: options?.labels?.join(","),
2233
+ assignee: options?.assignee,
2234
+ per_page: 100
2235
+ });
2236
+ return issues.filter((issue) => !issue.pull_request).map((issue) => this.mapIssue(issue));
2237
+ }
2238
+ /**
2239
+ * Update an issue
2240
+ */
2241
+ async updateIssue(owner, repo, issueNumber, options) {
2242
+ const { data: issue } = await this.octokit.issues.update({
2243
+ owner,
2244
+ repo,
2245
+ issue_number: issueNumber,
2246
+ ...options
2247
+ });
2248
+ this.log("info", { owner, repo, issueNumber }, "Issue updated");
2249
+ return this.mapIssue(issue);
2250
+ }
2251
+ /**
2252
+ * Add labels to an issue
2253
+ */
2254
+ async addLabels(owner, repo, issueNumber, labels) {
2255
+ await this.octokit.issues.addLabels({
2256
+ owner,
2257
+ repo,
2258
+ issue_number: issueNumber,
2259
+ labels
2260
+ });
2261
+ this.log("info", { owner, repo, issueNumber, labels }, "Labels added");
2262
+ }
2263
+ /**
2264
+ * Remove a label from an issue
2265
+ */
2266
+ async removeLabel(owner, repo, issueNumber, label) {
2267
+ await this.octokit.issues.removeLabel({
2268
+ owner,
2269
+ repo,
2270
+ issue_number: issueNumber,
2271
+ name: label
2272
+ });
2273
+ this.log("info", { owner, repo, issueNumber, label }, "Label removed");
2274
+ }
2275
+ /**
2276
+ * Add a comment to an issue or PR
2277
+ */
2278
+ async addComment(owner, repo, issueNumber, options) {
2279
+ const { data: comment } = await this.octokit.issues.createComment({
2280
+ owner,
2281
+ repo,
2282
+ issue_number: issueNumber,
2283
+ body: options.body
2284
+ });
2285
+ this.log("info", { owner, repo, issueNumber, commentId: comment.id }, "Comment added");
2286
+ return {
2287
+ id: comment.id,
2288
+ url: comment.html_url,
2289
+ body: comment.body || "",
2290
+ author: comment.user?.login || "unknown",
2291
+ createdAt: new Date(comment.created_at)
2292
+ };
2293
+ }
2294
+ /**
2295
+ * List comments on an issue or PR
2296
+ */
2297
+ async listComments(owner, repo, issueNumber) {
2298
+ const { data: comments } = await this.octokit.issues.listComments({
2299
+ owner,
2300
+ repo,
2301
+ issue_number: issueNumber,
2302
+ per_page: 100
2303
+ });
2304
+ return comments.map((comment) => ({
2305
+ id: comment.id,
2306
+ url: comment.html_url,
2307
+ body: comment.body || "",
2308
+ author: comment.user?.login || "unknown",
2309
+ createdAt: new Date(comment.created_at)
2310
+ }));
2311
+ }
2312
+ /**
2313
+ * Close an issue
2314
+ */
2315
+ async closeIssue(owner, repo, issueNumber) {
2316
+ return this.updateIssue(owner, repo, issueNumber, { state: "closed" });
2317
+ }
2318
+ /**
2319
+ * Reopen an issue
2320
+ */
2321
+ async reopenIssue(owner, repo, issueNumber) {
2322
+ return this.updateIssue(owner, repo, issueNumber, { state: "open" });
2323
+ }
2324
+ // ─────────────────────────────────────────────────────────────
2325
+ // Branches
2326
+ // ─────────────────────────────────────────────────────────────
2327
+ /**
2328
+ * Delete a branch
2329
+ */
2330
+ async deleteBranch(owner, repo, branch) {
2331
+ await this.octokit.git.deleteRef({
2332
+ owner,
2333
+ repo,
2334
+ ref: `heads/${branch}`
2335
+ });
2336
+ this.log("info", { owner, repo, branch }, "Branch deleted");
2337
+ }
2338
+ /**
2339
+ * Check if a branch exists
2340
+ */
2341
+ async branchExists(owner, repo, branch) {
2342
+ try {
2343
+ await this.octokit.repos.getBranch({ owner, repo, branch });
2344
+ return true;
2345
+ } catch {
2346
+ return false;
2347
+ }
2348
+ }
2349
+ // ─────────────────────────────────────────────────────────────
2350
+ // Private Methods
2351
+ // ─────────────────────────────────────────────────────────────
2352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2353
+ mapIssue(issue) {
2354
+ return {
2355
+ number: issue.number,
2356
+ url: issue.html_url,
2357
+ state: issue.state,
2358
+ title: issue.title,
2359
+ body: issue.body || "",
2360
+ labels: issue.labels.map(
2361
+ (l) => typeof l === "string" ? l : l.name || ""
2362
+ ),
2363
+ assignees: issue.assignees?.map((a) => a.login) || [],
2364
+ createdAt: new Date(issue.created_at),
2365
+ closedAt: issue.closed_at ? new Date(issue.closed_at) : void 0
2366
+ };
2367
+ }
2368
+ log(level, data, message) {
2369
+ if (this.logger) {
2370
+ this.logger[level](data, message);
2371
+ }
2372
+ }
2373
+ };
2374
+
2375
+ // src/types.ts
2376
+ var DEFAULT_AGENT_PERMISSIONS = {
2377
+ repositories: { type: "selected", repos: [] },
2378
+ contents: "write",
2379
+ pullRequests: "write",
2380
+ issues: "write",
2381
+ metadata: "read",
2382
+ canDeleteBranch: true,
2383
+ // Needed for cleanup
2384
+ canForcePush: false,
2385
+ canDeleteRepository: false,
2386
+ canAdminister: false
2387
+ };
2388
+ var READONLY_AGENT_PERMISSIONS = {
2389
+ repositories: { type: "selected", repos: [] },
2390
+ contents: "read",
2391
+ pullRequests: "read",
2392
+ issues: "read",
2393
+ metadata: "read",
2394
+ canDeleteBranch: false,
2395
+ canForcePush: false,
2396
+ canDeleteRepository: false,
2397
+ canAdminister: false
2398
+ };
2399
+
2400
+ exports.CredentialService = CredentialService;
2401
+ exports.DEFAULT_AGENT_PERMISSIONS = DEFAULT_AGENT_PERMISSIONS;
2402
+ exports.DEFAULT_BRANCH_PREFIX = DEFAULT_BRANCH_PREFIX;
2403
+ exports.FileTokenStore = FileTokenStore;
2404
+ exports.GitHubPatClient = GitHubPatClient;
2405
+ exports.GitHubProvider = GitHubProvider;
2406
+ exports.MemoryTokenStore = MemoryTokenStore;
2407
+ exports.OAuthDeviceFlow = OAuthDeviceFlow;
2408
+ exports.READONLY_AGENT_PERMISSIONS = READONLY_AGENT_PERMISSIONS;
2409
+ exports.TokenStore = TokenStore;
2410
+ exports.WorkspaceService = WorkspaceService;
2411
+ exports.cleanupCredentialFiles = cleanupCredentialFiles;
2412
+ exports.configureCredentialHelper = configureCredentialHelper;
2413
+ exports.createBranchInfo = createBranchInfo;
2414
+ exports.createNodeCredentialHelperScript = createNodeCredentialHelperScript;
2415
+ exports.createShellCredentialHelperScript = createShellCredentialHelperScript;
2416
+ exports.filterBranchesByExecution = filterBranchesByExecution;
2417
+ exports.generateBranchName = generateBranchName;
2418
+ exports.generateSlug = generateSlug;
2419
+ exports.getGitCredentialConfig = getGitCredentialConfig;
2420
+ exports.isManagedBranch = isManagedBranch;
2421
+ exports.outputCredentials = outputCredentials;
2422
+ exports.parseBranchName = parseBranchName;
2423
+ exports.updateCredentials = updateCredentials;
2424
+ //# sourceMappingURL=index.cjs.map
2425
+ //# sourceMappingURL=index.cjs.map