agent-relay 2.0.16 → 2.0.18

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 (153) hide show
  1. package/deploy/workspace/entrypoint.sh +35 -19
  2. package/deploy/workspace/git-credential-relay +82 -7
  3. package/dist/dashboard/out/404.html +1 -1
  4. package/dist/dashboard/out/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  5. package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  6. package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js → dist/dashboard/out/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  7. package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  8. package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  9. package/dist/dashboard/out/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  10. package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  11. package/dist/dashboard/out/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  12. package/dist/dashboard/out/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  13. package/dist/dashboard/out/app/onboarding.html +1 -1
  14. package/dist/dashboard/out/app/onboarding.txt +1 -1
  15. package/dist/dashboard/out/app.html +1 -1
  16. package/dist/dashboard/out/app.txt +2 -2
  17. package/dist/dashboard/out/cloud/link.html +1 -1
  18. package/dist/dashboard/out/cloud/link.txt +1 -1
  19. package/dist/dashboard/out/complete-profile.html +5 -0
  20. package/dist/dashboard/out/complete-profile.txt +7 -0
  21. package/dist/dashboard/out/connect-repos.html +1 -1
  22. package/dist/dashboard/out/connect-repos.txt +1 -1
  23. package/dist/dashboard/out/history.html +1 -1
  24. package/dist/dashboard/out/history.txt +1 -1
  25. package/dist/dashboard/out/index.html +1 -1
  26. package/dist/dashboard/out/index.txt +2 -2
  27. package/dist/dashboard/out/login.html +2 -2
  28. package/dist/dashboard/out/login.txt +2 -2
  29. package/dist/dashboard/out/metrics.html +1 -1
  30. package/dist/dashboard/out/metrics.txt +1 -1
  31. package/dist/dashboard/out/pricing.html +2 -2
  32. package/dist/dashboard/out/pricing.txt +1 -1
  33. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  34. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  35. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  36. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  37. package/dist/dashboard/out/providers/setup/cursor.html +1 -1
  38. package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
  39. package/dist/dashboard/out/providers.html +1 -1
  40. package/dist/dashboard/out/providers.txt +2 -2
  41. package/dist/dashboard/out/signup.html +2 -2
  42. package/dist/dashboard/out/signup.txt +2 -2
  43. package/dist/src/cli/index.js +3 -1
  44. package/package.json +22 -21
  45. package/packages/api-types/package.json +1 -1
  46. package/packages/bridge/package.json +8 -8
  47. package/packages/cloud/dist/api/auth.js +2 -0
  48. package/packages/cloud/dist/api/billing.js +4 -4
  49. package/packages/cloud/dist/api/email-auth.d.ts +11 -0
  50. package/packages/cloud/dist/api/email-auth.js +347 -0
  51. package/packages/cloud/dist/api/nango-auth.js +72 -5
  52. package/packages/cloud/dist/db/drizzle.d.ts +35 -1
  53. package/packages/cloud/dist/db/drizzle.js +136 -0
  54. package/packages/cloud/dist/db/index.d.ts +5 -4
  55. package/packages/cloud/dist/db/index.js +5 -3
  56. package/packages/cloud/dist/db/schema.d.ts +246 -2
  57. package/packages/cloud/dist/db/schema.js +39 -3
  58. package/packages/cloud/dist/provisioner/index.js +5 -1
  59. package/packages/cloud/dist/server.js +134 -24
  60. package/packages/cloud/dist/services/nango.d.ts +18 -0
  61. package/packages/cloud/dist/services/nango.js +32 -0
  62. package/packages/cloud/package.json +6 -6
  63. package/packages/config/package.json +2 -2
  64. package/packages/continuity/package.json +1 -1
  65. package/packages/daemon/package.json +12 -12
  66. package/packages/dashboard/dist/server.js +36 -7
  67. package/packages/dashboard/package.json +13 -13
  68. package/packages/dashboard/ui/app/complete-profile/page.tsx +204 -0
  69. package/packages/dashboard/ui/app/login/page.tsx +182 -38
  70. package/packages/dashboard/ui/app/signup/page.tsx +244 -54
  71. package/packages/dashboard/ui/lib/cloudApi.ts +1 -0
  72. package/packages/dashboard/ui/react-components/App.tsx +1 -1
  73. package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +10 -0
  74. package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +160 -3
  75. package/packages/dashboard/ui-dist/404.html +1 -1
  76. package/packages/dashboard/ui-dist/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  77. package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  78. package/{dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  79. package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  80. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  81. package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  82. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  83. package/packages/dashboard/ui-dist/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  84. package/packages/dashboard/ui-dist/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  85. package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
  86. package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
  87. package/packages/dashboard/ui-dist/app.html +1 -1
  88. package/packages/dashboard/ui-dist/app.txt +2 -2
  89. package/packages/dashboard/ui-dist/cloud/link.html +1 -1
  90. package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
  91. package/packages/dashboard/ui-dist/complete-profile.html +5 -0
  92. package/packages/dashboard/ui-dist/complete-profile.txt +7 -0
  93. package/packages/dashboard/ui-dist/connect-repos.html +1 -1
  94. package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
  95. package/packages/dashboard/ui-dist/history.html +1 -1
  96. package/packages/dashboard/ui-dist/history.txt +1 -1
  97. package/packages/dashboard/ui-dist/index.html +1 -1
  98. package/packages/dashboard/ui-dist/index.txt +2 -2
  99. package/packages/dashboard/ui-dist/login.html +2 -2
  100. package/packages/dashboard/ui-dist/login.txt +2 -2
  101. package/packages/dashboard/ui-dist/metrics.html +1 -1
  102. package/packages/dashboard/ui-dist/metrics.txt +1 -1
  103. package/packages/dashboard/ui-dist/pricing.html +2 -2
  104. package/packages/dashboard/ui-dist/pricing.txt +1 -1
  105. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
  106. package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
  107. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
  108. package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
  109. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
  110. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
  111. package/packages/dashboard/ui-dist/providers.html +1 -1
  112. package/packages/dashboard/ui-dist/providers.txt +2 -2
  113. package/packages/dashboard/ui-dist/signup.html +2 -2
  114. package/packages/dashboard/ui-dist/signup.txt +2 -2
  115. package/packages/dashboard-server/dist/server.js +36 -7
  116. package/packages/dashboard-server/package.json +12 -12
  117. package/packages/hooks/package.json +4 -4
  118. package/packages/mcp/package.json +2 -2
  119. package/packages/memory/package.json +2 -2
  120. package/packages/policy/package.json +2 -2
  121. package/packages/protocol/package.json +1 -1
  122. package/packages/resiliency/package.json +1 -1
  123. package/packages/sdk/package.json +2 -2
  124. package/packages/spawner/package.json +1 -1
  125. package/packages/state/package.json +1 -1
  126. package/packages/storage/package.json +2 -2
  127. package/packages/telemetry/package.json +1 -1
  128. package/packages/trajectory/package.json +2 -2
  129. package/packages/user-directory/package.json +2 -2
  130. package/packages/utils/package.json +1 -1
  131. package/packages/wrapper/dist/relay-pty-orchestrator.js +17 -3
  132. package/packages/wrapper/package.json +6 -6
  133. package/relay-snippets/agent-policy-snippet.md +40 -0
  134. package/relay-snippets/agent-relay-protocol.md +101 -0
  135. package/relay-snippets/agent-relay-snippet.md +177 -0
  136. package/SESSION_HANDOFF.md +0 -67
  137. package/dist/dashboard/out/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  138. package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  139. package/dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  140. package/dist/dashboard/out/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  141. package/packages/dashboard/ui-dist/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  142. package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  143. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  144. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  145. package/test-push.txt +0 -1
  146. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  147. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  148. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  149. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  150. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_buildManifest.js +0 -0
  151. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_ssgManifest.js +0 -0
  152. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_buildManifest.js +0 -0
  153. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_ssgManifest.js +0 -0
@@ -63,9 +63,12 @@ nangoAuthRouter.get('/login-status/:connectionId', async (req, res) => {
63
63
  // Check if user has any repos connected
64
64
  const repos = await db.repositories.findByUserId(user.id);
65
65
  const hasRepos = repos.length > 0;
66
+ // Check if user needs to provide an email
67
+ const needsEmail = !user.email;
66
68
  res.json({
67
69
  ready: true,
68
70
  hasRepos,
71
+ needsEmail,
69
72
  user: {
70
73
  id: user.id,
71
74
  githubUsername: user.githubUsername,
@@ -271,6 +274,32 @@ async function checkAndAutoAddToWorkspaces(userId, connectionId) {
271
274
  // Non-fatal - don't throw
272
275
  }
273
276
  }
277
+ /**
278
+ * Fetch and sync user emails from GitHub
279
+ * Requires 'user:email' scope to be configured in Nango's GitHub integration
280
+ */
281
+ async function syncGitHubEmails(userId, connectionId) {
282
+ try {
283
+ const emails = await nangoService.getGithubUserEmails(connectionId);
284
+ if (emails.length === 0) {
285
+ console.log(`[nango-webhook] No emails returned from GitHub (scope may not be granted)`);
286
+ return null;
287
+ }
288
+ // Sync all verified emails to user_emails table
289
+ const verifiedEmails = emails.filter(e => e.verified);
290
+ if (verifiedEmails.length > 0) {
291
+ await db.userEmails.syncFromGitHub(userId, verifiedEmails);
292
+ console.log(`[nango-webhook] Synced ${verifiedEmails.length} verified emails for user ${userId}`);
293
+ }
294
+ // Return the primary verified email
295
+ const primaryEmail = emails.find(e => e.primary && e.verified)?.email;
296
+ return primaryEmail || null;
297
+ }
298
+ catch (error) {
299
+ console.error('[nango-webhook] Error syncing GitHub emails:', error);
300
+ return null;
301
+ }
302
+ }
274
303
  /**
275
304
  * Handle GitHub login webhook
276
305
  *
@@ -283,10 +312,38 @@ async function handleLoginWebhook(connectionId, _endUser) {
283
312
  // Get GitHub user info via Nango proxy
284
313
  const githubUser = await nangoService.getGithubUser(connectionId);
285
314
  const githubId = String(githubUser.id);
286
- // Check if user already exists
287
- const existingUser = await db.users.findByGithubId(githubId);
288
- // SCENARIO 1: New user
315
+ // Check if user already exists by GitHub ID
316
+ let existingUser = await db.users.findByGithubId(githubId);
317
+ // If not found by GitHub ID, check by email (for email-signup users connecting GitHub)
318
+ if (!existingUser && githubUser.email) {
319
+ const userByEmail = await db.users.findByEmail(githubUser.email);
320
+ if (userByEmail) {
321
+ // Email-signup user is connecting their GitHub account
322
+ console.log(`[nango-webhook] Linking GitHub to existing email user: ${githubUser.login} -> ${userByEmail.email}`);
323
+ // Update the existing user with GitHub info
324
+ await db.users.update(userByEmail.id, {
325
+ githubId,
326
+ githubUsername: githubUser.login,
327
+ avatarUrl: githubUser.avatar_url || null,
328
+ nangoConnectionId: connectionId,
329
+ incomingConnectionId: connectionId,
330
+ });
331
+ // Sync GitHub emails
332
+ await syncGitHubEmails(userByEmail.id, connectionId);
333
+ // Update connection with user ID
334
+ await nangoService.updateEndUser(connectionId, NANGO_INTEGRATIONS.GITHUB_USER, {
335
+ id: userByEmail.id,
336
+ email: userByEmail.email || undefined,
337
+ });
338
+ // Check for auto-add to workspaces
339
+ await checkAndAutoAddToWorkspaces(userByEmail.id, connectionId);
340
+ return;
341
+ }
342
+ }
343
+ // SCENARIO 1: New user (no existing user by GitHub ID or email)
289
344
  if (!existingUser) {
345
+ // First, get the primary email from GitHub API (requires user:email scope)
346
+ // We'll create the user first, then sync emails
290
347
  const newUser = await db.users.upsert({
291
348
  githubId,
292
349
  githubUsername: githubUser.login,
@@ -295,10 +352,16 @@ async function handleLoginWebhook(connectionId, _endUser) {
295
352
  nangoConnectionId: connectionId,
296
353
  incomingConnectionId: connectionId,
297
354
  });
355
+ // Sync all GitHub emails and get primary email
356
+ const primaryEmail = await syncGitHubEmails(newUser.id, connectionId);
357
+ // If we got a primary email from the API and user doesn't have one, update it
358
+ if (primaryEmail && !newUser.email) {
359
+ await db.users.update(newUser.id, { email: primaryEmail });
360
+ }
298
361
  // Update connection with real user ID
299
362
  await nangoService.updateEndUser(connectionId, NANGO_INTEGRATIONS.GITHUB_USER, {
300
363
  id: newUser.id,
301
- email: newUser.email || undefined,
364
+ email: primaryEmail || newUser.email || undefined,
302
365
  });
303
366
  console.log(`[nango-webhook] New user created: ${githubUser.login}`);
304
367
  // Check for auto-add to workspaces based on repo access
@@ -317,6 +380,8 @@ async function handleLoginWebhook(connectionId, _endUser) {
317
380
  githubUsername: githubUser.login,
318
381
  avatarUrl: githubUser.avatar_url || null,
319
382
  });
383
+ // Sync GitHub emails using the temporary connection before we delete it
384
+ await syncGitHubEmails(existingUser.id, connectionId);
320
385
  // Delete the temporary connection from Nango to prevent duplicates
321
386
  try {
322
387
  await nangoService.deleteConnection(connectionId, NANGO_INTEGRATIONS.GITHUB_USER);
@@ -338,10 +403,12 @@ async function handleLoginWebhook(connectionId, _endUser) {
338
403
  githubUsername: githubUser.login,
339
404
  avatarUrl: githubUser.avatar_url || null,
340
405
  });
406
+ // Sync GitHub emails
407
+ const primaryEmail = await syncGitHubEmails(existingUser.id, connectionId);
341
408
  // Update connection with user ID
342
409
  await nangoService.updateEndUser(connectionId, NANGO_INTEGRATIONS.GITHUB_USER, {
343
410
  id: existingUser.id,
344
- email: existingUser.email || undefined,
411
+ email: primaryEmail || existingUser.email || undefined,
345
412
  });
346
413
  // Check for auto-add to workspaces
347
414
  await checkAndAutoAddToWorkspaces(existingUser.id, connectionId);
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { Pool } from 'pg';
8
8
  import * as schema from './schema.js';
9
- export type { User, NewUser, GitHubInstallation, NewGitHubInstallation, Credential, NewCredential, Workspace, NewWorkspace, WorkspaceConfig, WorkspaceMember, NewWorkspaceMember, ProjectGroup, NewProjectGroup, CoordinatorAgentConfig, ProjectAgentConfig, Repository, NewRepository, LinkedDaemon, NewLinkedDaemon, Subscription, NewSubscription, UsageRecord, NewUsageRecord, AgentSession, NewAgentSession, AgentSummary, NewAgentSummary, } from './schema.js';
9
+ export type { User, NewUser, UserEmail, NewUserEmail, GitHubInstallation, NewGitHubInstallation, Credential, NewCredential, Workspace, NewWorkspace, WorkspaceConfig, WorkspaceMember, NewWorkspaceMember, ProjectGroup, NewProjectGroup, CoordinatorAgentConfig, ProjectAgentConfig, Repository, NewRepository, LinkedDaemon, NewLinkedDaemon, Subscription, NewSubscription, UsageRecord, NewUsageRecord, AgentSession, NewAgentSession, AgentSummary, NewAgentSummary, } from './schema.js';
10
10
  export * from './schema.js';
11
11
  /**
12
12
  * Get the raw connection pool for bulk operations.
@@ -30,8 +30,42 @@ export interface UserQueries {
30
30
  clearIncomingConnectionId(userId: string): Promise<void>;
31
31
  setPendingInstallationRequest(userId: string): Promise<void>;
32
32
  clearPendingInstallationRequest(userId: string): Promise<void>;
33
+ createEmailUser(data: {
34
+ email: string;
35
+ passwordHash: string;
36
+ displayName?: string;
37
+ }): Promise<schema.User>;
38
+ verifyEmail(userId: string): Promise<void>;
39
+ setEmailVerificationToken(userId: string, token: string, expires: Date): Promise<void>;
40
+ findByEmailVerificationToken(token: string): Promise<schema.User | null>;
41
+ updatePassword(userId: string, passwordHash: string): Promise<void>;
33
42
  }
34
43
  export declare const userQueries: UserQueries;
44
+ export interface UserEmailQueries {
45
+ /** Find all emails for a user */
46
+ findByUserId(userId: string): Promise<schema.UserEmail[]>;
47
+ /** Find a user by any of their linked emails (for login reconciliation) */
48
+ findUserByEmail(email: string): Promise<schema.User | null>;
49
+ /** Add or update an email for a user */
50
+ upsert(data: {
51
+ userId: string;
52
+ email: string;
53
+ verified: boolean;
54
+ primary: boolean;
55
+ source?: string;
56
+ }): Promise<schema.UserEmail>;
57
+ /** Sync all emails from GitHub for a user (replaces GitHub-sourced emails) */
58
+ syncFromGitHub(userId: string, emails: Array<{
59
+ email: string;
60
+ verified: boolean;
61
+ primary: boolean;
62
+ }>): Promise<void>;
63
+ /** Delete a specific email for a user */
64
+ delete(userId: string, email: string): Promise<void>;
65
+ /** Check if an email is already linked to another user */
66
+ isEmailLinkedToOtherUser(email: string, excludeUserId: string): Promise<boolean>;
67
+ }
68
+ export declare const userEmailQueries: UserEmailQueries;
35
69
  export interface GitHubInstallationQueries {
36
70
  findById(id: string): Promise<schema.GitHubInstallation | null>;
37
71
  findByInstallationId(installationId: string): Promise<schema.GitHubInstallation | null>;
@@ -144,6 +144,142 @@ export const userQueries = {
144
144
  .set({ pendingInstallationRequest: null, updatedAt: new Date() })
145
145
  .where(eq(schema.users.id, userId));
146
146
  },
147
+ async createEmailUser(data) {
148
+ const db = getDb();
149
+ const result = await db
150
+ .insert(schema.users)
151
+ .values({
152
+ email: data.email,
153
+ passwordHash: data.passwordHash,
154
+ displayName: data.displayName || null,
155
+ emailVerified: false,
156
+ })
157
+ .returning();
158
+ return result[0];
159
+ },
160
+ async verifyEmail(userId) {
161
+ const db = getDb();
162
+ await db
163
+ .update(schema.users)
164
+ .set({
165
+ emailVerified: true,
166
+ emailVerificationToken: null,
167
+ emailVerificationExpires: null,
168
+ updatedAt: new Date(),
169
+ })
170
+ .where(eq(schema.users.id, userId));
171
+ },
172
+ async setEmailVerificationToken(userId, token, expires) {
173
+ const db = getDb();
174
+ await db
175
+ .update(schema.users)
176
+ .set({
177
+ emailVerificationToken: token,
178
+ emailVerificationExpires: expires,
179
+ updatedAt: new Date(),
180
+ })
181
+ .where(eq(schema.users.id, userId));
182
+ },
183
+ async findByEmailVerificationToken(token) {
184
+ const db = getDb();
185
+ const result = await db
186
+ .select()
187
+ .from(schema.users)
188
+ .where(eq(schema.users.emailVerificationToken, token));
189
+ return result[0] ?? null;
190
+ },
191
+ async updatePassword(userId, passwordHash) {
192
+ const db = getDb();
193
+ await db
194
+ .update(schema.users)
195
+ .set({ passwordHash, updatedAt: new Date() })
196
+ .where(eq(schema.users.id, userId));
197
+ },
198
+ };
199
+ export const userEmailQueries = {
200
+ async findByUserId(userId) {
201
+ const db = getDb();
202
+ return db
203
+ .select()
204
+ .from(schema.userEmails)
205
+ .where(eq(schema.userEmails.userId, userId));
206
+ },
207
+ async findUserByEmail(email) {
208
+ const db = getDb();
209
+ // First check user_emails table for linked emails
210
+ const linkedEmail = await db
211
+ .select({ userId: schema.userEmails.userId })
212
+ .from(schema.userEmails)
213
+ .where(eq(schema.userEmails.email, email.toLowerCase()))
214
+ .limit(1);
215
+ if (linkedEmail[0]) {
216
+ const user = await db
217
+ .select()
218
+ .from(schema.users)
219
+ .where(eq(schema.users.id, linkedEmail[0].userId));
220
+ return user[0] ?? null;
221
+ }
222
+ // Fall back to checking users.email directly
223
+ const user = await db
224
+ .select()
225
+ .from(schema.users)
226
+ .where(eq(schema.users.email, email.toLowerCase()));
227
+ return user[0] ?? null;
228
+ },
229
+ async upsert(data) {
230
+ const db = getDb();
231
+ const result = await db
232
+ .insert(schema.userEmails)
233
+ .values({
234
+ userId: data.userId,
235
+ email: data.email.toLowerCase(),
236
+ verified: data.verified,
237
+ primary: data.primary,
238
+ source: data.source ?? 'github',
239
+ })
240
+ .onConflictDoUpdate({
241
+ target: [schema.userEmails.userId, schema.userEmails.email],
242
+ set: {
243
+ verified: data.verified,
244
+ primary: data.primary,
245
+ updatedAt: new Date(),
246
+ },
247
+ })
248
+ .returning();
249
+ return result[0];
250
+ },
251
+ async syncFromGitHub(userId, emails) {
252
+ const db = getDb();
253
+ // Delete existing GitHub-sourced emails for this user
254
+ await db
255
+ .delete(schema.userEmails)
256
+ .where(and(eq(schema.userEmails.userId, userId), eq(schema.userEmails.source, 'github')));
257
+ // Insert all new emails
258
+ if (emails.length > 0) {
259
+ await db.insert(schema.userEmails).values(emails.map(e => ({
260
+ userId,
261
+ email: e.email.toLowerCase(),
262
+ verified: e.verified,
263
+ primary: e.primary,
264
+ source: 'github',
265
+ })));
266
+ }
267
+ },
268
+ async delete(userId, email) {
269
+ const db = getDb();
270
+ await db
271
+ .delete(schema.userEmails)
272
+ .where(and(eq(schema.userEmails.userId, userId), eq(schema.userEmails.email, email.toLowerCase())));
273
+ },
274
+ async isEmailLinkedToOtherUser(email, excludeUserId) {
275
+ const db = getDb();
276
+ const result = await db
277
+ .select({ userId: schema.userEmails.userId })
278
+ .from(schema.userEmails)
279
+ .where(and(eq(schema.userEmails.email, email.toLowerCase()), sql `${schema.userEmails.userId} != ${excludeUserId}`))
280
+ .limit(1);
281
+ return result.length > 0;
282
+ },
147
283
  };
148
284
  export const githubInstallationQueries = {
149
285
  async findById(id) {
@@ -7,14 +7,15 @@
7
7
  * Generate migrations: npm run db:generate
8
8
  * Run migrations: npm run db:migrate
9
9
  */
10
- export type { User, NewUser, GitHubInstallation, NewGitHubInstallation, Credential, NewCredential, Workspace, NewWorkspace, WorkspaceConfig, WorkspaceAgentPolicy, AgentPolicyRule, WorkspaceMember, NewWorkspaceMember, ProjectGroup, NewProjectGroup, CoordinatorAgentConfig, ProjectAgentConfig, Repository, NewRepository, LinkedDaemon, NewLinkedDaemon, Subscription, NewSubscription, UsageRecord, NewUsageRecord, CIAnnotation, CIFailureEvent, NewCIFailureEvent, CIFixAttempt, NewCIFixAttempt, CICheckStrategy, CIWebhookConfig, IssueAssignment, NewIssueAssignment, CommentMention, NewCommentMention, AgentTriggerConfig, Channel, NewChannel, ChannelMember, NewChannelMember, } from './schema.js';
11
- export { users as usersTable, githubInstallations as githubInstallationsTable, credentials as credentialsTable, workspaces as workspacesTable, workspaceMembers as workspaceMembersTable, projectGroups as projectGroupsTable, repositories as repositoriesTable, linkedDaemons as linkedDaemonsTable, subscriptions as subscriptionsTable, usageRecords as usageRecordsTable, ciFailureEvents as ciFailureEventsTable, ciFixAttempts as ciFixAttemptsTable, issueAssignments as issueAssignmentsTable, commentMentions as commentMentionsTable, channels as channelsTable, channelMembers as channelMembersTable, } from './schema.js';
12
- import { getDb, closeDb, runMigrations, getRawPool, userQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, linkedDaemonQueries, projectGroupQueries, repositoryQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries } from './drizzle.js';
10
+ export type { User, NewUser, UserEmail, NewUserEmail, GitHubInstallation, NewGitHubInstallation, Credential, NewCredential, Workspace, NewWorkspace, WorkspaceConfig, WorkspaceAgentPolicy, AgentPolicyRule, WorkspaceMember, NewWorkspaceMember, ProjectGroup, NewProjectGroup, CoordinatorAgentConfig, ProjectAgentConfig, Repository, NewRepository, LinkedDaemon, NewLinkedDaemon, Subscription, NewSubscription, UsageRecord, NewUsageRecord, CIAnnotation, CIFailureEvent, NewCIFailureEvent, CIFixAttempt, NewCIFixAttempt, CICheckStrategy, CIWebhookConfig, IssueAssignment, NewIssueAssignment, CommentMention, NewCommentMention, AgentTriggerConfig, Channel, NewChannel, ChannelMember, NewChannelMember, } from './schema.js';
11
+ export { users as usersTable, userEmails as userEmailsTable, githubInstallations as githubInstallationsTable, credentials as credentialsTable, workspaces as workspacesTable, workspaceMembers as workspaceMembersTable, projectGroups as projectGroupsTable, repositories as repositoriesTable, linkedDaemons as linkedDaemonsTable, subscriptions as subscriptionsTable, usageRecords as usageRecordsTable, ciFailureEvents as ciFailureEventsTable, ciFixAttempts as ciFixAttemptsTable, issueAssignments as issueAssignmentsTable, commentMentions as commentMentionsTable, channels as channelsTable, channelMembers as channelMembersTable, } from './schema.js';
12
+ import { getDb, closeDb, runMigrations, getRawPool, userQueries, userEmailQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, linkedDaemonQueries, projectGroupQueries, repositoryQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries } from './drizzle.js';
13
13
  import { bulkInsertMessages, streamingBulkInsert, optimizedBulkInsert, getPoolStats, checkPoolHealth, type BulkInsertResult } from './bulk-ingest.js';
14
14
  export type PlanType = 'free' | 'pro' | 'team' | 'enterprise';
15
15
  export type WorkspaceMemberRole = 'owner' | 'admin' | 'member' | 'viewer';
16
16
  export declare const db: {
17
17
  users: import("./drizzle.js").UserQueries;
18
+ userEmails: import("./drizzle.js").UserEmailQueries;
18
19
  githubInstallations: import("./drizzle.js").GitHubInstallationQueries;
19
20
  credentials: import("./drizzle.js").CredentialQueries;
20
21
  workspaces: import("./drizzle.js").WorkspaceQueries;
@@ -48,7 +49,7 @@ export declare const db: {
48
49
  close: typeof closeDb;
49
50
  runMigrations: typeof runMigrations;
50
51
  };
51
- export { userQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, projectGroupQueries, repositoryQueries, linkedDaemonQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, };
52
+ export { userQueries, userEmailQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, projectGroupQueries, repositoryQueries, linkedDaemonQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, };
52
53
  export { getDb, closeDb, runMigrations, getRawPool };
53
54
  export { bulkInsertMessages, streamingBulkInsert, optimizedBulkInsert, getPoolStats, checkPoolHealth, type BulkInsertResult, };
54
55
  export declare function initializeDatabase(): Promise<void>;
@@ -8,15 +8,17 @@
8
8
  * Run migrations: npm run db:migrate
9
9
  */
10
10
  // Re-export schema tables for direct access if needed
11
- export { users as usersTable, githubInstallations as githubInstallationsTable, credentials as credentialsTable, workspaces as workspacesTable, workspaceMembers as workspaceMembersTable, projectGroups as projectGroupsTable, repositories as repositoriesTable, linkedDaemons as linkedDaemonsTable, subscriptions as subscriptionsTable, usageRecords as usageRecordsTable, ciFailureEvents as ciFailureEventsTable, ciFixAttempts as ciFixAttemptsTable, issueAssignments as issueAssignmentsTable, commentMentions as commentMentionsTable, channels as channelsTable, channelMembers as channelMembersTable, } from './schema.js';
11
+ export { users as usersTable, userEmails as userEmailsTable, githubInstallations as githubInstallationsTable, credentials as credentialsTable, workspaces as workspacesTable, workspaceMembers as workspaceMembersTable, projectGroups as projectGroupsTable, repositories as repositoriesTable, linkedDaemons as linkedDaemonsTable, subscriptions as subscriptionsTable, usageRecords as usageRecordsTable, ciFailureEvents as ciFailureEventsTable, ciFixAttempts as ciFixAttemptsTable, issueAssignments as issueAssignmentsTable, commentMentions as commentMentionsTable, channels as channelsTable, channelMembers as channelMembersTable, } from './schema.js';
12
12
  // Import query modules
13
- import { getDb, closeDb, runMigrations, getRawPool, userQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, linkedDaemonQueries, projectGroupQueries, repositoryQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, channelQueries, channelMemberQueries, } from './drizzle.js';
13
+ import { getDb, closeDb, runMigrations, getRawPool, userQueries, userEmailQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, linkedDaemonQueries, projectGroupQueries, repositoryQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, channelQueries, channelMemberQueries, } from './drizzle.js';
14
14
  // Bulk ingest utilities for high-volume message sync to cloud
15
15
  import { bulkInsertMessages, streamingBulkInsert, optimizedBulkInsert, getPoolStats, checkPoolHealth, } from './bulk-ingest.js';
16
16
  // Export the db object with all query namespaces
17
17
  export const db = {
18
18
  // User operations
19
19
  users: userQueries,
20
+ // User email operations (for GitHub-linked emails and account reconciliation)
21
+ userEmails: userEmailQueries,
20
22
  // GitHub App installation operations
21
23
  githubInstallations: githubInstallationQueries,
22
24
  // Credential operations
@@ -55,7 +57,7 @@ export const db = {
55
57
  runMigrations,
56
58
  };
57
59
  // Export query objects for direct import
58
- export { userQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, projectGroupQueries, repositoryQueries, linkedDaemonQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, };
60
+ export { userQueries, userEmailQueries, githubInstallationQueries, credentialQueries, workspaceQueries, workspaceMemberQueries, projectGroupQueries, repositoryQueries, linkedDaemonQueries, ciFailureEventQueries, ciFixAttemptQueries, issueAssignmentQueries, commentMentionQueries, };
59
61
  // Export database utilities
60
62
  export { getDb, closeDb, runMigrations, getRawPool };
61
63
  // Bulk ingest utilities for direct import
@@ -33,7 +33,7 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
33
33
  columnType: "PgVarchar";
34
34
  data: string;
35
35
  driverParam: string;
36
- notNull: true;
36
+ notNull: false;
37
37
  hasDefault: false;
38
38
  isPrimaryKey: false;
39
39
  isAutoincrement: false;
@@ -52,7 +52,7 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
52
52
  columnType: "PgVarchar";
53
53
  data: string;
54
54
  driverParam: string;
55
- notNull: true;
55
+ notNull: false;
56
56
  hasDefault: false;
57
57
  isPrimaryKey: false;
58
58
  isAutoincrement: false;
@@ -83,6 +83,97 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
83
83
  }, {}, {
84
84
  length: 255;
85
85
  }>;
86
+ passwordHash: import("drizzle-orm/pg-core").PgColumn<{
87
+ name: "password_hash";
88
+ tableName: "users";
89
+ dataType: "string";
90
+ columnType: "PgVarchar";
91
+ data: string;
92
+ driverParam: string;
93
+ notNull: false;
94
+ hasDefault: false;
95
+ isPrimaryKey: false;
96
+ isAutoincrement: false;
97
+ hasRuntimeDefault: false;
98
+ enumValues: [string, ...string[]];
99
+ baseColumn: never;
100
+ identity: undefined;
101
+ generated: undefined;
102
+ }, {}, {
103
+ length: 255;
104
+ }>;
105
+ emailVerified: import("drizzle-orm/pg-core").PgColumn<{
106
+ name: "email_verified";
107
+ tableName: "users";
108
+ dataType: "boolean";
109
+ columnType: "PgBoolean";
110
+ data: boolean;
111
+ driverParam: boolean;
112
+ notNull: true;
113
+ hasDefault: true;
114
+ isPrimaryKey: false;
115
+ isAutoincrement: false;
116
+ hasRuntimeDefault: false;
117
+ enumValues: undefined;
118
+ baseColumn: never;
119
+ identity: undefined;
120
+ generated: undefined;
121
+ }, {}, {}>;
122
+ emailVerificationToken: import("drizzle-orm/pg-core").PgColumn<{
123
+ name: "email_verification_token";
124
+ tableName: "users";
125
+ dataType: "string";
126
+ columnType: "PgVarchar";
127
+ data: string;
128
+ driverParam: string;
129
+ notNull: false;
130
+ hasDefault: false;
131
+ isPrimaryKey: false;
132
+ isAutoincrement: false;
133
+ hasRuntimeDefault: false;
134
+ enumValues: [string, ...string[]];
135
+ baseColumn: never;
136
+ identity: undefined;
137
+ generated: undefined;
138
+ }, {}, {
139
+ length: 255;
140
+ }>;
141
+ emailVerificationExpires: import("drizzle-orm/pg-core").PgColumn<{
142
+ name: "email_verification_expires";
143
+ tableName: "users";
144
+ dataType: "date";
145
+ columnType: "PgTimestamp";
146
+ data: Date;
147
+ driverParam: string;
148
+ notNull: false;
149
+ hasDefault: false;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: undefined;
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {}>;
158
+ displayName: import("drizzle-orm/pg-core").PgColumn<{
159
+ name: "display_name";
160
+ tableName: "users";
161
+ dataType: "string";
162
+ columnType: "PgVarchar";
163
+ data: string;
164
+ driverParam: string;
165
+ notNull: false;
166
+ hasDefault: false;
167
+ isPrimaryKey: false;
168
+ isAutoincrement: false;
169
+ hasRuntimeDefault: false;
170
+ enumValues: [string, ...string[]];
171
+ baseColumn: never;
172
+ identity: undefined;
173
+ generated: undefined;
174
+ }, {}, {
175
+ length: 255;
176
+ }>;
86
177
  avatarUrl: import("drizzle-orm/pg-core").PgColumn<{
87
178
  name: "avatar_url";
88
179
  tableName: "users";
@@ -256,6 +347,157 @@ export declare const usersRelations: import("drizzle-orm").Relations<"users", {
256
347
  repositories: import("drizzle-orm").Many<"repositories">;
257
348
  linkedDaemons: import("drizzle-orm").Many<"linked_daemons">;
258
349
  installedGitHubApps: import("drizzle-orm").Many<"github_installations">;
350
+ emails: import("drizzle-orm").Many<"user_emails">;
351
+ }>;
352
+ export declare const userEmails: import("drizzle-orm/pg-core").PgTableWithColumns<{
353
+ name: "user_emails";
354
+ schema: undefined;
355
+ columns: {
356
+ id: import("drizzle-orm/pg-core").PgColumn<{
357
+ name: "id";
358
+ tableName: "user_emails";
359
+ dataType: "string";
360
+ columnType: "PgUUID";
361
+ data: string;
362
+ driverParam: string;
363
+ notNull: true;
364
+ hasDefault: true;
365
+ isPrimaryKey: true;
366
+ isAutoincrement: false;
367
+ hasRuntimeDefault: false;
368
+ enumValues: undefined;
369
+ baseColumn: never;
370
+ identity: undefined;
371
+ generated: undefined;
372
+ }, {}, {}>;
373
+ userId: import("drizzle-orm/pg-core").PgColumn<{
374
+ name: "user_id";
375
+ tableName: "user_emails";
376
+ dataType: "string";
377
+ columnType: "PgUUID";
378
+ data: string;
379
+ driverParam: string;
380
+ notNull: true;
381
+ hasDefault: false;
382
+ isPrimaryKey: false;
383
+ isAutoincrement: false;
384
+ hasRuntimeDefault: false;
385
+ enumValues: undefined;
386
+ baseColumn: never;
387
+ identity: undefined;
388
+ generated: undefined;
389
+ }, {}, {}>;
390
+ email: import("drizzle-orm/pg-core").PgColumn<{
391
+ name: "email";
392
+ tableName: "user_emails";
393
+ dataType: "string";
394
+ columnType: "PgVarchar";
395
+ data: string;
396
+ driverParam: string;
397
+ notNull: true;
398
+ hasDefault: false;
399
+ isPrimaryKey: false;
400
+ isAutoincrement: false;
401
+ hasRuntimeDefault: false;
402
+ enumValues: [string, ...string[]];
403
+ baseColumn: never;
404
+ identity: undefined;
405
+ generated: undefined;
406
+ }, {}, {
407
+ length: 255;
408
+ }>;
409
+ verified: import("drizzle-orm/pg-core").PgColumn<{
410
+ name: "verified";
411
+ tableName: "user_emails";
412
+ dataType: "boolean";
413
+ columnType: "PgBoolean";
414
+ data: boolean;
415
+ driverParam: boolean;
416
+ notNull: true;
417
+ hasDefault: true;
418
+ isPrimaryKey: false;
419
+ isAutoincrement: false;
420
+ hasRuntimeDefault: false;
421
+ enumValues: undefined;
422
+ baseColumn: never;
423
+ identity: undefined;
424
+ generated: undefined;
425
+ }, {}, {}>;
426
+ primary: import("drizzle-orm/pg-core").PgColumn<{
427
+ name: "primary";
428
+ tableName: "user_emails";
429
+ dataType: "boolean";
430
+ columnType: "PgBoolean";
431
+ data: boolean;
432
+ driverParam: boolean;
433
+ notNull: true;
434
+ hasDefault: true;
435
+ isPrimaryKey: false;
436
+ isAutoincrement: false;
437
+ hasRuntimeDefault: false;
438
+ enumValues: undefined;
439
+ baseColumn: never;
440
+ identity: undefined;
441
+ generated: undefined;
442
+ }, {}, {}>;
443
+ source: import("drizzle-orm/pg-core").PgColumn<{
444
+ name: "source";
445
+ tableName: "user_emails";
446
+ dataType: "string";
447
+ columnType: "PgVarchar";
448
+ data: string;
449
+ driverParam: string;
450
+ notNull: true;
451
+ hasDefault: true;
452
+ isPrimaryKey: false;
453
+ isAutoincrement: false;
454
+ hasRuntimeDefault: false;
455
+ enumValues: [string, ...string[]];
456
+ baseColumn: never;
457
+ identity: undefined;
458
+ generated: undefined;
459
+ }, {}, {
460
+ length: 50;
461
+ }>;
462
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
463
+ name: "created_at";
464
+ tableName: "user_emails";
465
+ dataType: "date";
466
+ columnType: "PgTimestamp";
467
+ data: Date;
468
+ driverParam: string;
469
+ notNull: true;
470
+ hasDefault: true;
471
+ isPrimaryKey: false;
472
+ isAutoincrement: false;
473
+ hasRuntimeDefault: false;
474
+ enumValues: undefined;
475
+ baseColumn: never;
476
+ identity: undefined;
477
+ generated: undefined;
478
+ }, {}, {}>;
479
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
480
+ name: "updated_at";
481
+ tableName: "user_emails";
482
+ dataType: "date";
483
+ columnType: "PgTimestamp";
484
+ data: Date;
485
+ driverParam: string;
486
+ notNull: true;
487
+ hasDefault: true;
488
+ isPrimaryKey: false;
489
+ isAutoincrement: false;
490
+ hasRuntimeDefault: false;
491
+ enumValues: undefined;
492
+ baseColumn: never;
493
+ identity: undefined;
494
+ generated: undefined;
495
+ }, {}, {}>;
496
+ };
497
+ dialect: "pg";
498
+ }>;
499
+ export declare const userEmailsRelations: import("drizzle-orm").Relations<"user_emails", {
500
+ user: import("drizzle-orm").One<"users", true>;
259
501
  }>;
260
502
  export declare const githubInstallations: import("drizzle-orm/pg-core").PgTableWithColumns<{
261
503
  name: "github_installations";
@@ -2785,6 +3027,8 @@ export declare const agentSummaries: import("drizzle-orm/pg-core").PgTableWithCo
2785
3027
  }>;
2786
3028
  export type User = typeof users.$inferSelect;
2787
3029
  export type NewUser = typeof users.$inferInsert;
3030
+ export type UserEmail = typeof userEmails.$inferSelect;
3031
+ export type NewUserEmail = typeof userEmails.$inferInsert;
2788
3032
  export type GitHubInstallation = typeof githubInstallations.$inferSelect;
2789
3033
  export type NewGitHubInstallation = typeof githubInstallations.$inferInsert;
2790
3034
  export type Credential = typeof credentials.$inferSelect;