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
|
@@ -9,7 +9,6 @@ import { createClient } from 'redis';
|
|
|
9
9
|
import { requireAuth } from './auth.js';
|
|
10
10
|
import { getConfig } from '../config.js';
|
|
11
11
|
import { db } from '../db/index.js';
|
|
12
|
-
import { vault } from '../vault/index.js';
|
|
13
12
|
export const providersRouter = Router();
|
|
14
13
|
// All routes require authentication
|
|
15
14
|
providersRouter.use(requireAuth);
|
|
@@ -243,19 +242,18 @@ providersRouter.post('/:provider/verify', async (req, res) => {
|
|
|
243
242
|
// For self-hosted: we trust the user completed the CLI flow
|
|
244
243
|
// In production, we'd verify by making a test API call with the credentials
|
|
245
244
|
try {
|
|
246
|
-
//
|
|
247
|
-
//
|
|
245
|
+
// Mark as connected (tokens are not stored centrally - CLI tools
|
|
246
|
+
// authenticate directly on workspace instances)
|
|
248
247
|
await db.credentials.upsert({
|
|
249
248
|
userId,
|
|
250
249
|
provider,
|
|
251
|
-
accessToken: 'cli-authenticated', // Placeholder - real token from CLI
|
|
252
250
|
scopes: [], // CLI auth doesn't use scopes
|
|
253
251
|
providerAccountEmail: req.body.email, // User can optionally provide
|
|
254
252
|
});
|
|
255
253
|
res.json({
|
|
256
254
|
success: true,
|
|
257
255
|
message: `${providerConfig.displayName} connected via CLI`,
|
|
258
|
-
note: '
|
|
256
|
+
note: 'CLI credentials remain on your local machine',
|
|
259
257
|
});
|
|
260
258
|
}
|
|
261
259
|
catch (error) {
|
|
@@ -306,17 +304,18 @@ providersRouter.post('/:provider/api-key', async (req, res) => {
|
|
|
306
304
|
if (!isValid) {
|
|
307
305
|
return res.status(400).json({ error: 'Invalid API key' });
|
|
308
306
|
}
|
|
309
|
-
//
|
|
307
|
+
// Mark provider as connected (tokens are not stored centrally - CLI tools
|
|
308
|
+
// authenticate directly on workspace instances)
|
|
310
309
|
const scopes = isDeviceFlowProvider(providerConfig) ? providerConfig.scopes : [];
|
|
311
|
-
await
|
|
310
|
+
await db.credentials.upsert({
|
|
312
311
|
userId,
|
|
313
312
|
provider,
|
|
314
|
-
accessToken: apiKey,
|
|
315
313
|
scopes,
|
|
316
314
|
});
|
|
317
315
|
res.json({
|
|
318
316
|
success: true,
|
|
319
317
|
message: `${providerConfig.displayName} connected`,
|
|
318
|
+
note: 'API key validated. Configure this key on your workspace for usage.',
|
|
320
319
|
});
|
|
321
320
|
}
|
|
322
321
|
catch (error) {
|
|
@@ -469,7 +468,9 @@ async function pollForToken(flowId, provider, clientId) {
|
|
|
469
468
|
.catch((err) => console.error('Poll start error:', err));
|
|
470
469
|
}
|
|
471
470
|
/**
|
|
472
|
-
*
|
|
471
|
+
* Mark provider as connected after successful device flow
|
|
472
|
+
* Note: Tokens are not stored centrally - CLI tools authenticate directly
|
|
473
|
+
* on workspace instances. We only record the connection status and user info.
|
|
473
474
|
*/
|
|
474
475
|
async function storeProviderTokens(userId, provider, tokens) {
|
|
475
476
|
const providerConfig = PROVIDERS[provider];
|
|
@@ -490,15 +491,10 @@ async function storeProviderTokens(userId, provider, tokens) {
|
|
|
490
491
|
console.error('Error fetching user info:', error);
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
|
-
//
|
|
494
|
-
await
|
|
494
|
+
// Mark provider as connected (without storing tokens)
|
|
495
|
+
await db.credentials.upsert({
|
|
495
496
|
userId,
|
|
496
497
|
provider,
|
|
497
|
-
accessToken: tokens.accessToken,
|
|
498
|
-
refreshToken: tokens.refreshToken,
|
|
499
|
-
tokenExpiresAt: tokens.expiresIn
|
|
500
|
-
? new Date(Date.now() + tokens.expiresIn * 1000)
|
|
501
|
-
: undefined,
|
|
502
498
|
scopes: tokens.scope?.split(' '),
|
|
503
499
|
providerAccountId: userInfo.id,
|
|
504
500
|
providerAccountEmail: userInfo.email,
|
package/dist/cloud/api/repos.js
CHANGED
|
@@ -4,10 +4,60 @@
|
|
|
4
4
|
* GitHub repository management - list, import, sync.
|
|
5
5
|
* Includes Nango-based GitHub permission checking for dashboard access control.
|
|
6
6
|
*/
|
|
7
|
+
import crypto from 'crypto';
|
|
7
8
|
import { Router } from 'express';
|
|
8
9
|
import { requireAuth } from './auth.js';
|
|
9
10
|
import { db } from '../db/index.js';
|
|
10
11
|
import { nangoService } from '../services/nango.js';
|
|
12
|
+
import { getConfig } from '../config.js';
|
|
13
|
+
/**
|
|
14
|
+
* Generate workspace token for API calls to workspace containers
|
|
15
|
+
*/
|
|
16
|
+
function generateWorkspaceToken(workspaceId) {
|
|
17
|
+
const config = getConfig();
|
|
18
|
+
return crypto
|
|
19
|
+
.createHmac('sha256', config.sessionSecret)
|
|
20
|
+
.update(`workspace:${workspaceId}`)
|
|
21
|
+
.digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Call workspace API endpoint
|
|
25
|
+
*/
|
|
26
|
+
async function callWorkspaceApi(publicUrl, workspaceId, method, endpoint, body) {
|
|
27
|
+
const token = generateWorkspaceToken(workspaceId);
|
|
28
|
+
const url = `${publicUrl.replace(/\/$/, '')}${endpoint}`;
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method,
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Authorization': `Bearer ${token}`,
|
|
35
|
+
},
|
|
36
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
37
|
+
});
|
|
38
|
+
const data = await response.json().catch((parseError) => {
|
|
39
|
+
console.error('Failed to parse JSON from workspace response', {
|
|
40
|
+
url,
|
|
41
|
+
status: response.status,
|
|
42
|
+
error: parseError instanceof Error ? parseError.message : parseError,
|
|
43
|
+
});
|
|
44
|
+
return null;
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
ok: response.ok,
|
|
48
|
+
status: response.status,
|
|
49
|
+
data,
|
|
50
|
+
error: response.ok ? undefined : (data?.error || `HTTP ${response.status}`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 0,
|
|
57
|
+
error: err instanceof Error ? err.message : 'Network error',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
11
61
|
export const reposRouter = Router();
|
|
12
62
|
// All routes require authentication
|
|
13
63
|
reposRouter.use(requireAuth);
|
|
@@ -190,83 +240,49 @@ reposRouter.post('/bulk', async (req, res) => {
|
|
|
190
240
|
});
|
|
191
241
|
});
|
|
192
242
|
/**
|
|
193
|
-
* GET /api/repos
|
|
194
|
-
*
|
|
243
|
+
* GET /api/repos/accessible
|
|
244
|
+
* List all GitHub repositories the authenticated user has access to.
|
|
245
|
+
* Uses Nango proxy with user's GitHub OAuth token.
|
|
195
246
|
*/
|
|
196
|
-
reposRouter.get('
|
|
247
|
+
reposRouter.get('/accessible', async (req, res) => {
|
|
197
248
|
const userId = req.session.userId;
|
|
198
|
-
const {
|
|
249
|
+
const { page, perPage, type, sort } = req.query;
|
|
199
250
|
try {
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
return res.status(404).json({ error: '
|
|
251
|
+
// Get user's Nango connection ID
|
|
252
|
+
const user = await db.users.findById(userId);
|
|
253
|
+
if (!user) {
|
|
254
|
+
return res.status(404).json({ error: 'User not found' });
|
|
255
|
+
}
|
|
256
|
+
if (!user.nangoConnectionId) {
|
|
257
|
+
return res.status(400).json({
|
|
258
|
+
error: 'GitHub not connected via Nango',
|
|
259
|
+
code: 'NANGO_NOT_CONNECTED',
|
|
260
|
+
message: 'Please reconnect your GitHub account',
|
|
261
|
+
});
|
|
204
262
|
}
|
|
263
|
+
// List accessible repos via Nango proxy
|
|
264
|
+
const result = await nangoService.listUserAccessibleRepos(user.nangoConnectionId, {
|
|
265
|
+
page: page ? parseInt(page, 10) : undefined,
|
|
266
|
+
perPage: perPage ? Math.min(parseInt(perPage, 10), 100) : undefined,
|
|
267
|
+
type: type,
|
|
268
|
+
sort: sort,
|
|
269
|
+
});
|
|
205
270
|
res.json({
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
271
|
+
repositories: result.repositories,
|
|
272
|
+
pagination: {
|
|
273
|
+
page: page ? parseInt(page, 10) : 1,
|
|
274
|
+
perPage: perPage ? Math.min(parseInt(perPage, 10), 100) : 100,
|
|
275
|
+
hasMore: result.hasMore,
|
|
276
|
+
},
|
|
277
|
+
checkedBy: {
|
|
278
|
+
userId: user.id,
|
|
279
|
+
githubUsername: user.githubUsername,
|
|
280
|
+
},
|
|
214
281
|
});
|
|
215
282
|
}
|
|
216
283
|
catch (error) {
|
|
217
|
-
console.error('Error
|
|
218
|
-
res.status(500).json({ error: 'Failed to
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
/**
|
|
222
|
-
* POST /api/repos/:id/sync
|
|
223
|
-
* Trigger repository sync (clone/pull to workspace)
|
|
224
|
-
*/
|
|
225
|
-
reposRouter.post('/:id/sync', async (req, res) => {
|
|
226
|
-
const userId = req.session.userId;
|
|
227
|
-
const { id } = req.params;
|
|
228
|
-
try {
|
|
229
|
-
const repositories = await db.repositories.findByUserId(userId);
|
|
230
|
-
const repo = repositories.find((r) => r.id === id);
|
|
231
|
-
if (!repo) {
|
|
232
|
-
return res.status(404).json({ error: 'Repository not found' });
|
|
233
|
-
}
|
|
234
|
-
if (!repo.workspaceId) {
|
|
235
|
-
return res.status(400).json({ error: 'Repository not assigned to a workspace' });
|
|
236
|
-
}
|
|
237
|
-
// Update sync status
|
|
238
|
-
await db.repositories.updateSyncStatus(id, 'syncing');
|
|
239
|
-
// In production, this would trigger the workspace to pull the repo
|
|
240
|
-
// For now, simulate success after a short delay
|
|
241
|
-
setTimeout(async () => {
|
|
242
|
-
await db.repositories.updateSyncStatus(id, 'synced', new Date());
|
|
243
|
-
}, 2000);
|
|
244
|
-
res.json({ message: 'Sync started', syncStatus: 'syncing' });
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
console.error('Error syncing repo:', error);
|
|
248
|
-
res.status(500).json({ error: 'Failed to sync repository' });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
/**
|
|
252
|
-
* DELETE /api/repos/:id
|
|
253
|
-
* Remove a repository
|
|
254
|
-
*/
|
|
255
|
-
reposRouter.delete('/:id', async (req, res) => {
|
|
256
|
-
const userId = req.session.userId;
|
|
257
|
-
const { id } = req.params;
|
|
258
|
-
try {
|
|
259
|
-
const repositories = await db.repositories.findByUserId(userId);
|
|
260
|
-
const repo = repositories.find((r) => r.id === id);
|
|
261
|
-
if (!repo) {
|
|
262
|
-
return res.status(404).json({ error: 'Repository not found' });
|
|
263
|
-
}
|
|
264
|
-
await db.repositories.delete(id);
|
|
265
|
-
res.json({ success: true });
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
console.error('Error deleting repo:', error);
|
|
269
|
-
res.status(500).json({ error: 'Failed to delete repository' });
|
|
284
|
+
console.error('Error listing accessible repos:', error);
|
|
285
|
+
res.status(500).json({ error: 'Failed to list accessible repositories' });
|
|
270
286
|
}
|
|
271
287
|
});
|
|
272
288
|
/**
|
|
@@ -365,61 +381,6 @@ reposRouter.get('/check-access/:owner/:repo', async (req, res) => {
|
|
|
365
381
|
res.status(500).json({ error: 'Failed to check repository access' });
|
|
366
382
|
}
|
|
367
383
|
});
|
|
368
|
-
/**
|
|
369
|
-
* GET /api/repos/accessible
|
|
370
|
-
* List all GitHub repositories the authenticated user has access to.
|
|
371
|
-
* Uses Nango proxy with user's GitHub OAuth token.
|
|
372
|
-
*
|
|
373
|
-
* Query params:
|
|
374
|
-
* - page: Page number (default: 1)
|
|
375
|
-
* - perPage: Results per page (default: 100, max: 100)
|
|
376
|
-
* - type: Filter by type (all, owner, public, private, member)
|
|
377
|
-
* - sort: Sort by (created, updated, pushed, full_name)
|
|
378
|
-
*
|
|
379
|
-
* Use this to determine which dashboards/workspaces a user can access.
|
|
380
|
-
*/
|
|
381
|
-
reposRouter.get('/accessible', async (req, res) => {
|
|
382
|
-
const userId = req.session.userId;
|
|
383
|
-
const { page, perPage, type, sort } = req.query;
|
|
384
|
-
try {
|
|
385
|
-
// Get user's Nango connection ID
|
|
386
|
-
const user = await db.users.findById(userId);
|
|
387
|
-
if (!user) {
|
|
388
|
-
return res.status(404).json({ error: 'User not found' });
|
|
389
|
-
}
|
|
390
|
-
if (!user.nangoConnectionId) {
|
|
391
|
-
return res.status(400).json({
|
|
392
|
-
error: 'GitHub not connected via Nango',
|
|
393
|
-
code: 'NANGO_NOT_CONNECTED',
|
|
394
|
-
message: 'Please reconnect your GitHub account',
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
// List accessible repos via Nango proxy
|
|
398
|
-
const result = await nangoService.listUserAccessibleRepos(user.nangoConnectionId, {
|
|
399
|
-
page: page ? parseInt(page, 10) : undefined,
|
|
400
|
-
perPage: perPage ? Math.min(parseInt(perPage, 10), 100) : undefined,
|
|
401
|
-
type: type,
|
|
402
|
-
sort: sort,
|
|
403
|
-
});
|
|
404
|
-
res.json({
|
|
405
|
-
repositories: result.repositories,
|
|
406
|
-
pagination: {
|
|
407
|
-
page: page ? parseInt(page, 10) : 1,
|
|
408
|
-
perPage: perPage ? Math.min(parseInt(perPage, 10), 100) : 100,
|
|
409
|
-
hasMore: result.hasMore,
|
|
410
|
-
},
|
|
411
|
-
// Include user info for context
|
|
412
|
-
checkedBy: {
|
|
413
|
-
userId: user.id,
|
|
414
|
-
githubUsername: user.githubUsername,
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
catch (error) {
|
|
419
|
-
console.error('Error listing accessible repos:', error);
|
|
420
|
-
res.status(500).json({ error: 'Failed to list accessible repositories' });
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
384
|
/**
|
|
424
385
|
* POST /api/repos/check-access-bulk
|
|
425
386
|
* Check access to multiple repositories at once.
|
|
@@ -497,4 +458,119 @@ reposRouter.post('/check-access-bulk', async (req, res) => {
|
|
|
497
458
|
res.status(500).json({ error: 'Failed to check repository access' });
|
|
498
459
|
}
|
|
499
460
|
});
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// WILDCARD ROUTES BELOW - All specific routes must be defined ABOVE this line
|
|
463
|
+
// ============================================================================
|
|
464
|
+
/**
|
|
465
|
+
* GET /api/repos/:id
|
|
466
|
+
* Get repository details
|
|
467
|
+
*/
|
|
468
|
+
reposRouter.get('/:id', async (req, res) => {
|
|
469
|
+
const userId = req.session.userId;
|
|
470
|
+
const { id } = req.params;
|
|
471
|
+
try {
|
|
472
|
+
const repositories = await db.repositories.findByUserId(userId);
|
|
473
|
+
const repo = repositories.find((r) => r.id === id);
|
|
474
|
+
if (!repo) {
|
|
475
|
+
return res.status(404).json({ error: 'Repository not found' });
|
|
476
|
+
}
|
|
477
|
+
res.json({
|
|
478
|
+
id: repo.id,
|
|
479
|
+
fullName: repo.githubFullName,
|
|
480
|
+
defaultBranch: repo.defaultBranch,
|
|
481
|
+
isPrivate: repo.isPrivate,
|
|
482
|
+
syncStatus: repo.syncStatus,
|
|
483
|
+
lastSyncedAt: repo.lastSyncedAt,
|
|
484
|
+
workspaceId: repo.workspaceId,
|
|
485
|
+
createdAt: repo.createdAt,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error('Error getting repo:', error);
|
|
490
|
+
res.status(500).json({ error: 'Failed to get repository' });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
/**
|
|
494
|
+
* POST /api/repos/:id/sync
|
|
495
|
+
* Trigger repository sync (clone/pull to workspace)
|
|
496
|
+
*
|
|
497
|
+
* Calls the workspace's /repos/sync API endpoint to clone or update the repo.
|
|
498
|
+
* This enables dynamic repo management without workspace restart.
|
|
499
|
+
*/
|
|
500
|
+
reposRouter.post('/:id/sync', async (req, res) => {
|
|
501
|
+
const userId = req.session.userId;
|
|
502
|
+
const { id } = req.params;
|
|
503
|
+
try {
|
|
504
|
+
const repositories = await db.repositories.findByUserId(userId);
|
|
505
|
+
const repo = repositories.find((r) => r.id === id);
|
|
506
|
+
if (!repo) {
|
|
507
|
+
return res.status(404).json({ error: 'Repository not found' });
|
|
508
|
+
}
|
|
509
|
+
if (!repo.workspaceId) {
|
|
510
|
+
return res.status(400).json({ error: 'Repository not assigned to a workspace' });
|
|
511
|
+
}
|
|
512
|
+
// Get the workspace to find its public URL
|
|
513
|
+
const workspace = await db.workspaces.findById(repo.workspaceId);
|
|
514
|
+
if (!workspace) {
|
|
515
|
+
return res.status(404).json({ error: 'Workspace not found' });
|
|
516
|
+
}
|
|
517
|
+
if (workspace.status !== 'running') {
|
|
518
|
+
return res.status(400).json({
|
|
519
|
+
error: 'Workspace is not running',
|
|
520
|
+
workspaceStatus: workspace.status,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (!workspace.publicUrl) {
|
|
524
|
+
return res.status(400).json({ error: 'Workspace has no public URL' });
|
|
525
|
+
}
|
|
526
|
+
// Update sync status
|
|
527
|
+
await db.repositories.updateSyncStatus(id, 'syncing');
|
|
528
|
+
// Call the workspace's repo sync API
|
|
529
|
+
const result = await callWorkspaceApi(workspace.publicUrl, workspace.id, 'POST', '/repos/sync', { repo: repo.githubFullName });
|
|
530
|
+
if (result.ok) {
|
|
531
|
+
// Update sync status to synced
|
|
532
|
+
await db.repositories.updateSyncStatus(id, 'synced', new Date());
|
|
533
|
+
res.json({
|
|
534
|
+
message: 'Repository synced successfully',
|
|
535
|
+
syncStatus: 'synced',
|
|
536
|
+
result: result.data,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Update sync status to error
|
|
541
|
+
await db.repositories.updateSyncStatus(id, 'error');
|
|
542
|
+
console.error('Workspace sync failed:', result.error);
|
|
543
|
+
res.status(502).json({
|
|
544
|
+
error: 'Failed to sync repository to workspace',
|
|
545
|
+
details: result.error,
|
|
546
|
+
syncStatus: 'error',
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
console.error('Error syncing repo:', error);
|
|
552
|
+
res.status(500).json({ error: 'Failed to sync repository' });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
/**
|
|
556
|
+
* DELETE /api/repos/:id
|
|
557
|
+
* Remove a repository
|
|
558
|
+
*/
|
|
559
|
+
reposRouter.delete('/:id', async (req, res) => {
|
|
560
|
+
const userId = req.session.userId;
|
|
561
|
+
const { id } = req.params;
|
|
562
|
+
try {
|
|
563
|
+
const repositories = await db.repositories.findByUserId(userId);
|
|
564
|
+
const repo = repositories.find((r) => r.id === id);
|
|
565
|
+
if (!repo) {
|
|
566
|
+
return res.status(404).json({ error: 'Repository not found' });
|
|
567
|
+
}
|
|
568
|
+
await db.repositories.delete(id);
|
|
569
|
+
res.json({ success: true });
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
console.error('Error deleting repo:', error);
|
|
573
|
+
res.status(500).json({ error: 'Failed to delete repository' });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
500
576
|
//# sourceMappingURL=repos.js.map
|
|
@@ -227,6 +227,46 @@ testHelpersRouter.post('/create-mock-repo', async (req, res) => {
|
|
|
227
227
|
res.status(500).json({ error: 'Failed to create mock repo' });
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
|
+
/**
|
|
231
|
+
* GET /api/test/auto-login
|
|
232
|
+
* Browser-friendly auto-login - visit this URL to login and redirect
|
|
233
|
+
* Usage: /api/test/auto-login?redirect=/providers/setup/claude?workspace=xxx
|
|
234
|
+
*/
|
|
235
|
+
testHelpersRouter.get('/auto-login', async (req, res) => {
|
|
236
|
+
if (!isTestMode) {
|
|
237
|
+
return res.status(403).json({ error: 'Test endpoints disabled in production' });
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const db = getDb();
|
|
241
|
+
const redirect = req.query.redirect || '/app';
|
|
242
|
+
// Find or create test user
|
|
243
|
+
let user;
|
|
244
|
+
const existingUsers = await db.select().from(users).limit(1);
|
|
245
|
+
if (existingUsers.length > 0) {
|
|
246
|
+
user = existingUsers[0];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const testId = `test-${randomUUID()}`;
|
|
250
|
+
const [newUser] = await db.insert(users).values({
|
|
251
|
+
email: `${testId}@test.local`,
|
|
252
|
+
githubId: testId,
|
|
253
|
+
githubUsername: 'test-user',
|
|
254
|
+
avatarUrl: null,
|
|
255
|
+
plan: 'free',
|
|
256
|
+
}).returning();
|
|
257
|
+
user = newUser;
|
|
258
|
+
}
|
|
259
|
+
// Set session and CSRF token
|
|
260
|
+
req.session.userId = user.id;
|
|
261
|
+
req.session.csrfToken = randomUUID();
|
|
262
|
+
// Redirect to requested page
|
|
263
|
+
res.redirect(redirect);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error('Error in auto-login:', error);
|
|
267
|
+
res.status(500).json({ error: 'Failed to auto-login' });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
230
270
|
/**
|
|
231
271
|
* POST /api/test/login-as
|
|
232
272
|
* Quick login for testing - creates session for existing or new test user
|
package/dist/cloud/api/usage.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Router } from 'express';
|
|
7
7
|
import { requireAuth } from './auth.js';
|
|
8
8
|
import { getRemainingQuota, getUserUsage, getPlanLimits } from '../services/planLimits.js';
|
|
9
|
+
import { getIntroStatus } from '../services/intro-expiration.js';
|
|
9
10
|
import { db } from '../db/index.js';
|
|
10
11
|
export const usageRouter = Router();
|
|
11
12
|
// All routes require authentication
|
|
@@ -23,6 +24,8 @@ usageRouter.get('/', async (req, res) => {
|
|
|
23
24
|
}
|
|
24
25
|
const plan = user.plan || 'free';
|
|
25
26
|
const quota = await getRemainingQuota(userId);
|
|
27
|
+
// Get intro period status for free tier users
|
|
28
|
+
const introStatus = getIntroStatus(user.createdAt, plan);
|
|
26
29
|
const calcPercent = (current, limit) => limit === Infinity ? 0 : Math.round((current / limit) * 100);
|
|
27
30
|
res.json({
|
|
28
31
|
plan,
|
|
@@ -51,6 +54,16 @@ usageRouter.get('/', async (req, res) => {
|
|
|
51
54
|
concurrentAgents: calcPercent(quota.usage.concurrentAgents, quota.limits.maxConcurrentAgents),
|
|
52
55
|
computeHours: calcPercent(quota.usage.computeHoursThisMonth, quota.limits.maxComputeHoursPerMonth),
|
|
53
56
|
},
|
|
57
|
+
// Intro period bonus for free tier users (2 CPU / 4GB for first 14 days)
|
|
58
|
+
introBonus: {
|
|
59
|
+
isActive: introStatus.isIntroPeriod,
|
|
60
|
+
daysRemaining: introStatus.daysRemaining,
|
|
61
|
+
totalDays: introStatus.introPeriodDays,
|
|
62
|
+
expiresAt: introStatus.expiresAt?.toISOString() || null,
|
|
63
|
+
resources: introStatus.isIntroPeriod
|
|
64
|
+
? { cpus: 2, memoryGb: 4, description: 'Pro-level resources' }
|
|
65
|
+
: { cpus: 1, memoryGb: 2, description: 'Standard free tier' },
|
|
66
|
+
},
|
|
54
67
|
});
|
|
55
68
|
}
|
|
56
69
|
catch (error) {
|
|
@@ -76,7 +76,7 @@ async function wakeWorkspaceMachine(workspaceId) {
|
|
|
76
76
|
* http://{workspace-internal}/webhooks/your/path
|
|
77
77
|
*/
|
|
78
78
|
// Handler for workspace webhook forwarding - matches any path under /workspace/:workspaceId
|
|
79
|
-
async function
|
|
79
|
+
async function _handleWorkspaceWebhook(req, res) {
|
|
80
80
|
// Extract workspaceId from URL path
|
|
81
81
|
const pathMatch = req.originalUrl.match(/\/workspace\/([^/]+)\/?(.*)$/);
|
|
82
82
|
if (!pathMatch) {
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
* Workspaces API Routes
|
|
3
3
|
*
|
|
4
4
|
* One-click workspace provisioning and management.
|
|
5
|
+
* Includes auto-access based on GitHub repo permissions.
|
|
5
6
|
*/
|
|
7
|
+
import { Request, Response, NextFunction } from 'express';
|
|
8
|
+
/**
|
|
9
|
+
* Check if user has access to a workspace via:
|
|
10
|
+
* 1. Workspace ownership (userId matches)
|
|
11
|
+
* 2. Explicit workspace_members record
|
|
12
|
+
* 3. GitHub repo access (just-in-time check via Nango)
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkWorkspaceAccess(userId: string, workspaceId: string): Promise<{
|
|
15
|
+
hasAccess: boolean;
|
|
16
|
+
accessType: 'owner' | 'member' | 'contributor' | 'none';
|
|
17
|
+
permission?: 'admin' | 'write' | 'read';
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Middleware to require workspace access.
|
|
21
|
+
* Checks ownership, membership, or GitHub repo access.
|
|
22
|
+
*/
|
|
23
|
+
export declare function requireWorkspaceAccess(req: Request, res: Response, next: NextFunction): void;
|
|
6
24
|
export declare const workspacesRouter: import("express-serve-static-core").Router;
|
|
7
25
|
//# sourceMappingURL=workspaces.d.ts.map
|