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.
Files changed (200) hide show
  1. package/.trajectories/agent-relay-322-324.md +17 -0
  2. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  5. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  6. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  7. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  8. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  9. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  10. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  11. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  12. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  13. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  14. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  15. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  16. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  17. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  18. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  19. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  20. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  21. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  22. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  23. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  24. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  25. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  26. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  27. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  28. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  29. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  30. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  31. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  32. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  33. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  34. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  35. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  36. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  37. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  38. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  39. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  40. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  41. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  42. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  43. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  44. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  45. package/.trajectories/consolidate-settings-panel.md +24 -0
  46. package/.trajectories/gh-cli-user-token.md +26 -0
  47. package/.trajectories/index.json +155 -1
  48. package/deploy/workspace/codex.config.toml +15 -0
  49. package/deploy/workspace/entrypoint.sh +167 -7
  50. package/deploy/workspace/git-credential-relay +17 -2
  51. package/dist/bridge/spawner.d.ts +7 -0
  52. package/dist/bridge/spawner.js +40 -9
  53. package/dist/bridge/types.d.ts +2 -0
  54. package/dist/cli/index.js +210 -168
  55. package/dist/cloud/api/admin.d.ts +8 -0
  56. package/dist/cloud/api/admin.js +212 -0
  57. package/dist/cloud/api/auth.js +8 -0
  58. package/dist/cloud/api/billing.d.ts +0 -10
  59. package/dist/cloud/api/billing.js +248 -58
  60. package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
  61. package/dist/cloud/api/codex-auth-helper.js +215 -8
  62. package/dist/cloud/api/coordinators.js +402 -0
  63. package/dist/cloud/api/daemons.js +15 -11
  64. package/dist/cloud/api/git.js +104 -17
  65. package/dist/cloud/api/github-app.js +42 -8
  66. package/dist/cloud/api/nango-auth.js +297 -16
  67. package/dist/cloud/api/onboarding.js +97 -33
  68. package/dist/cloud/api/providers.js +12 -16
  69. package/dist/cloud/api/repos.js +200 -124
  70. package/dist/cloud/api/test-helpers.js +40 -0
  71. package/dist/cloud/api/usage.js +13 -0
  72. package/dist/cloud/api/webhooks.js +1 -1
  73. package/dist/cloud/api/workspaces.d.ts +18 -0
  74. package/dist/cloud/api/workspaces.js +945 -15
  75. package/dist/cloud/config.d.ts +8 -0
  76. package/dist/cloud/config.js +15 -0
  77. package/dist/cloud/db/drizzle.d.ts +5 -2
  78. package/dist/cloud/db/drizzle.js +27 -20
  79. package/dist/cloud/db/schema.d.ts +19 -51
  80. package/dist/cloud/db/schema.js +5 -4
  81. package/dist/cloud/index.d.ts +0 -1
  82. package/dist/cloud/index.js +0 -1
  83. package/dist/cloud/provisioner/index.d.ts +93 -1
  84. package/dist/cloud/provisioner/index.js +608 -63
  85. package/dist/cloud/server.js +156 -16
  86. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  87. package/dist/cloud/services/compute-enforcement.js +175 -0
  88. package/dist/cloud/services/index.d.ts +2 -0
  89. package/dist/cloud/services/index.js +4 -0
  90. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  91. package/dist/cloud/services/intro-expiration.js +211 -0
  92. package/dist/cloud/services/nango.d.ts +14 -0
  93. package/dist/cloud/services/nango.js +74 -14
  94. package/dist/cloud/services/ssh-security.d.ts +31 -0
  95. package/dist/cloud/services/ssh-security.js +63 -0
  96. package/dist/continuity/manager.d.ts +5 -0
  97. package/dist/continuity/manager.js +56 -2
  98. package/dist/daemon/api.d.ts +2 -0
  99. package/dist/daemon/api.js +214 -5
  100. package/dist/daemon/cli-auth.d.ts +13 -1
  101. package/dist/daemon/cli-auth.js +166 -47
  102. package/dist/daemon/connection.d.ts +7 -1
  103. package/dist/daemon/connection.js +15 -0
  104. package/dist/daemon/orchestrator.d.ts +2 -0
  105. package/dist/daemon/orchestrator.js +26 -0
  106. package/dist/daemon/repo-manager.d.ts +116 -0
  107. package/dist/daemon/repo-manager.js +384 -0
  108. package/dist/daemon/router.d.ts +60 -1
  109. package/dist/daemon/router.js +281 -20
  110. package/dist/daemon/user-directory.d.ts +111 -0
  111. package/dist/daemon/user-directory.js +233 -0
  112. package/dist/dashboard/out/404.html +1 -1
  113. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
  114. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  115. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  117. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  118. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
  119. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
  120. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
  121. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +1 -0
  122. package/dist/dashboard/out/_next/static/chunks/app/history/{page-abb9ab2d329f56e9.js → page-8c8bed33beb2bf1c.js} +1 -1
  123. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
  124. package/dist/dashboard/out/_next/static/chunks/app/login/{page-c22d080201cbd9fb.js → page-16f3b49e55b1e0ed.js} +1 -1
  125. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
  126. package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-4a5938c18a11a654.js} +1 -1
  127. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +1 -0
  128. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
  129. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
  130. package/dist/dashboard/out/_next/static/chunks/app/signup/{page-68d34f50baa8ab6b.js → page-547dd0ca55ecd0ba.js} +1 -1
  131. package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
  132. package/dist/dashboard/out/_next/static/chunks/{main-app-6e8e8d3ef4e0192a.js → main-app-5d692157a8eb1fd9.js} +1 -1
  133. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
  134. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  135. package/dist/dashboard/out/app/onboarding.html +1 -1
  136. package/dist/dashboard/out/app/onboarding.txt +3 -3
  137. package/dist/dashboard/out/app.html +1 -1
  138. package/dist/dashboard/out/app.txt +3 -3
  139. package/dist/dashboard/out/apple-icon.png +0 -0
  140. package/dist/dashboard/out/connect-repos.html +1 -1
  141. package/dist/dashboard/out/connect-repos.txt +3 -3
  142. package/dist/dashboard/out/history.html +1 -1
  143. package/dist/dashboard/out/history.txt +3 -3
  144. package/dist/dashboard/out/index.html +1 -1
  145. package/dist/dashboard/out/index.txt +3 -3
  146. package/dist/dashboard/out/login.html +2 -2
  147. package/dist/dashboard/out/login.txt +3 -3
  148. package/dist/dashboard/out/metrics.html +1 -1
  149. package/dist/dashboard/out/metrics.txt +3 -3
  150. package/dist/dashboard/out/pricing.html +2 -2
  151. package/dist/dashboard/out/pricing.txt +3 -3
  152. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  153. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  154. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  155. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  156. package/dist/dashboard/out/providers.html +1 -1
  157. package/dist/dashboard/out/providers.txt +3 -3
  158. package/dist/dashboard/out/signup.html +2 -2
  159. package/dist/dashboard/out/signup.txt +3 -3
  160. package/dist/dashboard-server/server.js +316 -12
  161. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  162. package/dist/dashboard-server/user-bridge.js +189 -0
  163. package/dist/protocol/channels.d.ts +205 -0
  164. package/dist/protocol/channels.js +154 -0
  165. package/dist/protocol/types.d.ts +13 -1
  166. package/dist/resiliency/provider-context.js +2 -0
  167. package/dist/shared/cli-auth-config.d.ts +19 -0
  168. package/dist/shared/cli-auth-config.js +58 -2
  169. package/dist/utils/agent-config.js +1 -1
  170. package/dist/wrapper/auth-detection.d.ts +49 -0
  171. package/dist/wrapper/auth-detection.js +192 -0
  172. package/dist/wrapper/base-wrapper.d.ts +153 -0
  173. package/dist/wrapper/base-wrapper.js +393 -0
  174. package/dist/wrapper/client.d.ts +7 -1
  175. package/dist/wrapper/client.js +3 -0
  176. package/dist/wrapper/index.d.ts +1 -0
  177. package/dist/wrapper/index.js +4 -3
  178. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  179. package/dist/wrapper/pty-wrapper.js +154 -180
  180. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  181. package/dist/wrapper/tmux-wrapper.js +90 -134
  182. package/package.json +4 -2
  183. package/scripts/postinstall.js +11 -155
  184. package/scripts/test-interactive-terminal.sh +248 -0
  185. package/dist/cloud/vault/index.d.ts +0 -76
  186. package/dist/cloud/vault/index.js +0 -219
  187. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
  189. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +0 -1
  191. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
  192. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +0 -1
  193. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
  194. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
  195. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +0 -1
  196. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
  197. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  198. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
  199. package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
  200. /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
- // For now, mark as connected (in production, verify credentials exist)
247
- // This would be called after the user's workspace detects valid credentials
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: 'Credentials will be synced when workspace starts',
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
- // Store the API key - use scopes from device flow providers, empty for CLI providers
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 vault.storeCredential({
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
- * Store tokens after successful device flow
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
- // Encrypt and store
494
- await vault.storeCredential({
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,
@@ -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/:id
194
- * Get repository details
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('/:id', async (req, res) => {
247
+ reposRouter.get('/accessible', async (req, res) => {
197
248
  const userId = req.session.userId;
198
- const { id } = req.params;
249
+ const { page, perPage, type, sort } = req.query;
199
250
  try {
200
- const repositories = await db.repositories.findByUserId(userId);
201
- const repo = repositories.find((r) => r.id === id);
202
- if (!repo) {
203
- return res.status(404).json({ error: 'Repository not found' });
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
- id: repo.id,
207
- fullName: repo.githubFullName,
208
- defaultBranch: repo.defaultBranch,
209
- isPrivate: repo.isPrivate,
210
- syncStatus: repo.syncStatus,
211
- lastSyncedAt: repo.lastSyncedAt,
212
- workspaceId: repo.workspaceId,
213
- createdAt: repo.createdAt,
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 getting repo:', error);
218
- res.status(500).json({ error: 'Failed to get repository' });
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
@@ -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 handleWorkspaceWebhook(req, res) {
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