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
|
@@ -27,22 +27,56 @@ githubAppRouter.get('/status', (_req, res) => {
|
|
|
27
27
|
});
|
|
28
28
|
/**
|
|
29
29
|
* GET /api/github-app/repos
|
|
30
|
-
* List repositories the user has
|
|
30
|
+
* List repositories the user has access to
|
|
31
|
+
*
|
|
32
|
+
* First tries database (populated by GitHub App OAuth).
|
|
33
|
+
* If empty, queries GitHub directly via user OAuth connection.
|
|
31
34
|
*/
|
|
32
35
|
githubAppRouter.get('/repos', async (req, res) => {
|
|
33
36
|
const userId = req.session.userId;
|
|
34
37
|
try {
|
|
35
|
-
|
|
38
|
+
// Try database first (from GitHub App OAuth flow)
|
|
39
|
+
const dbRepos = await db.repositories.findByUserId(userId);
|
|
40
|
+
if (dbRepos.length > 0) {
|
|
41
|
+
// Return repos from database
|
|
42
|
+
return res.json({
|
|
43
|
+
repositories: dbRepos.map((r) => ({
|
|
44
|
+
id: r.id,
|
|
45
|
+
fullName: r.githubFullName,
|
|
46
|
+
isPrivate: r.isPrivate,
|
|
47
|
+
defaultBranch: r.defaultBranch,
|
|
48
|
+
syncStatus: r.syncStatus,
|
|
49
|
+
hasNangoConnection: !!r.nangoConnectionId,
|
|
50
|
+
lastSyncedAt: r.lastSyncedAt,
|
|
51
|
+
})),
|
|
52
|
+
source: 'database',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Database empty - query GitHub directly via user OAuth
|
|
56
|
+
const user = await db.users.findById(userId);
|
|
57
|
+
if (!user?.nangoConnectionId) {
|
|
58
|
+
return res.json({
|
|
59
|
+
repositories: [],
|
|
60
|
+
source: 'none',
|
|
61
|
+
hint: 'User not connected to GitHub',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
console.log(`[github-app/repos] Database empty, querying GitHub for user ${user.githubUsername}`);
|
|
65
|
+
const { repositories } = await nangoService.listUserAccessibleRepos(user.nangoConnectionId, {
|
|
66
|
+
perPage: 100,
|
|
67
|
+
type: 'all',
|
|
68
|
+
});
|
|
36
69
|
res.json({
|
|
37
|
-
repositories:
|
|
38
|
-
id:
|
|
39
|
-
fullName: r.
|
|
70
|
+
repositories: repositories.map((r) => ({
|
|
71
|
+
id: null, // No database ID yet
|
|
72
|
+
fullName: r.fullName,
|
|
40
73
|
isPrivate: r.isPrivate,
|
|
41
74
|
defaultBranch: r.defaultBranch,
|
|
42
|
-
syncStatus:
|
|
43
|
-
hasNangoConnection:
|
|
44
|
-
lastSyncedAt:
|
|
75
|
+
syncStatus: 'live', // Queried from GitHub, not cached
|
|
76
|
+
hasNangoConnection: true,
|
|
77
|
+
lastSyncedAt: null,
|
|
45
78
|
})),
|
|
79
|
+
source: 'github-api',
|
|
46
80
|
});
|
|
47
81
|
}
|
|
48
82
|
catch (error) {
|
|
@@ -160,27 +160,17 @@ nangoAuthRouter.get('/repo-status/:connectionId', requireAuth, async (req, res)
|
|
|
160
160
|
* Handle Nango webhooks for auth and sync events
|
|
161
161
|
*/
|
|
162
162
|
nangoAuthRouter.post('/webhook', async (req, res) => {
|
|
163
|
-
// Use the preserved raw body from express.json verify callback
|
|
164
163
|
const rawBody = req.rawBody || JSON.stringify(req.body);
|
|
165
|
-
// Verify signature
|
|
164
|
+
// Verify webhook signature if present
|
|
166
165
|
const hasSignature = req.headers['x-nango-signature'] || req.headers['x-nango-hmac-sha256'];
|
|
167
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
168
166
|
if (hasSignature) {
|
|
169
167
|
if (!nangoService.verifyWebhookSignature(rawBody, req.headers)) {
|
|
170
168
|
console.error('[nango-webhook] Invalid signature');
|
|
171
169
|
return res.status(401).json({ error: 'Invalid signature' });
|
|
172
170
|
}
|
|
173
|
-
console.log('[nango-webhook] Signature verified');
|
|
174
|
-
}
|
|
175
|
-
else if (!isDev) {
|
|
176
|
-
console.error('[nango-webhook] Missing signature in production');
|
|
177
|
-
return res.status(401).json({ error: 'Missing signature' });
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
console.warn('[nango-webhook] Skipping signature verification in development (no signature)');
|
|
181
171
|
}
|
|
182
172
|
const payload = req.body;
|
|
183
|
-
console.log(`[nango-webhook] Received ${payload.type} event
|
|
173
|
+
console.log(`[nango-webhook] Received ${payload.type} event`);
|
|
184
174
|
try {
|
|
185
175
|
switch (payload.type) {
|
|
186
176
|
case 'auth':
|
|
@@ -190,8 +180,7 @@ nangoAuthRouter.post('/webhook', async (req, res) => {
|
|
|
190
180
|
console.log('[nango-webhook] Sync event received');
|
|
191
181
|
break;
|
|
192
182
|
case 'forward':
|
|
193
|
-
|
|
194
|
-
console.log('[nango-webhook] Forward event from provider (ignored)');
|
|
183
|
+
await handleForwardWebhook(payload);
|
|
195
184
|
break;
|
|
196
185
|
default:
|
|
197
186
|
console.log(`[nango-webhook] Unhandled event type: ${payload.type}`);
|
|
@@ -216,6 +205,72 @@ async function handleAuthWebhook(payload) {
|
|
|
216
205
|
await handleRepoAuthWebhook(connectionId, endUser);
|
|
217
206
|
}
|
|
218
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Check user's repo access and auto-add them to workspaces
|
|
210
|
+
* Uses GitHub user OAuth to query accessible repos and persists them to database
|
|
211
|
+
*/
|
|
212
|
+
async function checkAndAutoAddToWorkspaces(userId, connectionId) {
|
|
213
|
+
try {
|
|
214
|
+
const user = await db.users.findById(userId);
|
|
215
|
+
if (!user)
|
|
216
|
+
return;
|
|
217
|
+
console.log(`[nango-webhook] Checking workspace auto-add for ${user.githubUsername}`);
|
|
218
|
+
// Query repos the user has access to via GitHub OAuth
|
|
219
|
+
const { repositories } = await nangoService.listUserAccessibleRepos(connectionId, {
|
|
220
|
+
perPage: 100,
|
|
221
|
+
type: 'all',
|
|
222
|
+
});
|
|
223
|
+
const workspacesToJoin = new Set();
|
|
224
|
+
// Check for workspace memberships - only persist repos that match existing workspaces
|
|
225
|
+
for (const repo of repositories) {
|
|
226
|
+
// Check if any user has this repo linked to a workspace
|
|
227
|
+
const allRepoRecords = await db.repositories.findByGithubFullName(repo.fullName);
|
|
228
|
+
let matchedWorkspaceId = null;
|
|
229
|
+
for (const record of allRepoRecords) {
|
|
230
|
+
if (record.workspaceId) {
|
|
231
|
+
workspacesToJoin.add(record.workspaceId);
|
|
232
|
+
matchedWorkspaceId = record.workspaceId; // Save the workspaceId to copy
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Only persist repos that are linked to workspaces
|
|
236
|
+
if (matchedWorkspaceId) {
|
|
237
|
+
await db.repositories.upsert({
|
|
238
|
+
userId: user.id,
|
|
239
|
+
githubFullName: repo.fullName,
|
|
240
|
+
githubId: repo.id,
|
|
241
|
+
isPrivate: repo.isPrivate,
|
|
242
|
+
defaultBranch: repo.defaultBranch,
|
|
243
|
+
nangoConnectionId: connectionId,
|
|
244
|
+
workspaceId: matchedWorkspaceId, // Copy the workspaceId
|
|
245
|
+
syncStatus: 'synced',
|
|
246
|
+
lastSyncedAt: new Date(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Auto-add user to workspaces
|
|
251
|
+
for (const workspaceId of workspacesToJoin) {
|
|
252
|
+
const existingMembership = await db.workspaceMembers.findMembership(workspaceId, userId);
|
|
253
|
+
if (!existingMembership) {
|
|
254
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
255
|
+
if (workspace) {
|
|
256
|
+
console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
|
|
257
|
+
await db.workspaceMembers.addMember({
|
|
258
|
+
workspaceId,
|
|
259
|
+
userId,
|
|
260
|
+
role: 'member',
|
|
261
|
+
invitedBy: workspace.userId,
|
|
262
|
+
});
|
|
263
|
+
await db.workspaceMembers.acceptInvite(workspaceId, userId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
console.log(`[nango-webhook] Synced ${repositories.length} repos, auto-added ${user.githubUsername} to ${workspacesToJoin.size} workspaces`);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
console.error(`[nango-webhook] Error checking workspace auto-add:`, error);
|
|
271
|
+
// Non-fatal - don't throw
|
|
272
|
+
}
|
|
273
|
+
}
|
|
219
274
|
/**
|
|
220
275
|
* Handle GitHub login webhook
|
|
221
276
|
*
|
|
@@ -246,6 +301,8 @@ async function handleLoginWebhook(connectionId, _endUser) {
|
|
|
246
301
|
email: newUser.email || undefined,
|
|
247
302
|
});
|
|
248
303
|
console.log(`[nango-webhook] New user created: ${githubUser.login}`);
|
|
304
|
+
// Check for auto-add to workspaces based on repo access
|
|
305
|
+
await checkAndAutoAddToWorkspaces(newUser.id, connectionId);
|
|
249
306
|
return;
|
|
250
307
|
}
|
|
251
308
|
// SCENARIO 2: Returning user with existing connection - delete temp connection
|
|
@@ -269,6 +326,8 @@ async function handleLoginWebhook(connectionId, _endUser) {
|
|
|
269
326
|
console.error(`[nango-webhook] Failed to delete temp connection:`, error);
|
|
270
327
|
// Non-fatal - continue anyway
|
|
271
328
|
}
|
|
329
|
+
// Check for auto-add using permanent connection
|
|
330
|
+
await checkAndAutoAddToWorkspaces(existingUser.id, existingUser.nangoConnectionId);
|
|
272
331
|
return;
|
|
273
332
|
}
|
|
274
333
|
// SCENARIO 3: Existing user, first connection (or same connection)
|
|
@@ -284,6 +343,192 @@ async function handleLoginWebhook(connectionId, _endUser) {
|
|
|
284
343
|
id: existingUser.id,
|
|
285
344
|
email: existingUser.email || undefined,
|
|
286
345
|
});
|
|
346
|
+
// Check for auto-add to workspaces
|
|
347
|
+
await checkAndAutoAddToWorkspaces(existingUser.id, connectionId);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Handle Nango forward webhook (GitHub events forwarded by Nango)
|
|
351
|
+
*/
|
|
352
|
+
async function handleForwardWebhook(payload) {
|
|
353
|
+
const githubPayload = payload.payload;
|
|
354
|
+
console.log(`[nango-webhook] Forward event: action=${githubPayload.action} from ${payload.providerConfigKey}`);
|
|
355
|
+
// Only process GitHub App events
|
|
356
|
+
if (payload.providerConfigKey !== NANGO_INTEGRATIONS.GITHUB_APP) {
|
|
357
|
+
console.log('[nango-webhook] Ignoring forward event from non-GitHub-App integration');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
// Determine event type from payload structure
|
|
362
|
+
if (githubPayload.installation && githubPayload.action === 'created' && githubPayload.repositories) {
|
|
363
|
+
// Installation created event
|
|
364
|
+
await handleInstallationForward(githubPayload, payload.connectionId);
|
|
365
|
+
}
|
|
366
|
+
else if (githubPayload.repositories_added || githubPayload.repositories_removed) {
|
|
367
|
+
// Installation repositories added/removed
|
|
368
|
+
await handleInstallationRepositoriesForward(githubPayload, payload.connectionId);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(`[nango-webhook] Unhandled forward event structure: action=${githubPayload.action}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
console.error(`[nango-webhook] Error processing forward event:`, error);
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Handle GitHub installation events forwarded by Nango
|
|
381
|
+
*/
|
|
382
|
+
async function handleInstallationForward(body, connectionId) {
|
|
383
|
+
const { action, installation, repositories, sender } = body;
|
|
384
|
+
if (!installation || !sender)
|
|
385
|
+
return;
|
|
386
|
+
const installationId = String(installation.id);
|
|
387
|
+
console.log(`[nango-webhook] Installation ${action}: ${installation.account.login} (${installationId})`);
|
|
388
|
+
if (action === 'created') {
|
|
389
|
+
// Find user by GitHub ID
|
|
390
|
+
const user = await db.users.findByGithubId(String(sender.id));
|
|
391
|
+
// Create/update installation record
|
|
392
|
+
await db.githubInstallations.upsert({
|
|
393
|
+
installationId,
|
|
394
|
+
accountType: installation.account.type.toLowerCase(),
|
|
395
|
+
accountLogin: installation.account.login,
|
|
396
|
+
accountId: String(installation.account.id),
|
|
397
|
+
installedById: user?.id ?? null,
|
|
398
|
+
permissions: installation.permissions,
|
|
399
|
+
events: installation.events,
|
|
400
|
+
});
|
|
401
|
+
// Sync repositories if provided
|
|
402
|
+
if (repositories && user) {
|
|
403
|
+
const dbInstallation = await db.githubInstallations.findByInstallationId(installationId);
|
|
404
|
+
if (dbInstallation) {
|
|
405
|
+
const workspacesToJoin = new Set();
|
|
406
|
+
for (const repo of repositories) {
|
|
407
|
+
const syncedRepo = await db.repositories.upsert({
|
|
408
|
+
userId: user.id,
|
|
409
|
+
githubFullName: repo.full_name,
|
|
410
|
+
githubId: repo.id,
|
|
411
|
+
isPrivate: repo.private,
|
|
412
|
+
installationId: dbInstallation.id,
|
|
413
|
+
nangoConnectionId: connectionId,
|
|
414
|
+
syncStatus: 'synced',
|
|
415
|
+
lastSyncedAt: new Date(),
|
|
416
|
+
});
|
|
417
|
+
// Check if repo is part of an existing workspace
|
|
418
|
+
// Look for ANY user's record of this repo that has a workspaceId
|
|
419
|
+
if (syncedRepo.workspaceId) {
|
|
420
|
+
workspacesToJoin.add(syncedRepo.workspaceId);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Check if other users have this repo linked to a workspace
|
|
424
|
+
const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
|
|
425
|
+
for (const otherRecord of allRepoRecords) {
|
|
426
|
+
if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
|
|
427
|
+
workspacesToJoin.add(otherRecord.workspaceId);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Auto-join user to workspaces for repos they have access to
|
|
433
|
+
for (const workspaceId of workspacesToJoin) {
|
|
434
|
+
const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
|
|
435
|
+
if (!existingMembership) {
|
|
436
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
437
|
+
if (workspace) {
|
|
438
|
+
console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
|
|
439
|
+
await db.workspaceMembers.addMember({
|
|
440
|
+
workspaceId,
|
|
441
|
+
userId: user.id,
|
|
442
|
+
role: 'member',
|
|
443
|
+
invitedBy: workspace.userId,
|
|
444
|
+
});
|
|
445
|
+
await db.workspaceMembers.acceptInvite(workspaceId, user.id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
console.log(`[nango-webhook] Installation created for ${installation.account.login}, auto-joined ${workspacesToJoin.size} workspaces`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Handle installation_repositories events forwarded by Nango
|
|
456
|
+
*/
|
|
457
|
+
async function handleInstallationRepositoriesForward(body, connectionId) {
|
|
458
|
+
const { action, installation, repositories_added, repositories_removed, sender } = body;
|
|
459
|
+
if (!installation || !sender)
|
|
460
|
+
return;
|
|
461
|
+
const installationId = String(installation.id);
|
|
462
|
+
console.log(`[nango-webhook] Repositories ${action} for ${installation.account.login}`);
|
|
463
|
+
// Find installation in database
|
|
464
|
+
const dbInstallation = await db.githubInstallations.findByInstallationId(installationId);
|
|
465
|
+
if (!dbInstallation) {
|
|
466
|
+
console.error(`[nango-webhook] Installation ${installationId} not found in database`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// Find user who triggered this
|
|
470
|
+
const user = await db.users.findByGithubId(String(sender.id));
|
|
471
|
+
if (!user) {
|
|
472
|
+
console.error(`[nango-webhook] User ${sender.login} not found in database`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (action === 'added' && repositories_added) {
|
|
476
|
+
const workspacesToJoin = new Set();
|
|
477
|
+
for (const repo of repositories_added) {
|
|
478
|
+
const syncedRepo = await db.repositories.upsert({
|
|
479
|
+
userId: user.id,
|
|
480
|
+
githubFullName: repo.full_name,
|
|
481
|
+
githubId: repo.id,
|
|
482
|
+
isPrivate: repo.private,
|
|
483
|
+
installationId: dbInstallation.id,
|
|
484
|
+
nangoConnectionId: connectionId,
|
|
485
|
+
syncStatus: 'synced',
|
|
486
|
+
lastSyncedAt: new Date(),
|
|
487
|
+
});
|
|
488
|
+
// Check if repo is part of an existing workspace
|
|
489
|
+
// Look for ANY user's record of this repo that has a workspaceId
|
|
490
|
+
if (syncedRepo.workspaceId) {
|
|
491
|
+
workspacesToJoin.add(syncedRepo.workspaceId);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Check if other users have this repo linked to a workspace
|
|
495
|
+
const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
|
|
496
|
+
for (const otherRecord of allRepoRecords) {
|
|
497
|
+
if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
|
|
498
|
+
workspacesToJoin.add(otherRecord.workspaceId);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Auto-join user to workspaces for repos they have access to
|
|
504
|
+
for (const workspaceId of workspacesToJoin) {
|
|
505
|
+
const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
|
|
506
|
+
if (!existingMembership) {
|
|
507
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
508
|
+
if (workspace) {
|
|
509
|
+
console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
|
|
510
|
+
await db.workspaceMembers.addMember({
|
|
511
|
+
workspaceId,
|
|
512
|
+
userId: user.id,
|
|
513
|
+
role: 'member',
|
|
514
|
+
invitedBy: workspace.userId,
|
|
515
|
+
});
|
|
516
|
+
await db.workspaceMembers.acceptInvite(workspaceId, user.id);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
console.log(`[nango-webhook] Added ${repositories_added.length} repositories, auto-joined ${workspacesToJoin.size} workspaces`);
|
|
521
|
+
}
|
|
522
|
+
if (action === 'removed' && repositories_removed) {
|
|
523
|
+
for (const repo of repositories_removed) {
|
|
524
|
+
const repos = await db.repositories.findByUserId(user.id);
|
|
525
|
+
const existingRepo = repos.find(r => r.githubFullName === repo.full_name);
|
|
526
|
+
if (existingRepo) {
|
|
527
|
+
await db.repositories.updateSyncStatus(existingRepo.id, 'access_removed');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
console.log(`[nango-webhook] Removed access to ${repositories_removed.length} repositories`);
|
|
531
|
+
}
|
|
287
532
|
}
|
|
288
533
|
/**
|
|
289
534
|
* Handle GitHub App OAuth webhook (repo access)
|
|
@@ -339,9 +584,11 @@ async function handleRepoAuthWebhook(connectionId, endUser) {
|
|
|
339
584
|
}
|
|
340
585
|
// Fetch repos the user has access to
|
|
341
586
|
const { repositories: repos } = await nangoService.listGithubAppRepos(connectionId);
|
|
587
|
+
// Track workspaces to auto-join
|
|
588
|
+
const workspacesToJoin = new Set();
|
|
342
589
|
// Sync repos to database
|
|
343
590
|
for (const repo of repos) {
|
|
344
|
-
await db.repositories.upsert({
|
|
591
|
+
const syncedRepo = await db.repositories.upsert({
|
|
345
592
|
userId: user.id,
|
|
346
593
|
githubFullName: repo.full_name,
|
|
347
594
|
githubId: repo.id,
|
|
@@ -352,10 +599,44 @@ async function handleRepoAuthWebhook(connectionId, endUser) {
|
|
|
352
599
|
syncStatus: 'synced',
|
|
353
600
|
lastSyncedAt: new Date(),
|
|
354
601
|
});
|
|
602
|
+
// Check if this repo is part of an existing workspace
|
|
603
|
+
// Look for ANY user's record of this repo that has a workspaceId
|
|
604
|
+
if (syncedRepo.workspaceId) {
|
|
605
|
+
workspacesToJoin.add(syncedRepo.workspaceId);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// Check if other users have this repo linked to a workspace
|
|
609
|
+
const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
|
|
610
|
+
for (const otherRecord of allRepoRecords) {
|
|
611
|
+
if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
|
|
612
|
+
workspacesToJoin.add(otherRecord.workspaceId);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Auto-join user to workspaces for repos they have access to
|
|
618
|
+
for (const workspaceId of workspacesToJoin) {
|
|
619
|
+
// Check if already a member
|
|
620
|
+
const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
|
|
621
|
+
if (!existingMembership) {
|
|
622
|
+
// Get workspace owner to use as invitedBy
|
|
623
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
624
|
+
if (workspace) {
|
|
625
|
+
console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
|
|
626
|
+
await db.workspaceMembers.addMember({
|
|
627
|
+
workspaceId,
|
|
628
|
+
userId: user.id,
|
|
629
|
+
role: 'member',
|
|
630
|
+
invitedBy: workspace.userId, // Workspace owner invited them
|
|
631
|
+
});
|
|
632
|
+
// Auto-accept since they have GitHub repo access
|
|
633
|
+
await db.workspaceMembers.acceptInvite(workspaceId, user.id);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
355
636
|
}
|
|
356
637
|
// Clear any pending installation request
|
|
357
638
|
await db.users.clearPendingInstallationRequest(user.id);
|
|
358
|
-
console.log(`[nango-webhook] Synced ${repos.length} repos for ${user.githubUsername} (installation: ${githubInstallationId || 'unknown'})`);
|
|
639
|
+
console.log(`[nango-webhook] Synced ${repos.length} repos for ${user.githubUsername} (installation: ${githubInstallationId || 'unknown'}), auto-joined ${workspacesToJoin.size} workspaces`);
|
|
359
640
|
// Note: We intentionally do NOT auto-provision workspaces here.
|
|
360
641
|
// Users should go through the onboarding flow at /app to:
|
|
361
642
|
// 1. Name their workspace
|
|
@@ -13,7 +13,6 @@ import { Router } from 'express';
|
|
|
13
13
|
import * as crypto from 'crypto';
|
|
14
14
|
import { requireAuth } from './auth.js';
|
|
15
15
|
import { db } from '../db/index.js';
|
|
16
|
-
import { vault } from '../vault/index.js';
|
|
17
16
|
// Import for local use
|
|
18
17
|
import { CLI_AUTH_CONFIG, runCLIAuthViaPTY, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, validateProviderConfig, validateAllProviderConfigs, getSupportedProviders, } from './cli-pty-runner.js';
|
|
19
18
|
// Re-export from shared module for backward compatibility
|
|
@@ -56,9 +55,12 @@ onboardingRouter.post('/cli/:provider/start', async (req, res) => {
|
|
|
56
55
|
console.log('[onboarding] Route handler entered! provider:', req.params.provider);
|
|
57
56
|
const { provider } = req.params;
|
|
58
57
|
const userId = req.session.userId;
|
|
59
|
-
const { workspaceId, useDeviceFlow } = req.body; // Optional: specific workspace, device flow option
|
|
60
|
-
|
|
58
|
+
const { workspaceId, useDeviceFlow: requestedDeviceFlow } = req.body; // Optional: specific workspace, device flow option
|
|
59
|
+
// Device flow is only used if explicitly requested by the client
|
|
60
|
+
// Standard flow: user runs `codex-auth` CLI locally to capture OAuth callback and forward to cloud
|
|
61
61
|
const config = CLI_AUTH_CONFIG[provider];
|
|
62
|
+
const useDeviceFlow = requestedDeviceFlow ?? false;
|
|
63
|
+
console.log('[onboarding] userId:', userId, 'workspaceId:', workspaceId, 'useDeviceFlow:', useDeviceFlow);
|
|
62
64
|
if (!config) {
|
|
63
65
|
return res.status(400).json({
|
|
64
66
|
error: 'Provider not supported for CLI auth',
|
|
@@ -153,6 +155,7 @@ onboardingRouter.post('/cli/:provider/start', async (req, res) => {
|
|
|
153
155
|
status: session.status,
|
|
154
156
|
authUrl: session.authUrl,
|
|
155
157
|
workspaceId: workspace.id,
|
|
158
|
+
useDeviceFlow, // Tell dashboard whether device flow is being used (no CLI helper needed)
|
|
156
159
|
message: session.authUrl ? 'Open the auth URL to complete login' : 'Auth session starting, poll for status',
|
|
157
160
|
});
|
|
158
161
|
}
|
|
@@ -185,6 +188,8 @@ onboardingRouter.get('/cli/:provider/status/:sessionId', async (req, res) => {
|
|
|
185
188
|
session.status = workspaceStatus.status || session.status;
|
|
186
189
|
session.authUrl = workspaceStatus.authUrl || session.authUrl;
|
|
187
190
|
session.error = workspaceStatus.error;
|
|
191
|
+
session.errorHint = workspaceStatus.errorHint;
|
|
192
|
+
session.recoverable = workspaceStatus.recoverable;
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
catch (err) {
|
|
@@ -195,6 +200,8 @@ onboardingRouter.get('/cli/:provider/status/:sessionId', async (req, res) => {
|
|
|
195
200
|
status: session.status,
|
|
196
201
|
authUrl: session.authUrl,
|
|
197
202
|
error: session.error,
|
|
203
|
+
errorHint: session.errorHint,
|
|
204
|
+
recoverable: session.recoverable,
|
|
198
205
|
});
|
|
199
206
|
});
|
|
200
207
|
/**
|
|
@@ -220,7 +227,7 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req, res) =>
|
|
|
220
227
|
try {
|
|
221
228
|
let accessToken = token || session.token;
|
|
222
229
|
let refreshToken = session.refreshToken;
|
|
223
|
-
let
|
|
230
|
+
let _tokenExpiresAt = session.tokenExpiresAt;
|
|
224
231
|
// If using workspace delegation, forward complete request first
|
|
225
232
|
if (session.workspaceUrl && session.workspaceSessionId) {
|
|
226
233
|
// Forward authCode to workspace if provided (for Codex-style redirects)
|
|
@@ -241,25 +248,52 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req, res) =>
|
|
|
241
248
|
}
|
|
242
249
|
session.status = 'success';
|
|
243
250
|
}
|
|
244
|
-
// Fetch credentials from workspace
|
|
251
|
+
// Fetch credentials from workspace with retry
|
|
252
|
+
// Credentials may not be immediately available after OAuth completes
|
|
245
253
|
if (!accessToken) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
+
const MAX_CREDS_RETRIES = 5;
|
|
255
|
+
const CREDS_RETRY_DELAY = 1000; // 1 second between retries
|
|
256
|
+
for (let attempt = 1; attempt <= MAX_CREDS_RETRIES; attempt++) {
|
|
257
|
+
try {
|
|
258
|
+
console.log(`[onboarding] Fetching credentials from workspace (attempt ${attempt}/${MAX_CREDS_RETRIES})`);
|
|
259
|
+
const credsResponse = await fetch(`${session.workspaceUrl}/auth/cli/${provider}/creds/${session.workspaceSessionId}`);
|
|
260
|
+
if (credsResponse.ok) {
|
|
261
|
+
const creds = await credsResponse.json();
|
|
262
|
+
accessToken = creds.token;
|
|
263
|
+
refreshToken = creds.refreshToken;
|
|
264
|
+
if (creds.tokenExpiresAt) {
|
|
265
|
+
_tokenExpiresAt = new Date(creds.tokenExpiresAt);
|
|
266
|
+
}
|
|
267
|
+
console.log('[onboarding] Fetched credentials from workspace:', {
|
|
268
|
+
hasToken: !!accessToken,
|
|
269
|
+
hasRefreshToken: !!refreshToken,
|
|
270
|
+
attempt,
|
|
271
|
+
});
|
|
272
|
+
break; // Success, exit retry loop
|
|
273
|
+
}
|
|
274
|
+
// Check if it's an error state (not just "not ready yet")
|
|
275
|
+
const errorBody = await credsResponse.json().catch(() => ({}));
|
|
276
|
+
if (errorBody.status === 'error') {
|
|
277
|
+
// Auth failed, don't retry
|
|
278
|
+
console.error('[onboarding] Auth failed in workspace:', errorBody);
|
|
279
|
+
return res.status(400).json({
|
|
280
|
+
error: errorBody.error || 'Authentication failed',
|
|
281
|
+
errorHint: errorBody.errorHint,
|
|
282
|
+
recoverable: errorBody.recoverable,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// If not ready yet and we have more retries, wait and try again
|
|
286
|
+
if (attempt < MAX_CREDS_RETRIES) {
|
|
287
|
+
console.log(`[onboarding] Credentials not ready yet, retrying in ${CREDS_RETRY_DELAY}ms...`);
|
|
288
|
+
await new Promise(resolve => setTimeout(resolve, CREDS_RETRY_DELAY));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
console.error(`[onboarding] Failed to get credentials from workspace (attempt ${attempt}):`, err);
|
|
293
|
+
if (attempt < MAX_CREDS_RETRIES) {
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, CREDS_RETRY_DELAY));
|
|
254
295
|
}
|
|
255
|
-
console.log('[onboarding] Fetched credentials from workspace:', {
|
|
256
|
-
hasToken: !!accessToken,
|
|
257
|
-
hasRefreshToken: !!refreshToken,
|
|
258
|
-
});
|
|
259
296
|
}
|
|
260
|
-
}
|
|
261
|
-
catch (err) {
|
|
262
|
-
console.error('[onboarding] Failed to get credentials from workspace:', err);
|
|
263
297
|
}
|
|
264
298
|
}
|
|
265
299
|
}
|
|
@@ -268,13 +302,11 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req, res) =>
|
|
|
268
302
|
error: 'No token found. Please complete authentication or paste your token.',
|
|
269
303
|
});
|
|
270
304
|
}
|
|
271
|
-
//
|
|
272
|
-
|
|
305
|
+
// Mark provider as connected (tokens are not stored centrally - CLI tools
|
|
306
|
+
// authenticate directly on workspace instances)
|
|
307
|
+
await db.credentials.upsert({
|
|
273
308
|
userId,
|
|
274
309
|
provider,
|
|
275
|
-
accessToken,
|
|
276
|
-
refreshToken,
|
|
277
|
-
tokenExpiresAt,
|
|
278
310
|
scopes: getProviderScopes(provider),
|
|
279
311
|
});
|
|
280
312
|
// Clean up session
|
|
@@ -297,8 +329,8 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req, res) =>
|
|
|
297
329
|
onboardingRouter.post('/cli/:provider/code/:sessionId', async (req, res) => {
|
|
298
330
|
const { provider, sessionId } = req.params;
|
|
299
331
|
const userId = req.session.userId;
|
|
300
|
-
const { code } = req.body;
|
|
301
|
-
console.log('[onboarding] Auth code submission request:', { provider, sessionId, codeLength: code?.length });
|
|
332
|
+
const { code, state } = req.body; // state is optional, used for Codex OAuth
|
|
333
|
+
console.log('[onboarding] Auth code submission request:', { provider, sessionId, codeLength: code?.length, hasState: !!state });
|
|
302
334
|
if (!code || typeof code !== 'string') {
|
|
303
335
|
return res.status(400).json({ error: 'Auth code is required' });
|
|
304
336
|
}
|
|
@@ -324,7 +356,7 @@ onboardingRouter.post('/cli/:provider/code/:sessionId', async (req, res) => {
|
|
|
324
356
|
const codeResponse = await fetch(targetUrl, {
|
|
325
357
|
method: 'POST',
|
|
326
358
|
headers: { 'Content-Type': 'application/json' },
|
|
327
|
-
body: JSON.stringify({ code }),
|
|
359
|
+
body: JSON.stringify({ code, state }), // Forward state for Codex CSRF validation
|
|
328
360
|
});
|
|
329
361
|
console.log('[onboarding] Workspace response:', { status: codeResponse.status });
|
|
330
362
|
if (codeResponse.ok) {
|
|
@@ -381,6 +413,37 @@ onboardingRouter.post('/cli/:provider/cancel/:sessionId', async (req, res) => {
|
|
|
381
413
|
}
|
|
382
414
|
res.json({ success: true });
|
|
383
415
|
});
|
|
416
|
+
/**
|
|
417
|
+
* POST /api/onboarding/mark-connected/:provider
|
|
418
|
+
* Mark a provider as connected without storing a token.
|
|
419
|
+
* Used by terminal-based setup where the CLI stores credentials locally.
|
|
420
|
+
*/
|
|
421
|
+
onboardingRouter.post('/mark-connected/:provider', async (req, res) => {
|
|
422
|
+
const { provider } = req.params;
|
|
423
|
+
const userId = req.session.userId;
|
|
424
|
+
// Validate provider
|
|
425
|
+
const validProviders = ['anthropic', 'openai', 'google', 'github'];
|
|
426
|
+
if (!validProviders.includes(provider)) {
|
|
427
|
+
return res.status(400).json({ error: 'Invalid provider' });
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
// Mark provider as connected (tokens are stored by CLI on workspace)
|
|
431
|
+
await db.credentials.upsert({
|
|
432
|
+
userId,
|
|
433
|
+
provider,
|
|
434
|
+
scopes: getProviderScopes(provider),
|
|
435
|
+
});
|
|
436
|
+
console.log(`[onboarding] Marked ${provider} as connected for user ${userId}`);
|
|
437
|
+
res.json({
|
|
438
|
+
success: true,
|
|
439
|
+
message: `${provider} connected successfully`,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.error(`Error marking ${provider} as connected:`, error);
|
|
444
|
+
res.status(500).json({ error: 'Failed to mark provider as connected' });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
384
447
|
/**
|
|
385
448
|
* POST /api/onboarding/token/:provider
|
|
386
449
|
* Directly store a token (for manual paste flow)
|
|
@@ -398,22 +461,23 @@ onboardingRouter.post('/token/:provider', async (req, res) => {
|
|
|
398
461
|
if (!isValid) {
|
|
399
462
|
return res.status(400).json({ error: 'Invalid token' });
|
|
400
463
|
}
|
|
401
|
-
//
|
|
402
|
-
|
|
464
|
+
// Mark provider as connected (tokens are not stored centrally - CLI tools
|
|
465
|
+
// authenticate directly on workspace instances)
|
|
466
|
+
await db.credentials.upsert({
|
|
403
467
|
userId,
|
|
404
468
|
provider,
|
|
405
|
-
accessToken: token,
|
|
406
469
|
scopes: getProviderScopes(provider),
|
|
407
470
|
providerAccountEmail: email,
|
|
408
471
|
});
|
|
409
472
|
res.json({
|
|
410
473
|
success: true,
|
|
411
474
|
message: `${provider} connected successfully`,
|
|
475
|
+
note: 'Token validated. Configure this on your workspace for usage.',
|
|
412
476
|
});
|
|
413
477
|
}
|
|
414
478
|
catch (error) {
|
|
415
|
-
console.error(`Error storing
|
|
416
|
-
res.status(500).json({ error: 'Failed to store
|
|
479
|
+
console.error(`Error storing provider connection for ${provider}:`, error);
|
|
480
|
+
res.status(500).json({ error: 'Failed to store provider connection' });
|
|
417
481
|
}
|
|
418
482
|
});
|
|
419
483
|
/**
|