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
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * Codex Auth Helper API
3
3
  *
4
- * Provides endpoints for the `npx agent-relay codex-auth` CLI command
5
- * to capture OAuth callbacks locally and send them to the cloud.
4
+ * Provides endpoints for the `npx agent-relay codex-auth` CLI command.
5
+ * Uses SSH tunneling to forward localhost:1455 to the workspace container,
6
+ * allowing the Codex CLI's OAuth callback to work in remote/container environments.
6
7
  *
7
- * This solves the "This site can't be reached" problem where Codex redirects
8
- * to localhost:1455 after auth but nothing is listening.
8
+ * Flow:
9
+ * 1. CLI gets workspace SSH info via /tunnel-info
10
+ * 2. CLI establishes SSH tunnel: local:1455 -> container:1455
11
+ * 3. User completes OAuth, browser redirects to localhost:1455
12
+ * 4. Tunnel forwards to container's Codex CLI server
13
+ * 5. Codex CLI exchanges code for tokens internally
14
+ * 6. CLI polls for auth completion
9
15
  */
10
16
  export declare const codexAuthHelperRouter: import("express-serve-static-core").Router;
11
17
  /**
@@ -1,17 +1,26 @@
1
1
  /**
2
2
  * Codex Auth Helper API
3
3
  *
4
- * Provides endpoints for the `npx agent-relay codex-auth` CLI command
5
- * to capture OAuth callbacks locally and send them to the cloud.
4
+ * Provides endpoints for the `npx agent-relay codex-auth` CLI command.
5
+ * Uses SSH tunneling to forward localhost:1455 to the workspace container,
6
+ * allowing the Codex CLI's OAuth callback to work in remote/container environments.
6
7
  *
7
- * This solves the "This site can't be reached" problem where Codex redirects
8
- * to localhost:1455 after auth but nothing is listening.
8
+ * Flow:
9
+ * 1. CLI gets workspace SSH info via /tunnel-info
10
+ * 2. CLI establishes SSH tunnel: local:1455 -> container:1455
11
+ * 3. User completes OAuth, browser redirects to localhost:1455
12
+ * 4. Tunnel forwards to container's Codex CLI server
13
+ * 5. Codex CLI exchanges code for tokens internally
14
+ * 6. CLI polls for auth completion
9
15
  */
10
16
  import { Router } from 'express';
11
17
  import crypto from 'crypto';
12
18
  import { requireAuth } from './auth.js';
19
+ import { db } from '../db/index.js';
20
+ import { deriveSshPassword } from '../services/ssh-security.js';
13
21
  export const codexAuthHelperRouter = Router();
14
22
  const pendingAuthSessions = new Map();
23
+ const pendingCliTokens = new Map();
15
24
  // Clean up old sessions every minute
16
25
  const cleanupInterval = setInterval(() => {
17
26
  const now = Date.now();
@@ -21,6 +30,13 @@ const cleanupInterval = setInterval(() => {
21
30
  pendingAuthSessions.delete(id);
22
31
  }
23
32
  }
33
+ // Also clean up CLI tokens
34
+ for (const [id, token] of pendingCliTokens) {
35
+ // Remove tokens older than 10 minutes
36
+ if (now - token.createdAt.getTime() > 10 * 60 * 1000) {
37
+ pendingCliTokens.delete(id);
38
+ }
39
+ }
24
40
  }, 60000);
25
41
  /**
26
42
  * Stop the cleanup interval. Call this on server shutdown.
@@ -31,16 +47,51 @@ export function stopCodexAuthCleanup() {
31
47
  /**
32
48
  * POST /api/auth/codex-helper/cli-session
33
49
  * Create a new auth session for the CLI command.
34
- * Returns an authSessionId that the CLI uses to send the auth code.
50
+ * Returns workspace info and CLI command for SSH tunnel approach.
35
51
  */
36
52
  codexAuthHelperRouter.post('/cli-session', requireAuth, async (req, res) => {
37
53
  const userId = req.session.userId;
54
+ const { workspaceId, authUrl, sessionId: onboardingSessionId } = req.body;
55
+ // If workspace ID provided, return SSH tunnel command
56
+ if (workspaceId) {
57
+ try {
58
+ const workspace = await db.workspaces.findById(workspaceId);
59
+ if (!workspace || workspace.userId !== userId) {
60
+ return res.status(404).json({ error: 'Workspace not found' });
61
+ }
62
+ // Generate a one-time CLI token for authentication
63
+ const cliToken = crypto.randomUUID();
64
+ pendingCliTokens.set(cliToken, {
65
+ userId,
66
+ workspaceId,
67
+ createdAt: new Date(),
68
+ authUrl, // Store authUrl so CLI can retrieve it
69
+ sessionId: onboardingSessionId, // Store sessionId for credential storage
70
+ });
71
+ console.log(`[codex-helper] Created CLI session for workspace ${workspaceId} with token ${cliToken.slice(0, 8)}...`);
72
+ const cloudUrl = process.env.PUBLIC_URL || 'https://agent-relay.com';
73
+ // Generate the CLI command with workspace ID and token
74
+ res.json({
75
+ workspaceId: workspace.id,
76
+ workspaceName: workspace.name,
77
+ expiresIn: 600, // 10 minutes
78
+ command: `npx agent-relay codex-auth --workspace=${workspaceId} --token=${cliToken}`,
79
+ commandWithUrl: `npx agent-relay codex-auth --workspace=${workspaceId} --token=${cliToken} --cloud-url=${cloudUrl}`,
80
+ });
81
+ return;
82
+ }
83
+ catch (error) {
84
+ console.error('[codex-helper] Error creating CLI session:', error);
85
+ return res.status(500).json({ error: 'Failed to create CLI session' });
86
+ }
87
+ }
88
+ // Legacy: token-based session (for backwards compatibility)
38
89
  const authSessionId = crypto.randomUUID();
39
90
  pendingAuthSessions.set(authSessionId, {
40
91
  userId,
41
92
  createdAt: new Date(),
42
93
  });
43
- console.log(`[codex-helper] Created CLI session ${authSessionId} for user ${userId}`);
94
+ console.log(`[codex-helper] Created legacy CLI session ${authSessionId} for user ${userId}`);
44
95
  res.json({
45
96
  authSessionId,
46
97
  expiresIn: 600, // 10 minutes
@@ -53,7 +104,7 @@ codexAuthHelperRouter.post('/cli-session', requireAuth, async (req, res) => {
53
104
  * No auth required - validated by authSessionId.
54
105
  */
55
106
  codexAuthHelperRouter.post('/callback', async (req, res) => {
56
- const { authSessionId, code, error } = req.body;
107
+ const { authSessionId, code, state, error } = req.body;
57
108
  if (!authSessionId) {
58
109
  return res.status(400).json({ error: 'Missing authSessionId' });
59
110
  }
@@ -69,8 +120,9 @@ codexAuthHelperRouter.post('/callback', async (req, res) => {
69
120
  if (!code) {
70
121
  return res.status(400).json({ error: 'Missing auth code' });
71
122
  }
72
- // Store the code so the polling endpoint can retrieve it
123
+ // Store the code and state so the polling endpoint can retrieve them
73
124
  session.code = code;
125
+ session.state = state; // Store state for CSRF validation
74
126
  pendingAuthSessions.set(authSessionId, session);
75
127
  console.log(`[codex-helper] Auth code received for session ${authSessionId}`);
76
128
  res.json({ success: true, message: 'Auth code received. You can close this terminal.' });
@@ -89,12 +141,167 @@ codexAuthHelperRouter.get('/status/:authSessionId', requireAuth, async (req, res
89
141
  if (session.code) {
90
142
  // Clean up session after successful retrieval (code is single-use)
91
143
  const code = session.code;
144
+ const state = session.state;
92
145
  pendingAuthSessions.delete(authSessionId);
93
146
  return res.json({
94
147
  ready: true,
95
148
  code,
149
+ state, // Return state for CSRF validation
96
150
  });
97
151
  }
98
152
  res.json({ ready: false });
99
153
  });
154
+ /**
155
+ * GET /api/auth/codex-helper/tunnel-info/:workspaceId
156
+ * Get SSH tunnel info for establishing port forwarding to a workspace.
157
+ * Returns host, port, user, and password for SSH connection.
158
+ *
159
+ * Authentication: Requires either session auth OR a valid CLI token.
160
+ */
161
+ codexAuthHelperRouter.get('/tunnel-info/:workspaceId', async (req, res) => {
162
+ const { workspaceId } = req.params;
163
+ const { token } = req.query;
164
+ // Authenticate via CLI token or session
165
+ let userId;
166
+ if (token && typeof token === 'string') {
167
+ // CLI token authentication
168
+ const cliToken = pendingCliTokens.get(token);
169
+ if (!cliToken) {
170
+ return res.status(401).json({ error: 'Invalid or expired token' });
171
+ }
172
+ if (cliToken.workspaceId !== workspaceId) {
173
+ return res.status(403).json({ error: 'Token does not match workspace' });
174
+ }
175
+ userId = cliToken.userId;
176
+ // Don't delete token - it's also used for auth-status polling
177
+ // Cleanup interval will remove it after 10 minutes
178
+ console.log(`[codex-helper] CLI token used for workspace ${workspaceId}`);
179
+ }
180
+ else if (req.session?.userId) {
181
+ // Session authentication
182
+ userId = req.session.userId;
183
+ }
184
+ else {
185
+ return res.status(401).json({ error: 'Authentication required. Provide token query parameter or valid session.' });
186
+ }
187
+ try {
188
+ const workspace = await db.workspaces.findById(workspaceId);
189
+ if (!workspace || workspace.userId !== userId) {
190
+ return res.status(404).json({ error: 'Workspace not found' });
191
+ }
192
+ if (workspace.status !== 'running') {
193
+ return res.status(400).json({ error: 'Workspace is not running' });
194
+ }
195
+ // Parse workspace URL to get host
196
+ const publicUrl = workspace.publicUrl;
197
+ if (!publicUrl) {
198
+ return res.status(400).json({ error: 'Workspace URL not available' });
199
+ }
200
+ const url = new URL(publicUrl);
201
+ const host = url.hostname;
202
+ const apiPort = parseInt(url.port, 10) || 80;
203
+ // SSH connection info varies by environment:
204
+ // - Fly.io: Use public fly.dev hostname with port 2222 (exposed via TCP service)
205
+ // - Local Docker: Use localhost with derived SSH port (22000 + apiPort - 3000)
206
+ const isOnFly = !!process.env.FLY_APP_NAME;
207
+ const isLocalDocker = (host === 'localhost' || host === '127.0.0.1') && apiPort >= 3000;
208
+ let sshHost;
209
+ let sshPort;
210
+ if (isOnFly) {
211
+ // Fly.io public hostname - SSH is exposed as a public TCP service on port 2222
212
+ // Users can SSH directly from their machine to {app}.fly.dev:2222
213
+ const appName = `ar-${workspace.id.substring(0, 8)}`;
214
+ sshHost = `${appName}.fly.dev`;
215
+ sshPort = 2222;
216
+ }
217
+ else if (isLocalDocker) {
218
+ // Local Docker: SSH port is derived from API port
219
+ // API port 3500 -> SSH port 22500 (formula: 22000 + apiPort - 3000)
220
+ sshHost = 'localhost';
221
+ sshPort = 22000 + (apiPort - 3000);
222
+ }
223
+ else {
224
+ // Default fallback
225
+ sshHost = host;
226
+ sshPort = 2222;
227
+ }
228
+ // SSH password is derived per-workspace for security
229
+ // Each workspace gets a unique password based on its ID + secret salt
230
+ const sshPassword = deriveSshPassword(workspace.id);
231
+ // Get authUrl from CLI token if available
232
+ let authUrl;
233
+ if (token && typeof token === 'string') {
234
+ const cliToken = pendingCliTokens.get(token);
235
+ authUrl = cliToken?.authUrl;
236
+ }
237
+ res.json({
238
+ host: sshHost,
239
+ port: sshPort,
240
+ user: 'workspace',
241
+ password: sshPassword,
242
+ tunnelPort: 1455, // Codex OAuth callback port
243
+ workspaceId: workspace.id,
244
+ workspaceName: workspace.name,
245
+ authUrl, // OAuth URL if available (set by dashboard)
246
+ });
247
+ }
248
+ catch (error) {
249
+ console.error('[codex-helper] Error getting tunnel info:', error);
250
+ res.status(500).json({ error: 'Failed to get tunnel info' });
251
+ }
252
+ });
253
+ /**
254
+ * GET /api/auth/codex-helper/auth-status/:workspaceId
255
+ * Poll for Codex authentication completion in a workspace.
256
+ * The CLI uses this after establishing the tunnel to know when auth is done.
257
+ *
258
+ * Authentication: Requires either session auth OR a valid CLI token.
259
+ */
260
+ codexAuthHelperRouter.get('/auth-status/:workspaceId', async (req, res) => {
261
+ const { workspaceId } = req.params;
262
+ const { token } = req.query;
263
+ // Authenticate via CLI token or session
264
+ let userId;
265
+ if (token && typeof token === 'string') {
266
+ // CLI token authentication
267
+ const cliToken = pendingCliTokens.get(token);
268
+ if (!cliToken) {
269
+ return res.status(401).json({ error: 'Invalid or expired token' });
270
+ }
271
+ if (cliToken.workspaceId !== workspaceId) {
272
+ return res.status(403).json({ error: 'Token does not match workspace' });
273
+ }
274
+ userId = cliToken.userId;
275
+ }
276
+ else if (req.session?.userId) {
277
+ // Session authentication
278
+ userId = req.session.userId;
279
+ }
280
+ else {
281
+ return res.status(401).json({ error: 'Authentication required. Provide token query parameter or valid session.' });
282
+ }
283
+ try {
284
+ const workspace = await db.workspaces.findById(workspaceId);
285
+ if (!workspace || workspace.userId !== userId) {
286
+ return res.status(404).json({ error: 'Workspace not found' });
287
+ }
288
+ if (!workspace.publicUrl) {
289
+ return res.status(400).json({ error: 'Workspace URL not available' });
290
+ }
291
+ // Check with workspace daemon if Codex is authenticated
292
+ const response = await fetch(`${workspace.publicUrl}/auth/cli/openai/check`, {
293
+ method: 'GET',
294
+ signal: AbortSignal.timeout(5000),
295
+ });
296
+ if (response.ok) {
297
+ const data = await response.json();
298
+ return res.json({ authenticated: data.authenticated });
299
+ }
300
+ res.json({ authenticated: false });
301
+ }
302
+ catch (error) {
303
+ // Workspace might not be reachable, return false
304
+ res.json({ authenticated: false });
305
+ }
306
+ });
100
307
  //# sourceMappingURL=codex-auth-helper.js.map