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.
- package/deploy/workspace/entrypoint.sh +35 -19
- package/deploy/workspace/git-credential-relay +82 -7
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
- 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
- package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
- package/dist/dashboard/out/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -1
- package/dist/dashboard/out/cloud/link.txt +1 -1
- package/dist/dashboard/out/complete-profile.html +5 -0
- package/dist/dashboard/out/complete-profile.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +1 -1
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +1 -1
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers/setup/cursor.html +1 -1
- package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +2 -2
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +2 -2
- package/dist/src/cli/index.js +3 -1
- package/package.json +22 -21
- package/packages/api-types/package.json +1 -1
- package/packages/bridge/package.json +8 -8
- package/packages/cloud/dist/api/auth.js +2 -0
- package/packages/cloud/dist/api/billing.js +4 -4
- package/packages/cloud/dist/api/email-auth.d.ts +11 -0
- package/packages/cloud/dist/api/email-auth.js +347 -0
- package/packages/cloud/dist/api/nango-auth.js +72 -5
- package/packages/cloud/dist/db/drizzle.d.ts +35 -1
- package/packages/cloud/dist/db/drizzle.js +136 -0
- package/packages/cloud/dist/db/index.d.ts +5 -4
- package/packages/cloud/dist/db/index.js +5 -3
- package/packages/cloud/dist/db/schema.d.ts +246 -2
- package/packages/cloud/dist/db/schema.js +39 -3
- package/packages/cloud/dist/provisioner/index.js +5 -1
- package/packages/cloud/dist/server.js +134 -24
- package/packages/cloud/dist/services/nango.d.ts +18 -0
- package/packages/cloud/dist/services/nango.js +32 -0
- package/packages/cloud/package.json +6 -6
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +1 -1
- package/packages/daemon/package.json +12 -12
- package/packages/dashboard/dist/server.js +36 -7
- package/packages/dashboard/package.json +13 -13
- package/packages/dashboard/ui/app/complete-profile/page.tsx +204 -0
- package/packages/dashboard/ui/app/login/page.tsx +182 -38
- package/packages/dashboard/ui/app/signup/page.tsx +244 -54
- package/packages/dashboard/ui/lib/cloudApi.ts +1 -0
- package/packages/dashboard/ui/react-components/App.tsx +1 -1
- package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +10 -0
- package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +160 -3
- package/packages/dashboard/ui-dist/404.html +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
- 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
- package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
- package/packages/dashboard/ui-dist/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
- package/packages/dashboard/ui-dist/app.html +1 -1
- package/packages/dashboard/ui-dist/app.txt +2 -2
- package/packages/dashboard/ui-dist/cloud/link.html +1 -1
- package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
- package/packages/dashboard/ui-dist/complete-profile.html +5 -0
- package/packages/dashboard/ui-dist/complete-profile.txt +7 -0
- package/packages/dashboard/ui-dist/connect-repos.html +1 -1
- package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
- package/packages/dashboard/ui-dist/history.html +1 -1
- package/packages/dashboard/ui-dist/history.txt +1 -1
- package/packages/dashboard/ui-dist/index.html +1 -1
- package/packages/dashboard/ui-dist/index.txt +2 -2
- package/packages/dashboard/ui-dist/login.html +2 -2
- package/packages/dashboard/ui-dist/login.txt +2 -2
- package/packages/dashboard/ui-dist/metrics.html +1 -1
- package/packages/dashboard/ui-dist/metrics.txt +1 -1
- package/packages/dashboard/ui-dist/pricing.html +2 -2
- package/packages/dashboard/ui-dist/pricing.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
- package/packages/dashboard/ui-dist/providers.html +1 -1
- package/packages/dashboard/ui-dist/providers.txt +2 -2
- package/packages/dashboard/ui-dist/signup.html +2 -2
- package/packages/dashboard/ui-dist/signup.txt +2 -2
- package/packages/dashboard-server/dist/server.js +36 -7
- package/packages/dashboard-server/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +1 -1
- package/packages/wrapper/dist/relay-pty-orchestrator.js +17 -3
- package/packages/wrapper/package.json +6 -6
- package/relay-snippets/agent-policy-snippet.md +40 -0
- package/relay-snippets/agent-relay-protocol.md +101 -0
- package/relay-snippets/agent-relay-snippet.md +177 -0
- package/SESSION_HANDOFF.md +0 -67
- package/dist/dashboard/out/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
- package/test-push.txt +0 -1
- /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_ssgManifest.js +0 -0
|
@@ -12,9 +12,17 @@ import { relations } from 'drizzle-orm';
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
export const users = pgTable('users', {
|
|
14
14
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// GitHub OAuth fields (nullable for email-only users)
|
|
16
|
+
githubId: varchar('github_id', { length: 255 }).unique(),
|
|
17
|
+
githubUsername: varchar('github_username', { length: 255 }),
|
|
18
|
+
// Email authentication fields
|
|
19
|
+
email: varchar('email', { length: 255 }).unique(),
|
|
20
|
+
passwordHash: varchar('password_hash', { length: 255 }), // For email login
|
|
21
|
+
emailVerified: boolean('email_verified').notNull().default(false),
|
|
22
|
+
emailVerificationToken: varchar('email_verification_token', { length: 255 }),
|
|
23
|
+
emailVerificationExpires: timestamp('email_verification_expires'),
|
|
24
|
+
// Profile
|
|
25
|
+
displayName: varchar('display_name', { length: 255 }), // User-provided name for email users
|
|
18
26
|
avatarUrl: varchar('avatar_url', { length: 512 }),
|
|
19
27
|
plan: varchar('plan', { length: 50 }).notNull().default('free'),
|
|
20
28
|
// Stripe billing
|
|
@@ -29,6 +37,7 @@ export const users = pgTable('users', {
|
|
|
29
37
|
}, (table) => ({
|
|
30
38
|
nangoConnectionIdx: index('idx_users_nango_connection').on(table.nangoConnectionId),
|
|
31
39
|
incomingConnectionIdx: index('idx_users_incoming_connection').on(table.incomingConnectionId),
|
|
40
|
+
emailIdx: index('idx_users_email').on(table.email),
|
|
32
41
|
}));
|
|
33
42
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
34
43
|
credentials: many(credentials),
|
|
@@ -37,6 +46,33 @@ export const usersRelations = relations(users, ({ many }) => ({
|
|
|
37
46
|
repositories: many(repositories),
|
|
38
47
|
linkedDaemons: many(linkedDaemons),
|
|
39
48
|
installedGitHubApps: many(githubInstallations),
|
|
49
|
+
emails: many(userEmails),
|
|
50
|
+
}));
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// User Emails (GitHub-linked email addresses for account reconciliation)
|
|
53
|
+
// ============================================================================
|
|
54
|
+
export const userEmails = pgTable('user_emails', {
|
|
55
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
56
|
+
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
57
|
+
email: varchar('email', { length: 255 }).notNull(),
|
|
58
|
+
/** Whether this email is verified on GitHub */
|
|
59
|
+
verified: boolean('verified').notNull().default(false),
|
|
60
|
+
/** Whether this is the primary email on GitHub */
|
|
61
|
+
primary: boolean('primary').notNull().default(false),
|
|
62
|
+
/** Source of this email: 'github', 'manual', etc. */
|
|
63
|
+
source: varchar('source', { length: 50 }).notNull().default('github'),
|
|
64
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
65
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
66
|
+
}, (table) => ({
|
|
67
|
+
userEmailIdx: unique('user_emails_user_email_unique').on(table.userId, table.email),
|
|
68
|
+
emailIdx: index('idx_user_emails_email').on(table.email),
|
|
69
|
+
userIdIdx: index('idx_user_emails_user_id').on(table.userId),
|
|
70
|
+
}));
|
|
71
|
+
export const userEmailsRelations = relations(userEmails, ({ one }) => ({
|
|
72
|
+
user: one(users, {
|
|
73
|
+
fields: [userEmails.userId],
|
|
74
|
+
references: [users.id],
|
|
75
|
+
}),
|
|
40
76
|
}));
|
|
41
77
|
// ============================================================================
|
|
42
78
|
// GitHub App Installations
|
|
@@ -1438,10 +1438,14 @@ class DockerProvisioner {
|
|
|
1438
1438
|
const publicUrl = `http://localhost:${hostPort}`;
|
|
1439
1439
|
// Wait for container to be healthy before returning
|
|
1440
1440
|
// When running in Docker, use the internal container name for health check
|
|
1441
|
+
// Use 120s timeout to allow for:
|
|
1442
|
+
// - GitHub token fetch with retries (up to 36s worst case)
|
|
1443
|
+
// - Repository cloning (can take minutes for large repos)
|
|
1444
|
+
// - Daemon startup
|
|
1441
1445
|
const healthCheckUrl = runningInDocker
|
|
1442
1446
|
? `http://${containerName}:${WORKSPACE_PORT}`
|
|
1443
1447
|
: publicUrl;
|
|
1444
|
-
await this.waitForHealthy(healthCheckUrl);
|
|
1448
|
+
await this.waitForHealthy(healthCheckUrl, 120_000);
|
|
1445
1449
|
return {
|
|
1446
1450
|
computeId: containerName,
|
|
1447
1451
|
publicUrl,
|
|
@@ -34,6 +34,7 @@ import { testHelpersRouter } from './api/test-helpers.js';
|
|
|
34
34
|
import { webhooksRouter } from './api/webhooks.js';
|
|
35
35
|
import { githubAppRouter } from './api/github-app.js';
|
|
36
36
|
import { nangoAuthRouter } from './api/nango-auth.js';
|
|
37
|
+
import { emailAuthRouter } from './api/email-auth.js';
|
|
37
38
|
import { gitRouter } from './api/git.js';
|
|
38
39
|
import { sessionsRouter } from './api/sessions.js';
|
|
39
40
|
import { codexAuthHelperRouter } from './api/codex-auth-helper.js';
|
|
@@ -288,6 +289,7 @@ export async function createServer() {
|
|
|
288
289
|
//
|
|
289
290
|
// --- Routes with alternative auth (must be before teamsRouter) ---
|
|
290
291
|
app.use('/api/auth', authRouter); // Login endpoints (public)
|
|
292
|
+
app.use('/api/auth/email', emailAuthRouter); // Email/password authentication
|
|
291
293
|
app.use('/api/auth/nango', nangoAuthRouter); // Nango webhook (signature verification)
|
|
292
294
|
app.use('/api/auth/codex-helper', codexAuthHelperRouter);
|
|
293
295
|
app.use('/api/git', gitRouter); // Workspace token auth
|
|
@@ -1203,23 +1205,56 @@ export async function createServer() {
|
|
|
1203
1205
|
maxPayload: 1024 * 1024, // 1MB - presence messages are small
|
|
1204
1206
|
});
|
|
1205
1207
|
const onlineUsers = new Map();
|
|
1208
|
+
// Track workspace per WebSocket connection for filtering
|
|
1209
|
+
const connectionWorkspace = new Map();
|
|
1206
1210
|
// Validation helpers
|
|
1207
1211
|
const isValidUsername = (username) => {
|
|
1208
1212
|
if (typeof username !== 'string')
|
|
1209
1213
|
return false;
|
|
1210
|
-
|
|
1214
|
+
// Username can be:
|
|
1215
|
+
// - GitHub-style: alphanumeric with hyphens (e.g., "khaliqgant")
|
|
1216
|
+
// - Display name style: allows spaces for email users (e.g., "Khaliq Gant")
|
|
1217
|
+
// Max 50 chars, must start/end with alphanumeric, no consecutive spaces
|
|
1218
|
+
if (username.length === 0 || username.length > 50)
|
|
1219
|
+
return false;
|
|
1220
|
+
// Must start and end with alphanumeric
|
|
1221
|
+
if (!/^[a-zA-Z0-9]/.test(username) || !/[a-zA-Z0-9]$/.test(username))
|
|
1222
|
+
return false;
|
|
1223
|
+
// Only allow alphanumeric, spaces, hyphens, underscores, and periods
|
|
1224
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9 _.-]*[a-zA-Z0-9]$/.test(username) && username.length > 1)
|
|
1225
|
+
return false;
|
|
1226
|
+
// Single character usernames must be alphanumeric
|
|
1227
|
+
if (username.length === 1 && !/^[a-zA-Z0-9]$/.test(username))
|
|
1228
|
+
return false;
|
|
1229
|
+
// No consecutive spaces
|
|
1230
|
+
if (/ /.test(username))
|
|
1231
|
+
return false;
|
|
1232
|
+
return true;
|
|
1211
1233
|
};
|
|
1212
1234
|
const isValidAvatarUrl = (url) => {
|
|
1213
1235
|
if (url === undefined || url === null)
|
|
1214
1236
|
return true;
|
|
1215
1237
|
if (typeof url !== 'string')
|
|
1216
1238
|
return false;
|
|
1239
|
+
// Must be a valid HTTPS URL from known avatar providers
|
|
1217
1240
|
try {
|
|
1218
1241
|
const parsed = new URL(url);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1242
|
+
if (parsed.protocol !== 'https:')
|
|
1243
|
+
return false;
|
|
1244
|
+
// Allow GitHub avatars
|
|
1245
|
+
if (parsed.hostname === 'avatars.githubusercontent.com' ||
|
|
1246
|
+
parsed.hostname === 'github.com' ||
|
|
1247
|
+
parsed.hostname.endsWith('.githubusercontent.com'))
|
|
1248
|
+
return true;
|
|
1249
|
+
// Allow Gravatar for email-based avatars
|
|
1250
|
+
if (parsed.hostname === 'www.gravatar.com' ||
|
|
1251
|
+
parsed.hostname === 'gravatar.com' ||
|
|
1252
|
+
parsed.hostname === 'secure.gravatar.com')
|
|
1253
|
+
return true;
|
|
1254
|
+
// Allow UI Avatars (placeholder service)
|
|
1255
|
+
if (parsed.hostname === 'ui-avatars.com')
|
|
1256
|
+
return true;
|
|
1257
|
+
return false;
|
|
1223
1258
|
}
|
|
1224
1259
|
catch {
|
|
1225
1260
|
return false;
|
|
@@ -1474,9 +1509,16 @@ export async function createServer() {
|
|
|
1474
1509
|
}
|
|
1475
1510
|
});
|
|
1476
1511
|
};
|
|
1477
|
-
// Get online users list
|
|
1478
|
-
const getOnlineUsersList = () => {
|
|
1479
|
-
|
|
1512
|
+
// Get online users list, optionally filtered by workspace
|
|
1513
|
+
const getOnlineUsersList = (workspaceId) => {
|
|
1514
|
+
if (!workspaceId) {
|
|
1515
|
+
// No workspace filter - return all (for backwards compat)
|
|
1516
|
+
return Array.from(onlineUsers.values()).map((state) => state.info);
|
|
1517
|
+
}
|
|
1518
|
+
// Filter to only users in this workspace
|
|
1519
|
+
return Array.from(onlineUsers.values())
|
|
1520
|
+
.filter((state) => state.workspaceIds.has(workspaceId))
|
|
1521
|
+
.map((state) => state.info);
|
|
1480
1522
|
};
|
|
1481
1523
|
// Heartbeat interval to detect dead connections (30 seconds)
|
|
1482
1524
|
const PRESENCE_HEARTBEAT_INTERVAL = 30000;
|
|
@@ -1676,32 +1718,45 @@ export async function createServer() {
|
|
|
1676
1718
|
onlineUsers.set(username, {
|
|
1677
1719
|
info: { username, avatarUrl, connectedAt: now, lastSeen: now },
|
|
1678
1720
|
connections: new Set([ws]),
|
|
1721
|
+
workspaceIds: new Set(), // Workspace set when subscribe_channels is called
|
|
1679
1722
|
});
|
|
1680
1723
|
// Register with shared presence registry for cross-module access
|
|
1681
1724
|
registerUserPresence({ username, avatarUrl, connectedAt: now, lastSeen: now });
|
|
1682
1725
|
console.log(`[cloud] User ${username} came online`);
|
|
1683
|
-
|
|
1684
|
-
type: 'presence_join',
|
|
1685
|
-
user: { username, avatarUrl, connectedAt: now, lastSeen: now },
|
|
1686
|
-
}, ws);
|
|
1726
|
+
// Don't broadcast globally - wait until we know which workspace they're in
|
|
1687
1727
|
}
|
|
1728
|
+
// Send empty presence list initially - real list sent when workspace is known
|
|
1688
1729
|
ws.send(JSON.stringify({
|
|
1689
1730
|
type: 'presence_list',
|
|
1690
|
-
users:
|
|
1731
|
+
users: [],
|
|
1691
1732
|
}));
|
|
1692
1733
|
}
|
|
1693
1734
|
else if (msg.action === 'leave') {
|
|
1694
1735
|
if (!clientUsername || msg.username !== clientUsername)
|
|
1695
1736
|
return;
|
|
1696
1737
|
const userState = onlineUsers.get(clientUsername);
|
|
1738
|
+
const leavingWorkspace = connectionWorkspace.get(ws);
|
|
1739
|
+
connectionWorkspace.delete(ws);
|
|
1697
1740
|
if (userState) {
|
|
1698
1741
|
userState.connections.delete(ws);
|
|
1742
|
+
// Remove workspace from user's set if no more connections to it
|
|
1743
|
+
if (leavingWorkspace) {
|
|
1744
|
+
const stillHasWorkspaceConnection = Array.from(userState.connections).some((conn) => connectionWorkspace.get(conn) === leavingWorkspace);
|
|
1745
|
+
if (!stillHasWorkspaceConnection) {
|
|
1746
|
+
userState.workspaceIds.delete(leavingWorkspace);
|
|
1747
|
+
// Broadcast leave to users in that workspace
|
|
1748
|
+
wssPresence.clients.forEach((client) => {
|
|
1749
|
+
if (client.readyState === WebSocket.OPEN && connectionWorkspace.get(client) === leavingWorkspace) {
|
|
1750
|
+
client.send(JSON.stringify({ type: 'presence_leave', username: clientUsername }));
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1699
1755
|
if (userState.connections.size === 0) {
|
|
1700
1756
|
onlineUsers.delete(clientUsername);
|
|
1701
1757
|
// Unregister from shared presence registry
|
|
1702
1758
|
unregisterUserPresence(clientUsername);
|
|
1703
1759
|
console.log(`[cloud] User ${clientUsername} went offline`);
|
|
1704
|
-
broadcastPresence({ type: 'presence_leave', username: clientUsername });
|
|
1705
1760
|
}
|
|
1706
1761
|
}
|
|
1707
1762
|
}
|
|
@@ -1715,12 +1770,20 @@ export async function createServer() {
|
|
|
1715
1770
|
// Update last seen in shared presence registry
|
|
1716
1771
|
updateUserLastSeen(clientUsername);
|
|
1717
1772
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1773
|
+
// Only broadcast typing to users in the same workspace
|
|
1774
|
+
const typingWorkspace = connectionWorkspace.get(ws);
|
|
1775
|
+
if (typingWorkspace) {
|
|
1776
|
+
wssPresence.clients.forEach((client) => {
|
|
1777
|
+
if (client !== ws && client.readyState === WebSocket.OPEN && connectionWorkspace.get(client) === typingWorkspace) {
|
|
1778
|
+
client.send(JSON.stringify({
|
|
1779
|
+
type: 'typing',
|
|
1780
|
+
username: clientUsername,
|
|
1781
|
+
avatarUrl: userState?.info.avatarUrl,
|
|
1782
|
+
isTyping: msg.isTyping,
|
|
1783
|
+
}));
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1724
1787
|
}
|
|
1725
1788
|
else if (msg.type === 'subscribe_channels') {
|
|
1726
1789
|
// Subscribe to channel messages for a specific workspace
|
|
@@ -1732,8 +1795,38 @@ export async function createServer() {
|
|
|
1732
1795
|
console.warn(`[cloud] subscribe_channels missing workspaceId`);
|
|
1733
1796
|
return;
|
|
1734
1797
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1798
|
+
const workspaceId = msg.workspaceId;
|
|
1799
|
+
console.log(`[cloud] User ${clientUsername} subscribing to channels in workspace ${workspaceId}`);
|
|
1800
|
+
// Track which workspace this connection is in
|
|
1801
|
+
connectionWorkspace.set(ws, workspaceId);
|
|
1802
|
+
// Add workspace to user's workspace set
|
|
1803
|
+
const userState = onlineUsers.get(clientUsername);
|
|
1804
|
+
if (userState) {
|
|
1805
|
+
const isNewToWorkspace = !userState.workspaceIds.has(workspaceId);
|
|
1806
|
+
userState.workspaceIds.add(workspaceId);
|
|
1807
|
+
// If user is new to this workspace, broadcast presence_join to others in workspace
|
|
1808
|
+
if (isNewToWorkspace) {
|
|
1809
|
+
const { info } = userState;
|
|
1810
|
+
// Broadcast to all users in this workspace
|
|
1811
|
+
wssPresence.clients.forEach((client) => {
|
|
1812
|
+
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
|
1813
|
+
const clientWsId = connectionWorkspace.get(client);
|
|
1814
|
+
if (clientWsId === workspaceId) {
|
|
1815
|
+
client.send(JSON.stringify({
|
|
1816
|
+
type: 'presence_join',
|
|
1817
|
+
user: info,
|
|
1818
|
+
}));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
// Send updated presence list filtered by this workspace
|
|
1824
|
+
ws.send(JSON.stringify({
|
|
1825
|
+
type: 'presence_list',
|
|
1826
|
+
users: getOnlineUsersList(workspaceId),
|
|
1827
|
+
}));
|
|
1828
|
+
}
|
|
1829
|
+
setupDaemonChannelProxy(ws, workspaceId, clientUsername).catch((err) => {
|
|
1737
1830
|
console.error(`[cloud] Failed to setup channel subscription:`, err);
|
|
1738
1831
|
});
|
|
1739
1832
|
}
|
|
@@ -1759,16 +1852,30 @@ export async function createServer() {
|
|
|
1759
1852
|
ws.on('close', () => {
|
|
1760
1853
|
// Clean up daemon proxies
|
|
1761
1854
|
cleanupDaemonProxies(ws);
|
|
1855
|
+
const closingWorkspace = connectionWorkspace.get(ws);
|
|
1856
|
+
connectionWorkspace.delete(ws);
|
|
1762
1857
|
if (clientUsername) {
|
|
1763
1858
|
const userState = onlineUsers.get(clientUsername);
|
|
1764
1859
|
if (userState) {
|
|
1765
1860
|
userState.connections.delete(ws);
|
|
1861
|
+
// Remove workspace from user's set if no more connections to it
|
|
1862
|
+
if (closingWorkspace) {
|
|
1863
|
+
const stillHasWorkspaceConnection = Array.from(userState.connections).some((conn) => connectionWorkspace.get(conn) === closingWorkspace);
|
|
1864
|
+
if (!stillHasWorkspaceConnection) {
|
|
1865
|
+
userState.workspaceIds.delete(closingWorkspace);
|
|
1866
|
+
// Broadcast leave to users in that workspace
|
|
1867
|
+
wssPresence.clients.forEach((client) => {
|
|
1868
|
+
if (client.readyState === WebSocket.OPEN && connectionWorkspace.get(client) === closingWorkspace) {
|
|
1869
|
+
client.send(JSON.stringify({ type: 'presence_leave', username: clientUsername }));
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1766
1874
|
if (userState.connections.size === 0) {
|
|
1767
1875
|
onlineUsers.delete(clientUsername);
|
|
1768
1876
|
// Unregister from shared presence registry
|
|
1769
1877
|
unregisterUserPresence(clientUsername);
|
|
1770
1878
|
console.log(`[cloud] User ${clientUsername} disconnected`);
|
|
1771
|
-
broadcastPresence({ type: 'presence_leave', username: clientUsername });
|
|
1772
1879
|
}
|
|
1773
1880
|
}
|
|
1774
1881
|
}
|
|
@@ -1917,7 +2024,10 @@ export async function createServer() {
|
|
|
1917
2024
|
if (server) {
|
|
1918
2025
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
1919
2026
|
}
|
|
1920
|
-
|
|
2027
|
+
// Only quit Redis if client is still open
|
|
2028
|
+
if (redisClient.isOpen) {
|
|
2029
|
+
await redisClient.quit();
|
|
2030
|
+
}
|
|
1921
2031
|
},
|
|
1922
2032
|
};
|
|
1923
2033
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nango Integration Configuration
|
|
3
|
+
*
|
|
4
|
+
* REQUIRED SCOPES:
|
|
5
|
+
* - github (GITHUB_USER): Requires 'user:email' scope for email reconciliation.
|
|
6
|
+
* Configure this in Nango Dashboard: Integrations → GitHub → OAuth Scopes
|
|
7
|
+
*/
|
|
1
8
|
export declare const NANGO_INTEGRATIONS: {
|
|
2
9
|
readonly GITHUB_USER: "github";
|
|
3
10
|
readonly GITHUB_APP: "github-app-oauth";
|
|
@@ -29,6 +36,17 @@ declare class NangoService {
|
|
|
29
36
|
* Fetch GitHub user profile via Nango proxy.
|
|
30
37
|
*/
|
|
31
38
|
getGithubUser(connectionId: string): Promise<GithubUserProfile>;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch all email addresses associated with a GitHub user.
|
|
41
|
+
* Requires 'user:email' scope to be configured in Nango.
|
|
42
|
+
* @see https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
|
43
|
+
*/
|
|
44
|
+
getGithubUserEmails(connectionId: string): Promise<Array<{
|
|
45
|
+
email: string;
|
|
46
|
+
verified: boolean;
|
|
47
|
+
primary: boolean;
|
|
48
|
+
visibility: string | null;
|
|
49
|
+
}>>;
|
|
32
50
|
/**
|
|
33
51
|
* Retrieve an installation access token from a GitHub App connection.
|
|
34
52
|
* Use this ONLY when you need the raw token (e.g., for git clone URLs).
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Nango } from '@nangohq/node';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import { getConfig } from '../config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Nango Integration Configuration
|
|
6
|
+
*
|
|
7
|
+
* REQUIRED SCOPES:
|
|
8
|
+
* - github (GITHUB_USER): Requires 'user:email' scope for email reconciliation.
|
|
9
|
+
* Configure this in Nango Dashboard: Integrations → GitHub → OAuth Scopes
|
|
10
|
+
*/
|
|
4
11
|
export const NANGO_INTEGRATIONS = {
|
|
5
12
|
GITHUB_USER: 'github',
|
|
6
13
|
GITHUB_APP: 'github-app-oauth',
|
|
@@ -50,6 +57,31 @@ class NangoService {
|
|
|
50
57
|
});
|
|
51
58
|
return response.data;
|
|
52
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Fetch all email addresses associated with a GitHub user.
|
|
62
|
+
* Requires 'user:email' scope to be configured in Nango.
|
|
63
|
+
* @see https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
|
64
|
+
*/
|
|
65
|
+
async getGithubUserEmails(connectionId) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await this.client.get({
|
|
68
|
+
connectionId,
|
|
69
|
+
providerConfigKey: NANGO_INTEGRATIONS.GITHUB_USER,
|
|
70
|
+
endpoint: '/user/emails',
|
|
71
|
+
});
|
|
72
|
+
return response.data || [];
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// If scope is not granted, return empty array
|
|
76
|
+
const error = err;
|
|
77
|
+
if (error.response?.status === 403 || error.response?.status === 404) {
|
|
78
|
+
console.warn('[nango] Cannot fetch user emails - user:email scope may not be granted');
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
console.error('[nango] Error fetching user emails:', err);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
53
85
|
/**
|
|
54
86
|
* Retrieve an installation access token from a GitHub App connection.
|
|
55
87
|
* Use this ONLY when you need the raw token (e.g., for git clone URLs).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/cloud",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Cloud API server and services for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"test:watch": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@agent-relay/wrapper": "2.0.
|
|
42
|
-
"@agent-relay/config": "2.0.
|
|
43
|
-
"@agent-relay/resiliency": "2.0.
|
|
44
|
-
"@agent-relay/storage": "2.0.
|
|
45
|
-
"@agent-relay/protocol": "2.0.
|
|
41
|
+
"@agent-relay/wrapper": "2.0.18",
|
|
42
|
+
"@agent-relay/config": "2.0.18",
|
|
43
|
+
"@agent-relay/resiliency": "2.0.18",
|
|
44
|
+
"@agent-relay/storage": "2.0.18",
|
|
45
|
+
"@agent-relay/protocol": "2.0.18"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/config",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Shared configuration schemas and loaders for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"test:watch": "vitest"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@agent-relay/protocol": "2.0.
|
|
86
|
+
"@agent-relay/protocol": "2.0.18",
|
|
87
87
|
"zod": "^3.23.8",
|
|
88
88
|
"zod-to-json-schema": "^3.23.1"
|
|
89
89
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/daemon",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Relay daemon server - agent coordination and message routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/protocol": "2.0.
|
|
26
|
-
"@agent-relay/config": "2.0.
|
|
27
|
-
"@agent-relay/storage": "2.0.
|
|
28
|
-
"@agent-relay/bridge": "2.0.
|
|
29
|
-
"@agent-relay/utils": "2.0.
|
|
30
|
-
"@agent-relay/policy": "2.0.
|
|
31
|
-
"@agent-relay/memory": "2.0.
|
|
32
|
-
"@agent-relay/resiliency": "2.0.
|
|
33
|
-
"@agent-relay/user-directory": "2.0.
|
|
34
|
-
"@agent-relay/wrapper": "2.0.
|
|
35
|
-
"@agent-relay/telemetry": "2.0.
|
|
25
|
+
"@agent-relay/protocol": "2.0.18",
|
|
26
|
+
"@agent-relay/config": "2.0.18",
|
|
27
|
+
"@agent-relay/storage": "2.0.18",
|
|
28
|
+
"@agent-relay/bridge": "2.0.18",
|
|
29
|
+
"@agent-relay/utils": "2.0.18",
|
|
30
|
+
"@agent-relay/policy": "2.0.18",
|
|
31
|
+
"@agent-relay/memory": "2.0.18",
|
|
32
|
+
"@agent-relay/resiliency": "2.0.18",
|
|
33
|
+
"@agent-relay/user-directory": "2.0.18",
|
|
34
|
+
"@agent-relay/wrapper": "2.0.18",
|
|
35
|
+
"@agent-relay/telemetry": "2.0.18",
|
|
36
36
|
"ws": "^8.18.3",
|
|
37
37
|
"better-sqlite3": "^12.6.2",
|
|
38
38
|
"pg": "^8.16.3",
|
|
@@ -551,21 +551,50 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
551
551
|
const isValidUsername = (username) => {
|
|
552
552
|
if (typeof username !== 'string')
|
|
553
553
|
return false;
|
|
554
|
-
// Username
|
|
555
|
-
|
|
554
|
+
// Username can be:
|
|
555
|
+
// - GitHub-style: alphanumeric with hyphens (e.g., "khaliqgant")
|
|
556
|
+
// - Display name style: allows spaces for email users (e.g., "Khaliq Gant")
|
|
557
|
+
// Max 50 chars, must start/end with alphanumeric, no consecutive spaces
|
|
558
|
+
if (username.length === 0 || username.length > 50)
|
|
559
|
+
return false;
|
|
560
|
+
// Must start and end with alphanumeric
|
|
561
|
+
if (!/^[a-zA-Z0-9]/.test(username) || !/[a-zA-Z0-9]$/.test(username))
|
|
562
|
+
return false;
|
|
563
|
+
// Only allow alphanumeric, spaces, hyphens, underscores, and periods
|
|
564
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9 _.-]*[a-zA-Z0-9]$/.test(username) && username.length > 1)
|
|
565
|
+
return false;
|
|
566
|
+
// Single character usernames must be alphanumeric
|
|
567
|
+
if (username.length === 1 && !/^[a-zA-Z0-9]$/.test(username))
|
|
568
|
+
return false;
|
|
569
|
+
// No consecutive spaces
|
|
570
|
+
if (/ /.test(username))
|
|
571
|
+
return false;
|
|
572
|
+
return true;
|
|
556
573
|
};
|
|
557
574
|
const isValidAvatarUrl = (url) => {
|
|
558
575
|
if (url === undefined || url === null)
|
|
559
576
|
return true;
|
|
560
577
|
if (typeof url !== 'string')
|
|
561
578
|
return false;
|
|
562
|
-
// Must be a valid HTTPS URL from
|
|
579
|
+
// Must be a valid HTTPS URL from known avatar providers
|
|
563
580
|
try {
|
|
564
581
|
const parsed = new URL(url);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
582
|
+
if (parsed.protocol !== 'https:')
|
|
583
|
+
return false;
|
|
584
|
+
// Allow GitHub avatars
|
|
585
|
+
if (parsed.hostname === 'avatars.githubusercontent.com' ||
|
|
586
|
+
parsed.hostname === 'github.com' ||
|
|
587
|
+
parsed.hostname.endsWith('.githubusercontent.com'))
|
|
588
|
+
return true;
|
|
589
|
+
// Allow Gravatar for email-based avatars
|
|
590
|
+
if (parsed.hostname === 'www.gravatar.com' ||
|
|
591
|
+
parsed.hostname === 'gravatar.com' ||
|
|
592
|
+
parsed.hostname === 'secure.gravatar.com')
|
|
593
|
+
return true;
|
|
594
|
+
// Allow UI Avatars (placeholder service)
|
|
595
|
+
if (parsed.hostname === 'ui-avatars.com')
|
|
596
|
+
return true;
|
|
597
|
+
return false;
|
|
569
598
|
}
|
|
570
599
|
catch {
|
|
571
600
|
return false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/dashboard",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Web dashboard for Agent Relay - optional package for visual agent coordination",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"test:watch": "vitest"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@agent-relay/protocol": "2.0.
|
|
29
|
-
"@agent-relay/config": "2.0.
|
|
30
|
-
"@agent-relay/storage": "2.0.
|
|
31
|
-
"@agent-relay/bridge": "2.0.
|
|
32
|
-
"@agent-relay/utils": "2.0.
|
|
33
|
-
"@agent-relay/resiliency": "2.0.
|
|
34
|
-
"@agent-relay/trajectory": "2.0.
|
|
35
|
-
"@agent-relay/cloud": "2.0.
|
|
36
|
-
"@agent-relay/daemon": "2.0.
|
|
37
|
-
"@agent-relay/user-directory": "2.0.
|
|
38
|
-
"@agent-relay/wrapper": "2.0.
|
|
39
|
-
"@agent-relay/sdk": "2.0.
|
|
28
|
+
"@agent-relay/protocol": "2.0.18",
|
|
29
|
+
"@agent-relay/config": "2.0.18",
|
|
30
|
+
"@agent-relay/storage": "2.0.18",
|
|
31
|
+
"@agent-relay/bridge": "2.0.18",
|
|
32
|
+
"@agent-relay/utils": "2.0.18",
|
|
33
|
+
"@agent-relay/resiliency": "2.0.18",
|
|
34
|
+
"@agent-relay/trajectory": "2.0.18",
|
|
35
|
+
"@agent-relay/cloud": "2.0.18",
|
|
36
|
+
"@agent-relay/daemon": "2.0.18",
|
|
37
|
+
"@agent-relay/user-directory": "2.0.18",
|
|
38
|
+
"@agent-relay/wrapper": "2.0.18",
|
|
39
|
+
"@agent-relay/sdk": "2.0.18",
|
|
40
40
|
"express": "^5.2.1",
|
|
41
41
|
"ws": "^8.18.3"
|
|
42
42
|
},
|