@agent-relay/cloud 0.1.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 (269) hide show
  1. package/dist/api/admin.d.ts +8 -0
  2. package/dist/api/admin.d.ts.map +1 -0
  3. package/dist/api/admin.js +225 -0
  4. package/dist/api/admin.js.map +1 -0
  5. package/dist/api/auth.d.ts +20 -0
  6. package/dist/api/auth.d.ts.map +1 -0
  7. package/dist/api/auth.js +136 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/billing.d.ts +7 -0
  10. package/dist/api/billing.d.ts.map +1 -0
  11. package/dist/api/billing.js +564 -0
  12. package/dist/api/billing.js.map +1 -0
  13. package/dist/api/cli-pty-runner.d.ts +53 -0
  14. package/dist/api/cli-pty-runner.d.ts.map +1 -0
  15. package/dist/api/cli-pty-runner.js +193 -0
  16. package/dist/api/cli-pty-runner.js.map +1 -0
  17. package/dist/api/codex-auth-helper.d.ts +21 -0
  18. package/dist/api/codex-auth-helper.d.ts.map +1 -0
  19. package/dist/api/codex-auth-helper.js +327 -0
  20. package/dist/api/codex-auth-helper.js.map +1 -0
  21. package/dist/api/consensus.d.ts +13 -0
  22. package/dist/api/consensus.d.ts.map +1 -0
  23. package/dist/api/consensus.js +261 -0
  24. package/dist/api/consensus.js.map +1 -0
  25. package/dist/api/coordinators.d.ts +8 -0
  26. package/dist/api/coordinators.d.ts.map +1 -0
  27. package/dist/api/coordinators.js +750 -0
  28. package/dist/api/coordinators.js.map +1 -0
  29. package/dist/api/daemons.d.ts +12 -0
  30. package/dist/api/daemons.d.ts.map +1 -0
  31. package/dist/api/daemons.js +535 -0
  32. package/dist/api/daemons.js.map +1 -0
  33. package/dist/api/generic-webhooks.d.ts +8 -0
  34. package/dist/api/generic-webhooks.d.ts.map +1 -0
  35. package/dist/api/generic-webhooks.js +129 -0
  36. package/dist/api/generic-webhooks.js.map +1 -0
  37. package/dist/api/git.d.ts +8 -0
  38. package/dist/api/git.d.ts.map +1 -0
  39. package/dist/api/git.js +269 -0
  40. package/dist/api/git.js.map +1 -0
  41. package/dist/api/github-app.d.ts +11 -0
  42. package/dist/api/github-app.d.ts.map +1 -0
  43. package/dist/api/github-app.js +223 -0
  44. package/dist/api/github-app.js.map +1 -0
  45. package/dist/api/middleware/planLimits.d.ts +43 -0
  46. package/dist/api/middleware/planLimits.d.ts.map +1 -0
  47. package/dist/api/middleware/planLimits.js +202 -0
  48. package/dist/api/middleware/planLimits.js.map +1 -0
  49. package/dist/api/monitoring.d.ts +11 -0
  50. package/dist/api/monitoring.d.ts.map +1 -0
  51. package/dist/api/monitoring.js +578 -0
  52. package/dist/api/monitoring.js.map +1 -0
  53. package/dist/api/nango-auth.d.ts +9 -0
  54. package/dist/api/nango-auth.d.ts.map +1 -0
  55. package/dist/api/nango-auth.js +674 -0
  56. package/dist/api/nango-auth.js.map +1 -0
  57. package/dist/api/onboarding.d.ts +15 -0
  58. package/dist/api/onboarding.d.ts.map +1 -0
  59. package/dist/api/onboarding.js +679 -0
  60. package/dist/api/onboarding.js.map +1 -0
  61. package/dist/api/policy.d.ts +8 -0
  62. package/dist/api/policy.d.ts.map +1 -0
  63. package/dist/api/policy.js +229 -0
  64. package/dist/api/policy.js.map +1 -0
  65. package/dist/api/provider-env.d.ts +14 -0
  66. package/dist/api/provider-env.d.ts.map +1 -0
  67. package/dist/api/provider-env.js +75 -0
  68. package/dist/api/provider-env.js.map +1 -0
  69. package/dist/api/providers.d.ts +7 -0
  70. package/dist/api/providers.d.ts.map +1 -0
  71. package/dist/api/providers.js +564 -0
  72. package/dist/api/providers.js.map +1 -0
  73. package/dist/api/repos.d.ts +8 -0
  74. package/dist/api/repos.d.ts.map +1 -0
  75. package/dist/api/repos.js +577 -0
  76. package/dist/api/repos.js.map +1 -0
  77. package/dist/api/sessions.d.ts +11 -0
  78. package/dist/api/sessions.d.ts.map +1 -0
  79. package/dist/api/sessions.js +302 -0
  80. package/dist/api/sessions.js.map +1 -0
  81. package/dist/api/teams.d.ts +7 -0
  82. package/dist/api/teams.d.ts.map +1 -0
  83. package/dist/api/teams.js +281 -0
  84. package/dist/api/teams.js.map +1 -0
  85. package/dist/api/test-helpers.d.ts +10 -0
  86. package/dist/api/test-helpers.d.ts.map +1 -0
  87. package/dist/api/test-helpers.js +745 -0
  88. package/dist/api/test-helpers.js.map +1 -0
  89. package/dist/api/usage.d.ts +7 -0
  90. package/dist/api/usage.d.ts.map +1 -0
  91. package/dist/api/usage.js +111 -0
  92. package/dist/api/usage.js.map +1 -0
  93. package/dist/api/webhooks.d.ts +8 -0
  94. package/dist/api/webhooks.d.ts.map +1 -0
  95. package/dist/api/webhooks.js +645 -0
  96. package/dist/api/webhooks.js.map +1 -0
  97. package/dist/api/workspaces.d.ts +25 -0
  98. package/dist/api/workspaces.d.ts.map +1 -0
  99. package/dist/api/workspaces.js +1799 -0
  100. package/dist/api/workspaces.js.map +1 -0
  101. package/dist/billing/index.d.ts +9 -0
  102. package/dist/billing/index.d.ts.map +1 -0
  103. package/dist/billing/index.js +9 -0
  104. package/dist/billing/index.js.map +1 -0
  105. package/dist/billing/plans.d.ts +39 -0
  106. package/dist/billing/plans.d.ts.map +1 -0
  107. package/dist/billing/plans.js +245 -0
  108. package/dist/billing/plans.js.map +1 -0
  109. package/dist/billing/service.d.ts +80 -0
  110. package/dist/billing/service.d.ts.map +1 -0
  111. package/dist/billing/service.js +388 -0
  112. package/dist/billing/service.js.map +1 -0
  113. package/dist/billing/types.d.ts +141 -0
  114. package/dist/billing/types.d.ts.map +1 -0
  115. package/dist/billing/types.js +7 -0
  116. package/dist/billing/types.js.map +1 -0
  117. package/dist/config.d.ts +5 -0
  118. package/dist/config.d.ts.map +1 -0
  119. package/dist/config.js +5 -0
  120. package/dist/config.js.map +1 -0
  121. package/dist/db/bulk-ingest.d.ts +89 -0
  122. package/dist/db/bulk-ingest.d.ts.map +1 -0
  123. package/dist/db/bulk-ingest.js +268 -0
  124. package/dist/db/bulk-ingest.js.map +1 -0
  125. package/dist/db/drizzle.d.ts +256 -0
  126. package/dist/db/drizzle.d.ts.map +1 -0
  127. package/dist/db/drizzle.js +1286 -0
  128. package/dist/db/drizzle.js.map +1 -0
  129. package/dist/db/index.d.ts +55 -0
  130. package/dist/db/index.d.ts.map +1 -0
  131. package/dist/db/index.js +68 -0
  132. package/dist/db/index.js.map +1 -0
  133. package/dist/db/schema.d.ts +4873 -0
  134. package/dist/db/schema.d.ts.map +1 -0
  135. package/dist/db/schema.js +620 -0
  136. package/dist/db/schema.js.map +1 -0
  137. package/dist/index.d.ts +11 -0
  138. package/dist/index.d.ts.map +1 -0
  139. package/dist/index.js +38 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/provisioner/index.d.ts +207 -0
  142. package/dist/provisioner/index.d.ts.map +1 -0
  143. package/dist/provisioner/index.js +2114 -0
  144. package/dist/provisioner/index.js.map +1 -0
  145. package/dist/server.d.ts +17 -0
  146. package/dist/server.d.ts.map +1 -0
  147. package/dist/server.js +1924 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/services/auto-scaler.d.ts +152 -0
  150. package/dist/services/auto-scaler.d.ts.map +1 -0
  151. package/dist/services/auto-scaler.js +439 -0
  152. package/dist/services/auto-scaler.js.map +1 -0
  153. package/dist/services/capacity-manager.d.ts +148 -0
  154. package/dist/services/capacity-manager.d.ts.map +1 -0
  155. package/dist/services/capacity-manager.js +449 -0
  156. package/dist/services/capacity-manager.js.map +1 -0
  157. package/dist/services/ci-agent-spawner.d.ts +49 -0
  158. package/dist/services/ci-agent-spawner.d.ts.map +1 -0
  159. package/dist/services/ci-agent-spawner.js +373 -0
  160. package/dist/services/ci-agent-spawner.js.map +1 -0
  161. package/dist/services/cloud-message-bus.d.ts +28 -0
  162. package/dist/services/cloud-message-bus.d.ts.map +1 -0
  163. package/dist/services/cloud-message-bus.js +19 -0
  164. package/dist/services/cloud-message-bus.js.map +1 -0
  165. package/dist/services/compute-enforcement.d.ts +57 -0
  166. package/dist/services/compute-enforcement.d.ts.map +1 -0
  167. package/dist/services/compute-enforcement.js +175 -0
  168. package/dist/services/compute-enforcement.js.map +1 -0
  169. package/dist/services/coordinator.d.ts +62 -0
  170. package/dist/services/coordinator.d.ts.map +1 -0
  171. package/dist/services/coordinator.js +389 -0
  172. package/dist/services/coordinator.js.map +1 -0
  173. package/dist/services/index.d.ts +17 -0
  174. package/dist/services/index.d.ts.map +1 -0
  175. package/dist/services/index.js +25 -0
  176. package/dist/services/index.js.map +1 -0
  177. package/dist/services/intro-expiration.d.ts +60 -0
  178. package/dist/services/intro-expiration.d.ts.map +1 -0
  179. package/dist/services/intro-expiration.js +252 -0
  180. package/dist/services/intro-expiration.js.map +1 -0
  181. package/dist/services/mention-handler.d.ts +65 -0
  182. package/dist/services/mention-handler.d.ts.map +1 -0
  183. package/dist/services/mention-handler.js +405 -0
  184. package/dist/services/mention-handler.js.map +1 -0
  185. package/dist/services/nango.d.ts +201 -0
  186. package/dist/services/nango.d.ts.map +1 -0
  187. package/dist/services/nango.js +392 -0
  188. package/dist/services/nango.js.map +1 -0
  189. package/dist/services/persistence.d.ts +131 -0
  190. package/dist/services/persistence.d.ts.map +1 -0
  191. package/dist/services/persistence.js +200 -0
  192. package/dist/services/persistence.js.map +1 -0
  193. package/dist/services/planLimits.d.ts +147 -0
  194. package/dist/services/planLimits.d.ts.map +1 -0
  195. package/dist/services/planLimits.js +335 -0
  196. package/dist/services/planLimits.js.map +1 -0
  197. package/dist/services/presence-registry.d.ts +56 -0
  198. package/dist/services/presence-registry.d.ts.map +1 -0
  199. package/dist/services/presence-registry.js +91 -0
  200. package/dist/services/presence-registry.js.map +1 -0
  201. package/dist/services/scaling-orchestrator.d.ts +159 -0
  202. package/dist/services/scaling-orchestrator.d.ts.map +1 -0
  203. package/dist/services/scaling-orchestrator.js +502 -0
  204. package/dist/services/scaling-orchestrator.js.map +1 -0
  205. package/dist/services/scaling-policy.d.ts +121 -0
  206. package/dist/services/scaling-policy.d.ts.map +1 -0
  207. package/dist/services/scaling-policy.js +415 -0
  208. package/dist/services/scaling-policy.js.map +1 -0
  209. package/dist/services/ssh-security.d.ts +31 -0
  210. package/dist/services/ssh-security.d.ts.map +1 -0
  211. package/dist/services/ssh-security.js +63 -0
  212. package/dist/services/ssh-security.js.map +1 -0
  213. package/dist/services/workspace-keepalive.d.ts +76 -0
  214. package/dist/services/workspace-keepalive.d.ts.map +1 -0
  215. package/dist/services/workspace-keepalive.js +234 -0
  216. package/dist/services/workspace-keepalive.js.map +1 -0
  217. package/dist/shims/consensus.d.ts +23 -0
  218. package/dist/shims/consensus.d.ts.map +1 -0
  219. package/dist/shims/consensus.js +5 -0
  220. package/dist/shims/consensus.js.map +1 -0
  221. package/dist/webhooks/index.d.ts +24 -0
  222. package/dist/webhooks/index.d.ts.map +1 -0
  223. package/dist/webhooks/index.js +29 -0
  224. package/dist/webhooks/index.js.map +1 -0
  225. package/dist/webhooks/parsers/github.d.ts +8 -0
  226. package/dist/webhooks/parsers/github.d.ts.map +1 -0
  227. package/dist/webhooks/parsers/github.js +234 -0
  228. package/dist/webhooks/parsers/github.js.map +1 -0
  229. package/dist/webhooks/parsers/index.d.ts +23 -0
  230. package/dist/webhooks/parsers/index.d.ts.map +1 -0
  231. package/dist/webhooks/parsers/index.js +30 -0
  232. package/dist/webhooks/parsers/index.js.map +1 -0
  233. package/dist/webhooks/parsers/linear.d.ts +9 -0
  234. package/dist/webhooks/parsers/linear.d.ts.map +1 -0
  235. package/dist/webhooks/parsers/linear.js +258 -0
  236. package/dist/webhooks/parsers/linear.js.map +1 -0
  237. package/dist/webhooks/parsers/slack.d.ts +9 -0
  238. package/dist/webhooks/parsers/slack.d.ts.map +1 -0
  239. package/dist/webhooks/parsers/slack.js +214 -0
  240. package/dist/webhooks/parsers/slack.js.map +1 -0
  241. package/dist/webhooks/responders/github.d.ts +8 -0
  242. package/dist/webhooks/responders/github.d.ts.map +1 -0
  243. package/dist/webhooks/responders/github.js +73 -0
  244. package/dist/webhooks/responders/github.js.map +1 -0
  245. package/dist/webhooks/responders/index.d.ts +23 -0
  246. package/dist/webhooks/responders/index.d.ts.map +1 -0
  247. package/dist/webhooks/responders/index.js +30 -0
  248. package/dist/webhooks/responders/index.js.map +1 -0
  249. package/dist/webhooks/responders/linear.d.ts +9 -0
  250. package/dist/webhooks/responders/linear.d.ts.map +1 -0
  251. package/dist/webhooks/responders/linear.js +149 -0
  252. package/dist/webhooks/responders/linear.js.map +1 -0
  253. package/dist/webhooks/responders/slack.d.ts +20 -0
  254. package/dist/webhooks/responders/slack.d.ts.map +1 -0
  255. package/dist/webhooks/responders/slack.js +178 -0
  256. package/dist/webhooks/responders/slack.js.map +1 -0
  257. package/dist/webhooks/router.d.ts +25 -0
  258. package/dist/webhooks/router.d.ts.map +1 -0
  259. package/dist/webhooks/router.js +504 -0
  260. package/dist/webhooks/router.js.map +1 -0
  261. package/dist/webhooks/rules-engine.d.ts +24 -0
  262. package/dist/webhooks/rules-engine.d.ts.map +1 -0
  263. package/dist/webhooks/rules-engine.js +287 -0
  264. package/dist/webhooks/rules-engine.js.map +1 -0
  265. package/dist/webhooks/types.d.ts +186 -0
  266. package/dist/webhooks/types.d.ts.map +1 -0
  267. package/dist/webhooks/types.js +8 -0
  268. package/dist/webhooks/types.js.map +1 -0
  269. package/package.json +55 -0
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Nango Auth API Routes
3
+ *
4
+ * Handles GitHub OAuth via Nango with two-connection pattern:
5
+ * - github: User login (identity)
6
+ * - github-app-oauth: Repository access
7
+ */
8
+ import { Router } from 'express';
9
+ import { randomUUID } from 'crypto';
10
+ import { requireAuth } from './auth.js';
11
+ import { db } from '../db/index.js';
12
+ import { nangoService, NANGO_INTEGRATIONS } from '../services/nango.js';
13
+ export const nangoAuthRouter = Router();
14
+ /**
15
+ * GET /api/auth/nango/status
16
+ * Check if Nango is configured
17
+ */
18
+ nangoAuthRouter.get('/status', (req, res) => {
19
+ try {
20
+ res.json({
21
+ configured: true,
22
+ integrations: NANGO_INTEGRATIONS,
23
+ });
24
+ }
25
+ catch (_error) {
26
+ res.json({
27
+ configured: false,
28
+ message: 'Nango not configured',
29
+ });
30
+ }
31
+ });
32
+ /**
33
+ * GET /api/auth/nango/login-session
34
+ * Create a Nango connect session for GitHub login
35
+ */
36
+ nangoAuthRouter.get('/login-session', async (req, res) => {
37
+ try {
38
+ const tempUserId = randomUUID();
39
+ const session = await nangoService.createConnectSession([NANGO_INTEGRATIONS.GITHUB_USER], { id: tempUserId });
40
+ res.json({ sessionToken: session.token, tempUserId });
41
+ }
42
+ catch (error) {
43
+ console.error('Error creating login session:', error);
44
+ res.status(500).json({ error: 'Failed to create login session' });
45
+ }
46
+ });
47
+ /**
48
+ * GET /api/auth/nango/login-status/:connectionId
49
+ * Poll for login completion after Nango connect UI
50
+ */
51
+ nangoAuthRouter.get('/login-status/:connectionId', async (req, res) => {
52
+ const connectionId = req.params.connectionId;
53
+ try {
54
+ // Check if a user exists with this incoming connection
55
+ const user = await db.users.findByIncomingConnectionId(connectionId);
56
+ if (!user) {
57
+ return res.json({ ready: false });
58
+ }
59
+ // Issue session
60
+ req.session.userId = user.id;
61
+ // Clear incoming connection ID
62
+ await db.users.clearIncomingConnectionId(user.id);
63
+ // Check if user has any repos connected
64
+ const repos = await db.repositories.findByUserId(user.id);
65
+ const hasRepos = repos.length > 0;
66
+ res.json({
67
+ ready: true,
68
+ hasRepos,
69
+ user: {
70
+ id: user.id,
71
+ githubUsername: user.githubUsername,
72
+ email: user.email,
73
+ avatarUrl: user.avatarUrl,
74
+ plan: user.plan,
75
+ },
76
+ });
77
+ }
78
+ catch (error) {
79
+ console.error('Error checking login status:', error);
80
+ res.status(500).json({ error: 'Failed to check login status' });
81
+ }
82
+ });
83
+ /**
84
+ * GET /api/auth/nango/repo-session
85
+ * Create a Nango connect session for GitHub App OAuth (repo access)
86
+ * Requires authentication
87
+ */
88
+ nangoAuthRouter.get('/repo-session', requireAuth, async (req, res) => {
89
+ const userId = req.session.userId;
90
+ try {
91
+ const user = await db.users.findById(userId);
92
+ if (!user) {
93
+ return res.status(404).json({ error: 'User not found' });
94
+ }
95
+ const session = await nangoService.createConnectSession([NANGO_INTEGRATIONS.GITHUB_APP], { id: user.id, email: user.email || undefined });
96
+ res.json({ sessionToken: session.token });
97
+ }
98
+ catch (error) {
99
+ console.error('Error creating repo session:', error);
100
+ res.status(500).json({ error: 'Failed to create repo session' });
101
+ }
102
+ });
103
+ /**
104
+ * GET /api/auth/nango/repo-status/:connectionId
105
+ * Poll for repo sync completion after GitHub App OAuth
106
+ * Requires authentication
107
+ */
108
+ nangoAuthRouter.get('/repo-status/:connectionId', requireAuth, async (req, res) => {
109
+ const userId = req.session.userId;
110
+ const _connectionId = req.params.connectionId;
111
+ try {
112
+ const user = await db.users.findById(userId);
113
+ if (!user) {
114
+ return res.status(404).json({ error: 'User not found' });
115
+ }
116
+ // Check for pending org approval
117
+ if (user.pendingInstallationRequest) {
118
+ return res.json({
119
+ ready: false,
120
+ pendingApproval: true,
121
+ message: 'Waiting for organization admin approval',
122
+ });
123
+ }
124
+ // Check if repos have been synced
125
+ const repos = await db.repositories.findByUserId(userId);
126
+ const reposFromConnection = repos.filter(r => r.syncStatus === 'synced' && r.nangoConnectionId);
127
+ if (reposFromConnection.length === 0) {
128
+ return res.json({ ready: false });
129
+ }
130
+ // Check workspace status for frontend visibility
131
+ const workspaces = await db.workspaces.findByUserId(userId);
132
+ const primaryWorkspace = workspaces[0];
133
+ res.json({
134
+ ready: true,
135
+ repos: reposFromConnection.map(r => ({
136
+ id: r.id,
137
+ fullName: r.githubFullName,
138
+ isPrivate: r.isPrivate,
139
+ defaultBranch: r.defaultBranch,
140
+ })),
141
+ workspace: primaryWorkspace ? {
142
+ id: primaryWorkspace.id,
143
+ name: primaryWorkspace.name,
144
+ status: primaryWorkspace.status,
145
+ publicUrl: primaryWorkspace.publicUrl,
146
+ } : null,
147
+ workspaceProvisioning: primaryWorkspace?.status === 'provisioning',
148
+ });
149
+ }
150
+ catch (error) {
151
+ console.error('Error checking repo status:', error);
152
+ res.status(500).json({ error: 'Failed to check repo status' });
153
+ }
154
+ });
155
+ // ============================================================================
156
+ // Nango Webhook Handler
157
+ // ============================================================================
158
+ /**
159
+ * POST /api/auth/nango/webhook
160
+ * Handle Nango webhooks for auth and sync events
161
+ */
162
+ nangoAuthRouter.post('/webhook', async (req, res) => {
163
+ const rawBody = req.rawBody || JSON.stringify(req.body);
164
+ // Verify webhook signature if present
165
+ const hasSignature = req.headers['x-nango-signature'] || req.headers['x-nango-hmac-sha256'];
166
+ if (hasSignature) {
167
+ if (!nangoService.verifyWebhookSignature(rawBody, req.headers)) {
168
+ console.error('[nango-webhook] Invalid signature');
169
+ return res.status(401).json({ error: 'Invalid signature' });
170
+ }
171
+ }
172
+ const payload = req.body;
173
+ console.log(`[nango-webhook] Received ${payload.type} event`);
174
+ try {
175
+ switch (payload.type) {
176
+ case 'auth':
177
+ await handleAuthWebhook(payload);
178
+ break;
179
+ case 'sync':
180
+ console.log('[nango-webhook] Sync event received');
181
+ break;
182
+ case 'forward':
183
+ await handleForwardWebhook(payload);
184
+ break;
185
+ default:
186
+ console.log(`[nango-webhook] Unhandled event type: ${payload.type}`);
187
+ }
188
+ res.json({ success: true });
189
+ }
190
+ catch (error) {
191
+ console.error('[nango-webhook] Error processing webhook:', error);
192
+ res.status(500).json({ error: 'Failed to process webhook' });
193
+ }
194
+ });
195
+ /**
196
+ * Handle Nango auth webhook
197
+ */
198
+ async function handleAuthWebhook(payload) {
199
+ const { connectionId, providerConfigKey, endUser } = payload;
200
+ console.log(`[nango-webhook] Auth event for ${providerConfigKey} (${connectionId})`);
201
+ if (providerConfigKey === NANGO_INTEGRATIONS.GITHUB_USER) {
202
+ await handleLoginWebhook(connectionId, endUser);
203
+ }
204
+ else if (providerConfigKey === NANGO_INTEGRATIONS.GITHUB_APP) {
205
+ await handleRepoAuthWebhook(connectionId, endUser);
206
+ }
207
+ }
208
+ /**
209
+ * Check user's repo access and auto-add them to workspaces
210
+ * Uses GitHub user OAuth to query accessible repos and persists them to database
211
+ */
212
+ async function checkAndAutoAddToWorkspaces(userId, connectionId) {
213
+ try {
214
+ const user = await db.users.findById(userId);
215
+ if (!user)
216
+ return;
217
+ console.log(`[nango-webhook] Checking workspace auto-add for ${user.githubUsername}`);
218
+ // Query repos the user has access to via GitHub OAuth
219
+ const { repositories } = await nangoService.listUserAccessibleRepos(connectionId, {
220
+ perPage: 100,
221
+ type: 'all',
222
+ });
223
+ const workspacesToJoin = new Set();
224
+ // Check for workspace memberships - only persist repos that match existing workspaces
225
+ for (const repo of repositories) {
226
+ // Check if any user has this repo linked to a workspace
227
+ const allRepoRecords = await db.repositories.findByGithubFullName(repo.fullName);
228
+ let matchedWorkspaceId = null;
229
+ for (const record of allRepoRecords) {
230
+ if (record.workspaceId) {
231
+ workspacesToJoin.add(record.workspaceId);
232
+ matchedWorkspaceId = record.workspaceId; // Save the workspaceId to copy
233
+ }
234
+ }
235
+ // Only persist repos that are linked to workspaces
236
+ if (matchedWorkspaceId) {
237
+ await db.repositories.upsert({
238
+ userId: user.id,
239
+ githubFullName: repo.fullName,
240
+ githubId: repo.id,
241
+ isPrivate: repo.isPrivate,
242
+ defaultBranch: repo.defaultBranch,
243
+ nangoConnectionId: connectionId,
244
+ workspaceId: matchedWorkspaceId, // Copy the workspaceId
245
+ syncStatus: 'synced',
246
+ lastSyncedAt: new Date(),
247
+ });
248
+ }
249
+ }
250
+ // Auto-add user to workspaces
251
+ for (const workspaceId of workspacesToJoin) {
252
+ const existingMembership = await db.workspaceMembers.findMembership(workspaceId, userId);
253
+ if (!existingMembership) {
254
+ const workspace = await db.workspaces.findById(workspaceId);
255
+ if (workspace) {
256
+ console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
257
+ await db.workspaceMembers.addMember({
258
+ workspaceId,
259
+ userId,
260
+ role: 'member',
261
+ invitedBy: workspace.userId,
262
+ });
263
+ await db.workspaceMembers.acceptInvite(workspaceId, userId);
264
+ }
265
+ }
266
+ }
267
+ console.log(`[nango-webhook] Synced ${repositories.length} repos, auto-added ${user.githubUsername} to ${workspacesToJoin.size} workspaces`);
268
+ }
269
+ catch (error) {
270
+ console.error(`[nango-webhook] Error checking workspace auto-add:`, error);
271
+ // Non-fatal - don't throw
272
+ }
273
+ }
274
+ /**
275
+ * Handle GitHub login webhook
276
+ *
277
+ * Three scenarios:
278
+ * 1. New user - Create user record, keep connection as permanent
279
+ * 2. Returning user with existing connection - Store incoming ID for polling, delete temp connection
280
+ * 3. Existing user, first connection - Set connection ID as permanent
281
+ */
282
+ async function handleLoginWebhook(connectionId, _endUser) {
283
+ // Get GitHub user info via Nango proxy
284
+ const githubUser = await nangoService.getGithubUser(connectionId);
285
+ const githubId = String(githubUser.id);
286
+ // Check if user already exists
287
+ const existingUser = await db.users.findByGithubId(githubId);
288
+ // SCENARIO 1: New user
289
+ if (!existingUser) {
290
+ const newUser = await db.users.upsert({
291
+ githubId,
292
+ githubUsername: githubUser.login,
293
+ email: githubUser.email || null,
294
+ avatarUrl: githubUser.avatar_url || null,
295
+ nangoConnectionId: connectionId,
296
+ incomingConnectionId: connectionId,
297
+ });
298
+ // Update connection with real user ID
299
+ await nangoService.updateEndUser(connectionId, NANGO_INTEGRATIONS.GITHUB_USER, {
300
+ id: newUser.id,
301
+ email: newUser.email || undefined,
302
+ });
303
+ console.log(`[nango-webhook] New user created: ${githubUser.login}`);
304
+ // Check for auto-add to workspaces based on repo access
305
+ await checkAndAutoAddToWorkspaces(newUser.id, connectionId);
306
+ return;
307
+ }
308
+ // SCENARIO 2: Returning user with existing connection - delete temp connection
309
+ if (existingUser.nangoConnectionId && existingUser.nangoConnectionId !== connectionId) {
310
+ console.log(`[nango-webhook] Returning user: ${githubUser.login}`, {
311
+ permanentConnectionId: existingUser.nangoConnectionId,
312
+ incomingConnectionId: connectionId,
313
+ });
314
+ // Store incoming connection ID for polling
315
+ await db.users.update(existingUser.id, {
316
+ incomingConnectionId: connectionId,
317
+ githubUsername: githubUser.login,
318
+ avatarUrl: githubUser.avatar_url || null,
319
+ });
320
+ // Delete the temporary connection from Nango to prevent duplicates
321
+ try {
322
+ await nangoService.deleteConnection(connectionId, NANGO_INTEGRATIONS.GITHUB_USER);
323
+ console.log(`[nango-webhook] Deleted temp connection for returning user`);
324
+ }
325
+ catch (error) {
326
+ console.error(`[nango-webhook] Failed to delete temp connection:`, error);
327
+ // Non-fatal - continue anyway
328
+ }
329
+ // Check for auto-add using permanent connection
330
+ await checkAndAutoAddToWorkspaces(existingUser.id, existingUser.nangoConnectionId);
331
+ return;
332
+ }
333
+ // SCENARIO 3: Existing user, first connection (or same connection)
334
+ console.log(`[nango-webhook] First/same connection for existing user: ${githubUser.login}`);
335
+ await db.users.update(existingUser.id, {
336
+ nangoConnectionId: connectionId,
337
+ incomingConnectionId: connectionId,
338
+ githubUsername: githubUser.login,
339
+ avatarUrl: githubUser.avatar_url || null,
340
+ });
341
+ // Update connection with user ID
342
+ await nangoService.updateEndUser(connectionId, NANGO_INTEGRATIONS.GITHUB_USER, {
343
+ id: existingUser.id,
344
+ email: existingUser.email || undefined,
345
+ });
346
+ // Check for auto-add to workspaces
347
+ await checkAndAutoAddToWorkspaces(existingUser.id, connectionId);
348
+ }
349
+ /**
350
+ * Handle Nango forward webhook (GitHub events forwarded by Nango)
351
+ */
352
+ async function handleForwardWebhook(payload) {
353
+ const githubPayload = payload.payload;
354
+ console.log(`[nango-webhook] Forward event: action=${githubPayload.action} from ${payload.providerConfigKey}`);
355
+ // Only process GitHub App events
356
+ if (payload.providerConfigKey !== NANGO_INTEGRATIONS.GITHUB_APP) {
357
+ console.log('[nango-webhook] Ignoring forward event from non-GitHub-App integration');
358
+ return;
359
+ }
360
+ try {
361
+ // Determine event type from payload structure
362
+ if (githubPayload.installation && githubPayload.action === 'created' && githubPayload.repositories) {
363
+ // Installation created event
364
+ await handleInstallationForward(githubPayload, payload.connectionId);
365
+ }
366
+ else if (githubPayload.repositories_added || githubPayload.repositories_removed) {
367
+ // Installation repositories added/removed
368
+ await handleInstallationRepositoriesForward(githubPayload, payload.connectionId);
369
+ }
370
+ else {
371
+ console.log(`[nango-webhook] Unhandled forward event structure: action=${githubPayload.action}`);
372
+ }
373
+ }
374
+ catch (error) {
375
+ console.error(`[nango-webhook] Error processing forward event:`, error);
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * Handle GitHub installation events forwarded by Nango
381
+ */
382
+ async function handleInstallationForward(body, connectionId) {
383
+ const { action, installation, repositories, sender } = body;
384
+ if (!installation || !sender)
385
+ return;
386
+ const installationId = String(installation.id);
387
+ console.log(`[nango-webhook] Installation ${action}: ${installation.account.login} (${installationId})`);
388
+ if (action === 'created') {
389
+ // Find user by GitHub ID
390
+ const user = await db.users.findByGithubId(String(sender.id));
391
+ // Create/update installation record
392
+ await db.githubInstallations.upsert({
393
+ installationId,
394
+ accountType: installation.account.type.toLowerCase(),
395
+ accountLogin: installation.account.login,
396
+ accountId: String(installation.account.id),
397
+ installedById: user?.id ?? null,
398
+ permissions: installation.permissions,
399
+ events: installation.events,
400
+ });
401
+ // Sync repositories if provided
402
+ if (repositories && user) {
403
+ const dbInstallation = await db.githubInstallations.findByInstallationId(installationId);
404
+ if (dbInstallation) {
405
+ const workspacesToJoin = new Set();
406
+ for (const repo of repositories) {
407
+ const syncedRepo = await db.repositories.upsert({
408
+ userId: user.id,
409
+ githubFullName: repo.full_name,
410
+ githubId: repo.id,
411
+ isPrivate: repo.private,
412
+ installationId: dbInstallation.id,
413
+ nangoConnectionId: connectionId,
414
+ syncStatus: 'synced',
415
+ lastSyncedAt: new Date(),
416
+ });
417
+ // Check if repo is part of an existing workspace
418
+ // Look for ANY user's record of this repo that has a workspaceId
419
+ if (syncedRepo.workspaceId) {
420
+ workspacesToJoin.add(syncedRepo.workspaceId);
421
+ }
422
+ else {
423
+ // Check if other users have this repo linked to a workspace
424
+ const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
425
+ for (const otherRecord of allRepoRecords) {
426
+ if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
427
+ workspacesToJoin.add(otherRecord.workspaceId);
428
+ }
429
+ }
430
+ }
431
+ }
432
+ // Auto-join user to workspaces for repos they have access to
433
+ for (const workspaceId of workspacesToJoin) {
434
+ const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
435
+ if (!existingMembership) {
436
+ const workspace = await db.workspaces.findById(workspaceId);
437
+ if (workspace) {
438
+ console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
439
+ await db.workspaceMembers.addMember({
440
+ workspaceId,
441
+ userId: user.id,
442
+ role: 'member',
443
+ invitedBy: workspace.userId,
444
+ });
445
+ await db.workspaceMembers.acceptInvite(workspaceId, user.id);
446
+ }
447
+ }
448
+ }
449
+ console.log(`[nango-webhook] Installation created for ${installation.account.login}, auto-joined ${workspacesToJoin.size} workspaces`);
450
+ }
451
+ }
452
+ }
453
+ }
454
+ /**
455
+ * Handle installation_repositories events forwarded by Nango
456
+ */
457
+ async function handleInstallationRepositoriesForward(body, connectionId) {
458
+ const { action, installation, repositories_added, repositories_removed, sender } = body;
459
+ console.log(`[nango-webhook] handleInstallationRepositoriesForward called`, {
460
+ action,
461
+ installation: installation?.id,
462
+ sender: sender?.login,
463
+ connectionId,
464
+ repositories_added: repositories_added?.map(r => r.full_name),
465
+ repositories_removed: repositories_removed?.map(r => r.full_name),
466
+ });
467
+ if (!installation || !sender) {
468
+ console.log(`[nango-webhook] Missing installation or sender, skipping`);
469
+ return;
470
+ }
471
+ const installationId = String(installation.id);
472
+ console.log(`[nango-webhook] Repositories ${action} for ${installation.account.login}`);
473
+ // Find installation in database
474
+ const dbInstallation = await db.githubInstallations.findByInstallationId(installationId);
475
+ if (!dbInstallation) {
476
+ console.error(`[nango-webhook] Installation ${installationId} not found in database`);
477
+ return;
478
+ }
479
+ console.log(`[nango-webhook] Found installation: ${dbInstallation.id}`);
480
+ // Find user who triggered this
481
+ const user = await db.users.findByGithubId(String(sender.id));
482
+ if (!user) {
483
+ console.error(`[nango-webhook] User ${sender.login} (github id: ${sender.id}) not found in database`);
484
+ return;
485
+ }
486
+ console.log(`[nango-webhook] Found user: ${user.id} (${user.githubUsername})`);
487
+ console.log(`[nango-webhook] Processing action: ${action}, repos_added: ${repositories_added?.length}, repos_removed: ${repositories_removed?.length}`);
488
+ if (action === 'added' && repositories_added) {
489
+ console.log(`[nango-webhook] Adding ${repositories_added.length} repos for user ${user.id}`);
490
+ const workspacesToJoin = new Set();
491
+ for (const repo of repositories_added) {
492
+ console.log(`[nango-webhook] Upserting repo: ${repo.full_name}`);
493
+ const syncedRepo = await db.repositories.upsert({
494
+ userId: user.id,
495
+ githubFullName: repo.full_name,
496
+ githubId: repo.id,
497
+ isPrivate: repo.private,
498
+ installationId: dbInstallation.id,
499
+ nangoConnectionId: connectionId,
500
+ syncStatus: 'synced',
501
+ lastSyncedAt: new Date(),
502
+ });
503
+ console.log(`[nango-webhook] Upserted repo: ${syncedRepo.id}, syncStatus: ${syncedRepo.syncStatus}, nangoConnectionId: ${syncedRepo.nangoConnectionId}`);
504
+ // Check if repo is part of an existing workspace
505
+ // Look for ANY user's record of this repo that has a workspaceId
506
+ if (syncedRepo.workspaceId) {
507
+ workspacesToJoin.add(syncedRepo.workspaceId);
508
+ }
509
+ else {
510
+ // Check if other users have this repo linked to a workspace
511
+ const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
512
+ for (const otherRecord of allRepoRecords) {
513
+ if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
514
+ workspacesToJoin.add(otherRecord.workspaceId);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ // Auto-join user to workspaces for repos they have access to
520
+ for (const workspaceId of workspacesToJoin) {
521
+ const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
522
+ if (!existingMembership) {
523
+ const workspace = await db.workspaces.findById(workspaceId);
524
+ if (workspace) {
525
+ console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
526
+ await db.workspaceMembers.addMember({
527
+ workspaceId,
528
+ userId: user.id,
529
+ role: 'member',
530
+ invitedBy: workspace.userId,
531
+ });
532
+ await db.workspaceMembers.acceptInvite(workspaceId, user.id);
533
+ }
534
+ }
535
+ }
536
+ console.log(`[nango-webhook] Added ${repositories_added.length} repositories, auto-joined ${workspacesToJoin.size} workspaces`);
537
+ }
538
+ if (action === 'removed' && repositories_removed) {
539
+ for (const repo of repositories_removed) {
540
+ const repos = await db.repositories.findByUserId(user.id);
541
+ const existingRepo = repos.find(r => r.githubFullName === repo.full_name);
542
+ if (existingRepo) {
543
+ await db.repositories.updateSyncStatus(existingRepo.id, 'access_removed');
544
+ }
545
+ }
546
+ console.log(`[nango-webhook] Removed access to ${repositories_removed.length} repositories`);
547
+ }
548
+ }
549
+ /**
550
+ * Handle GitHub App OAuth webhook (repo access)
551
+ */
552
+ async function handleRepoAuthWebhook(connectionId, endUser) {
553
+ let userId = endUser?.id;
554
+ // Fallback: If endUser.id not in webhook, fetch connection metadata from Nango
555
+ if (!userId) {
556
+ console.log('[nango-webhook] No user ID in webhook payload, fetching from connection metadata...');
557
+ try {
558
+ const connection = await nangoService.getConnection(connectionId, NANGO_INTEGRATIONS.GITHUB_APP);
559
+ userId = connection.end_user?.id;
560
+ console.log(`[nango-webhook] Got user ID from connection: ${userId || 'not found'}`);
561
+ }
562
+ catch (err) {
563
+ console.error('[nango-webhook] Failed to fetch connection metadata:', err);
564
+ }
565
+ }
566
+ if (!userId) {
567
+ console.error('[nango-webhook] No user ID found - cannot sync repos');
568
+ return;
569
+ }
570
+ const user = await db.users.findById(userId);
571
+ if (!user) {
572
+ console.error(`[nango-webhook] User ${userId} not found`);
573
+ return;
574
+ }
575
+ try {
576
+ // Get the GitHub App installation ID
577
+ const githubInstallationId = await nangoService.getGithubAppInstallationId(connectionId);
578
+ let installationUuid = null;
579
+ if (githubInstallationId) {
580
+ // Find or create the github_installations record
581
+ let installation = await db.githubInstallations.findByInstallationId(String(githubInstallationId));
582
+ if (!installation) {
583
+ // Create a new installation record
584
+ // We need to get more info about the installation - for now use user info
585
+ installation = await db.githubInstallations.upsert({
586
+ installationId: String(githubInstallationId),
587
+ accountType: 'user', // Could be 'organization' - we'd need to detect this
588
+ accountLogin: user.githubUsername || 'unknown',
589
+ accountId: user.githubId || 'unknown',
590
+ installedById: user.id,
591
+ permissions: {},
592
+ events: [],
593
+ });
594
+ console.log(`[nango-webhook] Created installation record for ${githubInstallationId}`);
595
+ }
596
+ installationUuid = installation.id;
597
+ }
598
+ else {
599
+ console.warn('[nango-webhook] Could not get installation ID from Nango connection');
600
+ }
601
+ // Fetch repos the user has access to
602
+ const { repositories: repos } = await nangoService.listGithubAppRepos(connectionId);
603
+ // Track workspaces to auto-join
604
+ const workspacesToJoin = new Set();
605
+ // Sync repos to database
606
+ for (const repo of repos) {
607
+ const syncedRepo = await db.repositories.upsert({
608
+ userId: user.id,
609
+ githubFullName: repo.full_name,
610
+ githubId: repo.id,
611
+ isPrivate: repo.private,
612
+ defaultBranch: repo.default_branch,
613
+ nangoConnectionId: connectionId,
614
+ installationId: installationUuid,
615
+ syncStatus: 'synced',
616
+ lastSyncedAt: new Date(),
617
+ });
618
+ // Check if this repo is part of an existing workspace
619
+ // Look for ANY user's record of this repo that has a workspaceId
620
+ if (syncedRepo.workspaceId) {
621
+ workspacesToJoin.add(syncedRepo.workspaceId);
622
+ }
623
+ else {
624
+ // Check if other users have this repo linked to a workspace
625
+ const allRepoRecords = await db.repositories.findByGithubFullName(repo.full_name);
626
+ for (const otherRecord of allRepoRecords) {
627
+ if (otherRecord.workspaceId && otherRecord.userId !== user.id) {
628
+ workspacesToJoin.add(otherRecord.workspaceId);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ // Auto-join user to workspaces for repos they have access to
634
+ for (const workspaceId of workspacesToJoin) {
635
+ // Check if already a member
636
+ const existingMembership = await db.workspaceMembers.findMembership(workspaceId, user.id);
637
+ if (!existingMembership) {
638
+ // Get workspace owner to use as invitedBy
639
+ const workspace = await db.workspaces.findById(workspaceId);
640
+ if (workspace) {
641
+ console.log(`[nango-webhook] Auto-adding ${user.githubUsername} to workspace ${workspace.name}`);
642
+ await db.workspaceMembers.addMember({
643
+ workspaceId,
644
+ userId: user.id,
645
+ role: 'member',
646
+ invitedBy: workspace.userId, // Workspace owner invited them
647
+ });
648
+ // Auto-accept since they have GitHub repo access
649
+ await db.workspaceMembers.acceptInvite(workspaceId, user.id);
650
+ }
651
+ }
652
+ }
653
+ // Clear any pending installation request
654
+ await db.users.clearPendingInstallationRequest(user.id);
655
+ console.log(`[nango-webhook] Synced ${repos.length} repos for ${user.githubUsername} (installation: ${githubInstallationId || 'unknown'}), auto-joined ${workspacesToJoin.size} workspaces`);
656
+ // Note: We intentionally do NOT auto-provision workspaces here.
657
+ // Users should go through the onboarding flow at /app to:
658
+ // 1. Name their workspace
659
+ // 2. Choose which repos to include
660
+ // 3. Understand what they're creating
661
+ }
662
+ catch (error) {
663
+ const err = error;
664
+ if (err.message?.includes('403')) {
665
+ // Org approval pending
666
+ await db.users.setPendingInstallationRequest(user.id);
667
+ console.log(`[nango-webhook] Org approval pending for ${user.githubUsername}`);
668
+ }
669
+ else {
670
+ throw error;
671
+ }
672
+ }
673
+ }
674
+ //# sourceMappingURL=nango-auth.js.map