agent-relay 1.2.3 → 1.3.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/.trajectories/agent-relay-322-324.md +17 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
- package/.trajectories/consolidate-settings-panel.md +24 -0
- package/.trajectories/gh-cli-user-token.md +26 -0
- package/.trajectories/index.json +155 -1
- package/deploy/workspace/codex.config.toml +15 -0
- package/deploy/workspace/entrypoint.sh +167 -7
- package/deploy/workspace/git-credential-relay +17 -2
- package/dist/bridge/spawner.d.ts +7 -0
- package/dist/bridge/spawner.js +40 -9
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.js +210 -168
- package/dist/cloud/api/admin.d.ts +8 -0
- package/dist/cloud/api/admin.js +212 -0
- package/dist/cloud/api/auth.js +8 -0
- package/dist/cloud/api/billing.d.ts +0 -10
- package/dist/cloud/api/billing.js +248 -58
- package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
- package/dist/cloud/api/codex-auth-helper.js +215 -8
- package/dist/cloud/api/coordinators.js +402 -0
- package/dist/cloud/api/daemons.js +15 -11
- package/dist/cloud/api/git.js +104 -17
- package/dist/cloud/api/github-app.js +42 -8
- package/dist/cloud/api/nango-auth.js +297 -16
- package/dist/cloud/api/onboarding.js +97 -33
- package/dist/cloud/api/providers.js +12 -16
- package/dist/cloud/api/repos.js +200 -124
- package/dist/cloud/api/test-helpers.js +40 -0
- package/dist/cloud/api/usage.js +13 -0
- package/dist/cloud/api/webhooks.js +1 -1
- package/dist/cloud/api/workspaces.d.ts +18 -0
- package/dist/cloud/api/workspaces.js +945 -15
- package/dist/cloud/config.d.ts +8 -0
- package/dist/cloud/config.js +15 -0
- package/dist/cloud/db/drizzle.d.ts +5 -2
- package/dist/cloud/db/drizzle.js +27 -20
- package/dist/cloud/db/schema.d.ts +19 -51
- package/dist/cloud/db/schema.js +5 -4
- package/dist/cloud/index.d.ts +0 -1
- package/dist/cloud/index.js +0 -1
- package/dist/cloud/provisioner/index.d.ts +93 -1
- package/dist/cloud/provisioner/index.js +608 -63
- package/dist/cloud/server.js +156 -16
- package/dist/cloud/services/compute-enforcement.d.ts +57 -0
- package/dist/cloud/services/compute-enforcement.js +175 -0
- package/dist/cloud/services/index.d.ts +2 -0
- package/dist/cloud/services/index.js +4 -0
- package/dist/cloud/services/intro-expiration.d.ts +55 -0
- package/dist/cloud/services/intro-expiration.js +211 -0
- package/dist/cloud/services/nango.d.ts +14 -0
- package/dist/cloud/services/nango.js +74 -14
- package/dist/cloud/services/ssh-security.d.ts +31 -0
- package/dist/cloud/services/ssh-security.js +63 -0
- package/dist/continuity/manager.d.ts +5 -0
- package/dist/continuity/manager.js +56 -2
- package/dist/daemon/api.d.ts +2 -0
- package/dist/daemon/api.js +214 -5
- package/dist/daemon/cli-auth.d.ts +13 -1
- package/dist/daemon/cli-auth.js +166 -47
- package/dist/daemon/connection.d.ts +7 -1
- package/dist/daemon/connection.js +15 -0
- package/dist/daemon/orchestrator.d.ts +2 -0
- package/dist/daemon/orchestrator.js +26 -0
- package/dist/daemon/repo-manager.d.ts +116 -0
- package/dist/daemon/repo-manager.js +384 -0
- package/dist/daemon/router.d.ts +60 -1
- package/dist/daemon/router.js +281 -20
- package/dist/daemon/user-directory.d.ts +111 -0
- package/dist/daemon/user-directory.js +233 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/{page-abb9ab2d329f56e9.js → page-8c8bed33beb2bf1c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/{page-c22d080201cbd9fb.js → page-16f3b49e55b1e0ed.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-4a5938c18a11a654.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/{page-68d34f50baa8ab6b.js → page-547dd0ca55ecd0ba.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-app-6e8e8d3ef4e0192a.js → main-app-5d692157a8eb1fd9.js} +1 -1
- package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +3 -3
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +3 -3
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +3 -3
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +3 -3
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +3 -3
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +3 -3
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +3 -3
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +3 -3
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +3 -3
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +3 -3
- package/dist/dashboard-server/server.js +316 -12
- package/dist/dashboard-server/user-bridge.d.ts +103 -0
- package/dist/dashboard-server/user-bridge.js +189 -0
- package/dist/protocol/channels.d.ts +205 -0
- package/dist/protocol/channels.js +154 -0
- package/dist/protocol/types.d.ts +13 -1
- package/dist/resiliency/provider-context.js +2 -0
- package/dist/shared/cli-auth-config.d.ts +19 -0
- package/dist/shared/cli-auth-config.js +58 -2
- package/dist/utils/agent-config.js +1 -1
- package/dist/wrapper/auth-detection.d.ts +49 -0
- package/dist/wrapper/auth-detection.js +192 -0
- package/dist/wrapper/base-wrapper.d.ts +153 -0
- package/dist/wrapper/base-wrapper.js +393 -0
- package/dist/wrapper/client.d.ts +7 -1
- package/dist/wrapper/client.js +3 -0
- package/dist/wrapper/index.d.ts +1 -0
- package/dist/wrapper/index.js +4 -3
- package/dist/wrapper/pty-wrapper.d.ts +62 -84
- package/dist/wrapper/pty-wrapper.js +154 -180
- package/dist/wrapper/tmux-wrapper.d.ts +41 -66
- package/dist/wrapper/tmux-wrapper.js +90 -134
- package/package.json +4 -2
- package/scripts/postinstall.js +11 -155
- package/scripts/test-interactive-terminal.sh +248 -0
- package/dist/cloud/vault/index.d.ts +0 -76
- package/dist/cloud/vault/index.js +0 -219
- package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
- package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
- package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
- /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → T1tgCqVWHFIkV7ClEtzD7}/_buildManifest.js +0 -0
|
@@ -20,6 +20,408 @@ const coordinatorWriteRoutes = [
|
|
|
20
20
|
coordinatorWriteRoutes.forEach(route => {
|
|
21
21
|
coordinatorsRouter.use(route, checkCoordinatorAccess);
|
|
22
22
|
});
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Project Group CRUD Routes
|
|
25
|
+
// These must come BEFORE the /:groupId/coordinator routes
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* GET /api/project-groups
|
|
29
|
+
* List all project groups for the authenticated user
|
|
30
|
+
*/
|
|
31
|
+
coordinatorsRouter.get('/', async (req, res) => {
|
|
32
|
+
const userId = req.session.userId;
|
|
33
|
+
try {
|
|
34
|
+
const result = await db.projectGroups.findAllWithRepositories(userId);
|
|
35
|
+
res.json({
|
|
36
|
+
groups: result.groups.map(group => ({
|
|
37
|
+
id: group.id,
|
|
38
|
+
name: group.name,
|
|
39
|
+
description: group.description,
|
|
40
|
+
color: group.color,
|
|
41
|
+
icon: group.icon,
|
|
42
|
+
coordinatorAgent: group.coordinatorAgent,
|
|
43
|
+
sortOrder: group.sortOrder,
|
|
44
|
+
repositoryCount: group.repositories.length,
|
|
45
|
+
repositories: group.repositories.map(repo => ({
|
|
46
|
+
id: repo.id,
|
|
47
|
+
githubFullName: repo.githubFullName,
|
|
48
|
+
defaultBranch: repo.defaultBranch,
|
|
49
|
+
isPrivate: repo.isPrivate,
|
|
50
|
+
})),
|
|
51
|
+
createdAt: group.createdAt,
|
|
52
|
+
updatedAt: group.updatedAt,
|
|
53
|
+
})),
|
|
54
|
+
ungroupedRepositories: result.ungroupedRepositories.map(repo => ({
|
|
55
|
+
id: repo.id,
|
|
56
|
+
githubFullName: repo.githubFullName,
|
|
57
|
+
defaultBranch: repo.defaultBranch,
|
|
58
|
+
isPrivate: repo.isPrivate,
|
|
59
|
+
workspaceId: repo.workspaceId,
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('Error listing project groups:', error);
|
|
65
|
+
res.status(500).json({ error: 'Failed to list project groups' });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* POST /api/project-groups
|
|
70
|
+
* Create a new project group
|
|
71
|
+
*/
|
|
72
|
+
coordinatorsRouter.post('/', async (req, res) => {
|
|
73
|
+
const userId = req.session.userId;
|
|
74
|
+
const { name, description, color, icon, repositoryIds } = req.body;
|
|
75
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
76
|
+
return res.status(400).json({ error: 'Name is required' });
|
|
77
|
+
}
|
|
78
|
+
if (name.length > 255) {
|
|
79
|
+
return res.status(400).json({ error: 'Name must be 255 characters or less' });
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// Check for duplicate name
|
|
83
|
+
const existing = await db.projectGroups.findByName(userId, name.trim());
|
|
84
|
+
if (existing) {
|
|
85
|
+
return res.status(409).json({ error: 'A project group with this name already exists' });
|
|
86
|
+
}
|
|
87
|
+
// Create the group
|
|
88
|
+
const group = await db.projectGroups.create({
|
|
89
|
+
userId,
|
|
90
|
+
name: name.trim(),
|
|
91
|
+
description: description?.trim() || null,
|
|
92
|
+
color: color || null,
|
|
93
|
+
icon: icon || null,
|
|
94
|
+
coordinatorAgent: { enabled: false },
|
|
95
|
+
sortOrder: 0,
|
|
96
|
+
});
|
|
97
|
+
// Assign repositories to the group if provided
|
|
98
|
+
if (repositoryIds && Array.isArray(repositoryIds) && repositoryIds.length > 0) {
|
|
99
|
+
// Verify all repositories belong to the user
|
|
100
|
+
const userRepos = await db.repositories.findByUserId(userId);
|
|
101
|
+
const userRepoIds = new Set(userRepos.map(r => r.id));
|
|
102
|
+
for (const repoId of repositoryIds) {
|
|
103
|
+
if (!userRepoIds.has(repoId)) {
|
|
104
|
+
return res.status(400).json({
|
|
105
|
+
error: `Repository ${repoId} not found or not owned by user`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Assign repositories to the group
|
|
110
|
+
for (const repoId of repositoryIds) {
|
|
111
|
+
await db.repositories.assignToGroup(repoId, group.id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Fetch the group with repositories for response
|
|
115
|
+
const groupWithRepos = await db.projectGroups.findWithRepositories(group.id);
|
|
116
|
+
res.status(201).json({
|
|
117
|
+
success: true,
|
|
118
|
+
group: {
|
|
119
|
+
id: groupWithRepos.id,
|
|
120
|
+
name: groupWithRepos.name,
|
|
121
|
+
description: groupWithRepos.description,
|
|
122
|
+
color: groupWithRepos.color,
|
|
123
|
+
icon: groupWithRepos.icon,
|
|
124
|
+
coordinatorAgent: groupWithRepos.coordinatorAgent,
|
|
125
|
+
repositories: groupWithRepos.repositories.map(repo => ({
|
|
126
|
+
id: repo.id,
|
|
127
|
+
githubFullName: repo.githubFullName,
|
|
128
|
+
defaultBranch: repo.defaultBranch,
|
|
129
|
+
isPrivate: repo.isPrivate,
|
|
130
|
+
})),
|
|
131
|
+
createdAt: groupWithRepos.createdAt,
|
|
132
|
+
updatedAt: groupWithRepos.updatedAt,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('Error creating project group:', error);
|
|
138
|
+
res.status(500).json({ error: 'Failed to create project group' });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* GET /api/project-groups/:id
|
|
143
|
+
* Get a specific project group with its repositories
|
|
144
|
+
*/
|
|
145
|
+
coordinatorsRouter.get('/:id', async (req, res) => {
|
|
146
|
+
const userId = req.session.userId;
|
|
147
|
+
const { id } = req.params;
|
|
148
|
+
// Skip if this looks like a coordinator route
|
|
149
|
+
if (id === 'coordinators') {
|
|
150
|
+
return res.status(404).json({ error: 'Not found' });
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const group = await db.projectGroups.findWithRepositories(id);
|
|
154
|
+
if (!group) {
|
|
155
|
+
return res.status(404).json({ error: 'Project group not found' });
|
|
156
|
+
}
|
|
157
|
+
if (group.userId !== userId) {
|
|
158
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
159
|
+
}
|
|
160
|
+
res.json({
|
|
161
|
+
id: group.id,
|
|
162
|
+
name: group.name,
|
|
163
|
+
description: group.description,
|
|
164
|
+
color: group.color,
|
|
165
|
+
icon: group.icon,
|
|
166
|
+
coordinatorAgent: group.coordinatorAgent,
|
|
167
|
+
sortOrder: group.sortOrder,
|
|
168
|
+
repositories: group.repositories.map(repo => ({
|
|
169
|
+
id: repo.id,
|
|
170
|
+
githubFullName: repo.githubFullName,
|
|
171
|
+
defaultBranch: repo.defaultBranch,
|
|
172
|
+
isPrivate: repo.isPrivate,
|
|
173
|
+
syncStatus: repo.syncStatus,
|
|
174
|
+
lastSyncedAt: repo.lastSyncedAt,
|
|
175
|
+
workspaceId: repo.workspaceId,
|
|
176
|
+
})),
|
|
177
|
+
createdAt: group.createdAt,
|
|
178
|
+
updatedAt: group.updatedAt,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error('Error getting project group:', error);
|
|
183
|
+
res.status(500).json({ error: 'Failed to get project group' });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
/**
|
|
187
|
+
* PATCH /api/project-groups/:id
|
|
188
|
+
* Update a project group's metadata
|
|
189
|
+
*/
|
|
190
|
+
coordinatorsRouter.patch('/:id', async (req, res) => {
|
|
191
|
+
const userId = req.session.userId;
|
|
192
|
+
const { id } = req.params;
|
|
193
|
+
const { name, description, color, icon } = req.body;
|
|
194
|
+
try {
|
|
195
|
+
const group = await db.projectGroups.findById(id);
|
|
196
|
+
if (!group) {
|
|
197
|
+
return res.status(404).json({ error: 'Project group not found' });
|
|
198
|
+
}
|
|
199
|
+
if (group.userId !== userId) {
|
|
200
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
201
|
+
}
|
|
202
|
+
// Build update object with only provided fields
|
|
203
|
+
const updates = {};
|
|
204
|
+
if (name !== undefined) {
|
|
205
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
206
|
+
return res.status(400).json({ error: 'Name cannot be empty' });
|
|
207
|
+
}
|
|
208
|
+
if (name.length > 255) {
|
|
209
|
+
return res.status(400).json({ error: 'Name must be 255 characters or less' });
|
|
210
|
+
}
|
|
211
|
+
// Check for duplicate name (excluding current group)
|
|
212
|
+
const existing = await db.projectGroups.findByName(userId, name.trim());
|
|
213
|
+
if (existing && existing.id !== id) {
|
|
214
|
+
return res.status(409).json({ error: 'A project group with this name already exists' });
|
|
215
|
+
}
|
|
216
|
+
updates.name = name.trim();
|
|
217
|
+
}
|
|
218
|
+
if (description !== undefined) {
|
|
219
|
+
updates.description = description?.trim() || null;
|
|
220
|
+
}
|
|
221
|
+
if (color !== undefined) {
|
|
222
|
+
// Validate hex color format if provided
|
|
223
|
+
if (color && !/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
|
224
|
+
return res.status(400).json({ error: 'Color must be a valid hex color (e.g., #3B82F6)' });
|
|
225
|
+
}
|
|
226
|
+
updates.color = color || null;
|
|
227
|
+
}
|
|
228
|
+
if (icon !== undefined) {
|
|
229
|
+
updates.icon = icon || null;
|
|
230
|
+
}
|
|
231
|
+
if (Object.keys(updates).length === 0) {
|
|
232
|
+
return res.status(400).json({ error: 'No valid fields to update' });
|
|
233
|
+
}
|
|
234
|
+
await db.projectGroups.update(id, updates);
|
|
235
|
+
// Fetch updated group
|
|
236
|
+
const updatedGroup = await db.projectGroups.findWithRepositories(id);
|
|
237
|
+
res.json({
|
|
238
|
+
success: true,
|
|
239
|
+
group: {
|
|
240
|
+
id: updatedGroup.id,
|
|
241
|
+
name: updatedGroup.name,
|
|
242
|
+
description: updatedGroup.description,
|
|
243
|
+
color: updatedGroup.color,
|
|
244
|
+
icon: updatedGroup.icon,
|
|
245
|
+
coordinatorAgent: updatedGroup.coordinatorAgent,
|
|
246
|
+
repositories: updatedGroup.repositories.map(repo => ({
|
|
247
|
+
id: repo.id,
|
|
248
|
+
githubFullName: repo.githubFullName,
|
|
249
|
+
defaultBranch: repo.defaultBranch,
|
|
250
|
+
isPrivate: repo.isPrivate,
|
|
251
|
+
})),
|
|
252
|
+
updatedAt: updatedGroup.updatedAt,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
console.error('Error updating project group:', error);
|
|
258
|
+
res.status(500).json({ error: 'Failed to update project group' });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
/**
|
|
262
|
+
* DELETE /api/project-groups/:id
|
|
263
|
+
* Delete a project group (repositories are unassigned, not deleted)
|
|
264
|
+
*/
|
|
265
|
+
coordinatorsRouter.delete('/:id', async (req, res) => {
|
|
266
|
+
const userId = req.session.userId;
|
|
267
|
+
const { id } = req.params;
|
|
268
|
+
try {
|
|
269
|
+
const group = await db.projectGroups.findById(id);
|
|
270
|
+
if (!group) {
|
|
271
|
+
return res.status(404).json({ error: 'Project group not found' });
|
|
272
|
+
}
|
|
273
|
+
if (group.userId !== userId) {
|
|
274
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
275
|
+
}
|
|
276
|
+
// Stop coordinator if running
|
|
277
|
+
if (group.coordinatorAgent?.enabled) {
|
|
278
|
+
try {
|
|
279
|
+
const coordinatorService = getCoordinatorService();
|
|
280
|
+
await coordinatorService.stop(id);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
console.warn('Error stopping coordinator during group deletion:', err);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Delete the group (repositories will have projectGroupId set to null due to ON DELETE SET NULL)
|
|
287
|
+
await db.projectGroups.delete(id);
|
|
288
|
+
res.json({
|
|
289
|
+
success: true,
|
|
290
|
+
message: 'Project group deleted',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error('Error deleting project group:', error);
|
|
295
|
+
res.status(500).json({ error: 'Failed to delete project group' });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* POST /api/project-groups/:id/repositories
|
|
300
|
+
* Add repositories to a project group
|
|
301
|
+
*/
|
|
302
|
+
coordinatorsRouter.post('/:id/repositories', async (req, res) => {
|
|
303
|
+
const userId = req.session.userId;
|
|
304
|
+
const { id } = req.params;
|
|
305
|
+
const { repositoryIds } = req.body;
|
|
306
|
+
if (!repositoryIds || !Array.isArray(repositoryIds) || repositoryIds.length === 0) {
|
|
307
|
+
return res.status(400).json({ error: 'repositoryIds array is required' });
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const group = await db.projectGroups.findById(id);
|
|
311
|
+
if (!group) {
|
|
312
|
+
return res.status(404).json({ error: 'Project group not found' });
|
|
313
|
+
}
|
|
314
|
+
if (group.userId !== userId) {
|
|
315
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
316
|
+
}
|
|
317
|
+
// Verify all repositories belong to the user
|
|
318
|
+
const userRepos = await db.repositories.findByUserId(userId);
|
|
319
|
+
const userRepoIds = new Set(userRepos.map(r => r.id));
|
|
320
|
+
for (const repoId of repositoryIds) {
|
|
321
|
+
if (!userRepoIds.has(repoId)) {
|
|
322
|
+
return res.status(400).json({
|
|
323
|
+
error: `Repository ${repoId} not found or not owned by user`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Assign repositories to the group
|
|
328
|
+
for (const repoId of repositoryIds) {
|
|
329
|
+
await db.repositories.assignToGroup(repoId, id);
|
|
330
|
+
}
|
|
331
|
+
// Fetch updated group
|
|
332
|
+
const updatedGroup = await db.projectGroups.findWithRepositories(id);
|
|
333
|
+
res.json({
|
|
334
|
+
success: true,
|
|
335
|
+
group: {
|
|
336
|
+
id: updatedGroup.id,
|
|
337
|
+
name: updatedGroup.name,
|
|
338
|
+
repositories: updatedGroup.repositories.map(repo => ({
|
|
339
|
+
id: repo.id,
|
|
340
|
+
githubFullName: repo.githubFullName,
|
|
341
|
+
defaultBranch: repo.defaultBranch,
|
|
342
|
+
isPrivate: repo.isPrivate,
|
|
343
|
+
})),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.error('Error adding repositories to group:', error);
|
|
349
|
+
res.status(500).json({ error: 'Failed to add repositories to group' });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
/**
|
|
353
|
+
* DELETE /api/project-groups/:id/repositories/:repoId
|
|
354
|
+
* Remove a repository from a project group
|
|
355
|
+
*/
|
|
356
|
+
coordinatorsRouter.delete('/:id/repositories/:repoId', async (req, res) => {
|
|
357
|
+
const userId = req.session.userId;
|
|
358
|
+
const { id, repoId } = req.params;
|
|
359
|
+
try {
|
|
360
|
+
const group = await db.projectGroups.findById(id);
|
|
361
|
+
if (!group) {
|
|
362
|
+
return res.status(404).json({ error: 'Project group not found' });
|
|
363
|
+
}
|
|
364
|
+
if (group.userId !== userId) {
|
|
365
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
366
|
+
}
|
|
367
|
+
// Verify repository exists and belongs to this group
|
|
368
|
+
const repo = await db.repositories.findById(repoId);
|
|
369
|
+
if (!repo) {
|
|
370
|
+
return res.status(404).json({ error: 'Repository not found' });
|
|
371
|
+
}
|
|
372
|
+
if (repo.userId !== userId) {
|
|
373
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
374
|
+
}
|
|
375
|
+
if (repo.projectGroupId !== id) {
|
|
376
|
+
return res.status(400).json({ error: 'Repository is not in this group' });
|
|
377
|
+
}
|
|
378
|
+
// Remove repository from group (set projectGroupId to null)
|
|
379
|
+
await db.repositories.assignToGroup(repoId, null);
|
|
380
|
+
res.json({
|
|
381
|
+
success: true,
|
|
382
|
+
message: 'Repository removed from group',
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
console.error('Error removing repository from group:', error);
|
|
387
|
+
res.status(500).json({ error: 'Failed to remove repository from group' });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
/**
|
|
391
|
+
* PUT /api/project-groups/reorder
|
|
392
|
+
* Reorder project groups
|
|
393
|
+
*/
|
|
394
|
+
coordinatorsRouter.put('/reorder', async (req, res) => {
|
|
395
|
+
const userId = req.session.userId;
|
|
396
|
+
const { orderedIds } = req.body;
|
|
397
|
+
if (!orderedIds || !Array.isArray(orderedIds)) {
|
|
398
|
+
return res.status(400).json({ error: 'orderedIds array is required' });
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
// Verify all groups belong to user
|
|
402
|
+
const userGroups = await db.projectGroups.findByUserId(userId);
|
|
403
|
+
const userGroupIds = new Set(userGroups.map(g => g.id));
|
|
404
|
+
for (const groupId of orderedIds) {
|
|
405
|
+
if (!userGroupIds.has(groupId)) {
|
|
406
|
+
return res.status(400).json({
|
|
407
|
+
error: `Group ${groupId} not found or not owned by user`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await db.projectGroups.reorder(userId, orderedIds);
|
|
412
|
+
res.json({
|
|
413
|
+
success: true,
|
|
414
|
+
message: 'Groups reordered',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.error('Error reordering project groups:', error);
|
|
419
|
+
res.status(500).json({ error: 'Failed to reorder project groups' });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// Coordinator Agent Routes
|
|
424
|
+
// ============================================================================
|
|
23
425
|
/**
|
|
24
426
|
* GET /api/project-groups/:groupId/coordinator
|
|
25
427
|
* Get coordinator agent configuration
|
|
@@ -12,7 +12,6 @@ import { Router } from 'express';
|
|
|
12
12
|
import { randomBytes, createHash } from 'crypto';
|
|
13
13
|
import { requireAuth } from './auth.js';
|
|
14
14
|
import { db } from '../db/index.js';
|
|
15
|
-
import { vault } from '../vault/index.js';
|
|
16
15
|
export const daemonsRouter = Router();
|
|
17
16
|
/**
|
|
18
17
|
* Generate a secure API key
|
|
@@ -201,21 +200,26 @@ daemonsRouter.post('/heartbeat', requireDaemonAuth, async (req, res) => {
|
|
|
201
200
|
});
|
|
202
201
|
/**
|
|
203
202
|
* GET /api/daemons/credentials
|
|
204
|
-
* Get credentials for the daemon's user
|
|
203
|
+
* Get credentials for the daemon's user
|
|
204
|
+
*
|
|
205
|
+
* Note: Tokens are no longer stored centrally. CLI tools authenticate directly
|
|
206
|
+
* on workspace/local instances. This endpoint returns connected provider info only.
|
|
205
207
|
*/
|
|
206
208
|
daemonsRouter.get('/credentials', requireDaemonAuth, async (req, res) => {
|
|
207
209
|
const daemon = req.daemon;
|
|
208
210
|
try {
|
|
209
|
-
// Get
|
|
210
|
-
const
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
provider,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expiresAt: cred.tokenExpiresAt,
|
|
211
|
+
// Get connected providers for this user (no tokens stored centrally)
|
|
212
|
+
const credentials = await db.credentials.findByUserId(daemon.userId);
|
|
213
|
+
// Return provider info without tokens
|
|
214
|
+
const providers = credentials.map((cred) => ({
|
|
215
|
+
provider: cred.provider,
|
|
216
|
+
providerAccountEmail: cred.providerAccountEmail,
|
|
217
|
+
connectedAt: cred.createdAt,
|
|
217
218
|
}));
|
|
218
|
-
res.json({
|
|
219
|
+
res.json({
|
|
220
|
+
providers,
|
|
221
|
+
note: 'Tokens are authenticated locally on workspace instances via CLI.',
|
|
222
|
+
});
|
|
219
223
|
}
|
|
220
224
|
catch (error) {
|
|
221
225
|
console.error('Error fetching credentials:', error);
|
package/dist/cloud/api/git.js
CHANGED
|
@@ -23,20 +23,34 @@ function generateExpectedToken(workspaceId) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Verify workspace access token
|
|
25
25
|
* Workspaces authenticate with a secret passed at provisioning time
|
|
26
|
+
*
|
|
27
|
+
* Returns:
|
|
28
|
+
* - { valid: true } if token matches
|
|
29
|
+
* - { valid: false, reason: string } if token is invalid or missing
|
|
26
30
|
*/
|
|
27
31
|
function verifyWorkspaceToken(req, workspaceId) {
|
|
28
32
|
const authHeader = req.get('authorization');
|
|
29
|
-
if (!authHeader
|
|
30
|
-
return false;
|
|
33
|
+
if (!authHeader) {
|
|
34
|
+
return { valid: false, reason: 'No Authorization header. WORKSPACE_TOKEN may not be set in the container.' };
|
|
35
|
+
}
|
|
36
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
37
|
+
return { valid: false, reason: 'Invalid Authorization header format. Expected: Bearer <token>' };
|
|
31
38
|
}
|
|
32
39
|
const providedToken = authHeader.slice(7);
|
|
40
|
+
if (!providedToken) {
|
|
41
|
+
return { valid: false, reason: 'Empty bearer token provided.' };
|
|
42
|
+
}
|
|
33
43
|
const expectedToken = generateExpectedToken(workspaceId);
|
|
34
44
|
// Use timing-safe comparison to prevent timing attacks
|
|
35
45
|
try {
|
|
36
|
-
|
|
46
|
+
const isValid = crypto.timingSafeEqual(Buffer.from(providedToken), Buffer.from(expectedToken));
|
|
47
|
+
if (!isValid) {
|
|
48
|
+
return { valid: false, reason: 'Token mismatch. Workspace may need reprovisioning or SESSION_SECRET changed.' };
|
|
49
|
+
}
|
|
50
|
+
return { valid: true };
|
|
37
51
|
}
|
|
38
52
|
catch {
|
|
39
|
-
return false;
|
|
53
|
+
return { valid: false, reason: 'Token comparison failed (length mismatch). Workspace may need reprovisioning.' };
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
/**
|
|
@@ -57,29 +71,67 @@ gitRouter.get('/token', async (req, res) => {
|
|
|
57
71
|
return res.status(400).json({ error: 'workspaceId is required' });
|
|
58
72
|
}
|
|
59
73
|
// Verify the request is from a valid workspace
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
const tokenVerification = verifyWorkspaceToken(req, workspaceId);
|
|
75
|
+
if (!tokenVerification.valid) {
|
|
76
|
+
console.warn(`[git] Token verification failed for workspace ${workspaceId.substring(0, 8)}: ${tokenVerification.reason}`);
|
|
77
|
+
return res.status(401).json({
|
|
78
|
+
error: 'Invalid workspace token',
|
|
79
|
+
code: 'INVALID_WORKSPACE_TOKEN',
|
|
80
|
+
hint: tokenVerification.reason,
|
|
81
|
+
});
|
|
62
82
|
}
|
|
63
83
|
try {
|
|
64
84
|
// Get workspace to find the user
|
|
65
85
|
const workspace = await db.workspaces.findById(workspaceId);
|
|
66
86
|
if (!workspace) {
|
|
67
|
-
|
|
87
|
+
console.warn(`[git] Workspace not found: ${workspaceId}`);
|
|
88
|
+
return res.status(404).json({
|
|
89
|
+
error: 'Workspace not found',
|
|
90
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
91
|
+
hint: 'The workspace may have been deleted. Try reprovisioning.',
|
|
92
|
+
});
|
|
68
93
|
}
|
|
69
94
|
const userId = workspace.userId;
|
|
95
|
+
console.log(`[git] Token request for workspace ${workspaceId.substring(0, 8)}, user ${userId.substring(0, 8)}`);
|
|
70
96
|
// Find a repository with a Nango connection for this user
|
|
71
97
|
const repos = await db.repositories.findByUserId(userId);
|
|
72
98
|
const repoWithConnection = repos.find(r => r.nangoConnectionId);
|
|
73
99
|
if (!repoWithConnection?.nangoConnectionId) {
|
|
100
|
+
console.warn(`[git] No Nango connection found for user ${userId.substring(0, 8)}. Repos: ${repos.length}, with connections: ${repos.filter(r => r.nangoConnectionId).length}`);
|
|
74
101
|
return res.status(404).json({
|
|
75
102
|
error: 'No GitHub App connection found',
|
|
76
|
-
|
|
103
|
+
code: 'NO_GITHUB_APP_CONNECTION',
|
|
104
|
+
hint: 'Install the GitHub App on your repositories at https://github.com/apps/agent-relay',
|
|
105
|
+
repoCount: repos.length,
|
|
77
106
|
});
|
|
78
107
|
}
|
|
108
|
+
console.log(`[git] Fetching token from Nango for connection ${repoWithConnection.nangoConnectionId.substring(0, 8)}...`);
|
|
79
109
|
// Get fresh tokens from Nango (auto-refreshes if needed)
|
|
80
110
|
// - installationToken: for git operations (clone, push, pull)
|
|
81
111
|
// - userToken: for gh CLI operations (requires user context)
|
|
82
|
-
|
|
112
|
+
let installationToken;
|
|
113
|
+
try {
|
|
114
|
+
installationToken = await nangoService.getGithubAppToken(repoWithConnection.nangoConnectionId);
|
|
115
|
+
}
|
|
116
|
+
catch (nangoError) {
|
|
117
|
+
const errorMessage = nangoError instanceof Error ? nangoError.message : 'Unknown error';
|
|
118
|
+
console.error(`[git] Nango token fetch failed for connection ${repoWithConnection.nangoConnectionId}:`, errorMessage);
|
|
119
|
+
// Provide specific hints based on error type
|
|
120
|
+
if (errorMessage.includes('not found') || errorMessage.includes('404')) {
|
|
121
|
+
return res.status(500).json({
|
|
122
|
+
error: 'GitHub App connection expired or revoked',
|
|
123
|
+
code: 'NANGO_CONNECTION_EXPIRED',
|
|
124
|
+
hint: 'Reconnect your GitHub App at https://github.com/apps/agent-relay',
|
|
125
|
+
details: errorMessage,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return res.status(500).json({
|
|
129
|
+
error: 'Failed to fetch GitHub token from Nango',
|
|
130
|
+
code: 'NANGO_TOKEN_FETCH_FAILED',
|
|
131
|
+
hint: 'This may be a temporary issue. Try again in a few seconds.',
|
|
132
|
+
details: errorMessage,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
83
135
|
// Try to get user OAuth token from github-app-oauth connection_config first
|
|
84
136
|
// Fall back to separate 'github' user connection if available
|
|
85
137
|
let userToken = null;
|
|
@@ -100,6 +152,7 @@ gitRouter.get('/token', async (req, res) => {
|
|
|
100
152
|
}
|
|
101
153
|
// GitHub App installation tokens expire after 1 hour
|
|
102
154
|
const expiresAt = new Date(Date.now() + 55 * 60 * 1000).toISOString(); // 55 min buffer
|
|
155
|
+
console.log(`[git] Token fetched successfully for workspace ${workspaceId.substring(0, 8)}`);
|
|
103
156
|
res.json({
|
|
104
157
|
token: installationToken,
|
|
105
158
|
userToken, // For gh CLI - may be null if not available
|
|
@@ -108,8 +161,13 @@ gitRouter.get('/token', async (req, res) => {
|
|
|
108
161
|
});
|
|
109
162
|
}
|
|
110
163
|
catch (error) {
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
165
|
+
console.error('[git] Unexpected error getting token:', error);
|
|
166
|
+
res.status(500).json({
|
|
167
|
+
error: 'Failed to get GitHub token',
|
|
168
|
+
code: 'UNEXPECTED_ERROR',
|
|
169
|
+
details: errorMessage,
|
|
170
|
+
});
|
|
113
171
|
}
|
|
114
172
|
});
|
|
115
173
|
/**
|
|
@@ -121,22 +179,46 @@ gitRouter.post('/token', async (req, res) => {
|
|
|
121
179
|
if (!workspaceId || typeof workspaceId !== 'string') {
|
|
122
180
|
return res.status(400).json({ error: 'workspaceId is required' });
|
|
123
181
|
}
|
|
124
|
-
|
|
125
|
-
|
|
182
|
+
const tokenVerification = verifyWorkspaceToken(req, workspaceId);
|
|
183
|
+
if (!tokenVerification.valid) {
|
|
184
|
+
console.warn(`[git] POST: Token verification failed for workspace ${workspaceId.substring(0, 8)}: ${tokenVerification.reason}`);
|
|
185
|
+
return res.status(401).json({
|
|
186
|
+
error: 'Invalid workspace token',
|
|
187
|
+
code: 'INVALID_WORKSPACE_TOKEN',
|
|
188
|
+
hint: tokenVerification.reason,
|
|
189
|
+
});
|
|
126
190
|
}
|
|
127
191
|
try {
|
|
128
192
|
const workspace = await db.workspaces.findById(workspaceId);
|
|
129
193
|
if (!workspace) {
|
|
130
|
-
|
|
194
|
+
console.warn(`[git] POST: Workspace not found: ${workspaceId}`);
|
|
195
|
+
return res.status(404).json({
|
|
196
|
+
error: 'Workspace not found',
|
|
197
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
198
|
+
});
|
|
131
199
|
}
|
|
132
200
|
const repos = await db.repositories.findByUserId(workspace.userId);
|
|
133
201
|
const repoWithConnection = repos.find(r => r.nangoConnectionId);
|
|
134
202
|
if (!repoWithConnection?.nangoConnectionId) {
|
|
203
|
+
console.warn(`[git] POST: No Nango connection for user ${workspace.userId.substring(0, 8)}`);
|
|
135
204
|
return res.status(404).json({
|
|
136
205
|
error: 'No GitHub App connection found',
|
|
206
|
+
code: 'NO_GITHUB_APP_CONNECTION',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
let token;
|
|
210
|
+
try {
|
|
211
|
+
token = await nangoService.getGithubAppToken(repoWithConnection.nangoConnectionId);
|
|
212
|
+
}
|
|
213
|
+
catch (nangoError) {
|
|
214
|
+
const errorMessage = nangoError instanceof Error ? nangoError.message : 'Unknown error';
|
|
215
|
+
console.error(`[git] POST: Nango token fetch failed:`, errorMessage);
|
|
216
|
+
return res.status(500).json({
|
|
217
|
+
error: 'Failed to fetch GitHub token',
|
|
218
|
+
code: 'NANGO_TOKEN_FETCH_FAILED',
|
|
219
|
+
details: errorMessage,
|
|
137
220
|
});
|
|
138
221
|
}
|
|
139
|
-
const token = await nangoService.getGithubAppToken(repoWithConnection.nangoConnectionId);
|
|
140
222
|
const expiresAt = new Date(Date.now() + 55 * 60 * 1000).toISOString();
|
|
141
223
|
res.json({
|
|
142
224
|
token,
|
|
@@ -145,8 +227,13 @@ gitRouter.post('/token', async (req, res) => {
|
|
|
145
227
|
});
|
|
146
228
|
}
|
|
147
229
|
catch (error) {
|
|
148
|
-
|
|
149
|
-
|
|
230
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
231
|
+
console.error('[git] POST: Unexpected error:', error);
|
|
232
|
+
res.status(500).json({
|
|
233
|
+
error: 'Failed to get GitHub token',
|
|
234
|
+
code: 'UNEXPECTED_ERROR',
|
|
235
|
+
details: errorMessage,
|
|
236
|
+
});
|
|
150
237
|
}
|
|
151
238
|
});
|
|
152
239
|
//# sourceMappingURL=git.js.map
|