Skip to content

Commit 05c6534

Browse files
authored
Merge pull request #83 from AgentWorkforce/feature/billing-bridge-fixes
Feature/billing bridge fixes
2 parents 13cb93b + ae0ebb7 commit 05c6534

File tree

19 files changed

+853
-715
lines changed

19 files changed

+853
-715
lines changed

.beads/issues.jsonl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@
133133
{"id":"agent-relay-468","title":"Implement messages API endpoints","description":"Create new router: src/cloud/api/messages.ts\n\nEndpoints:\n1. GET /api/messages/conversations\n - List all conversations for user in workspace\n - Include unread count per conversation\n - Sort by last_message_at descending\n\n2. GET /api/messages/conversations/:conversationId\n - Paginated message list (50 per page, cursor-based)\n - Include sender name/avatar lookup\n\n3. POST /api/messages/send\n - Body: { recipientId, recipientType, workspaceId, content, contentType? }\n - Create conversation if doesn't exist\n - Store message\n - Return message + conversationId\n\n4. POST /api/messages/conversations/:conversationId/read\n - Update message_read_state\n - Return success\n\n5. GET /api/messages/unread-count\n - Total unread across all conversations\n - For badge display\n\nAll endpoints require workspace access (use requireWorkspaceAccess middleware)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:32.043028+01:00","updated_at":"2026-01-06T00:14:32.043028+01:00","dependencies":[{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-467","type":"blocks","created_at":"2026-01-06T14:07:38.45694+01:00","created_by":"import"},{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-464","type":"blocks","created_at":"2026-01-06T14:07:38.457577+01:00","created_by":"import"}]}
134134
{"id":"agent-relay-469","title":"Extend presence WebSocket for real-time DM delivery","description":"Extend /ws/presence WebSocket handler to support DM delivery:\n\nNew message types:\n1. dm_send (client-\u003eserver)\n - recipientId, recipientType, content, tempId\n - Store message via messages API\n - Forward to recipient's WebSocket connection\n\n2. dm_received (server-\u003eclient)\n - Full message object\n - Sent to recipient when they're online\n\n3. dm_sent (server-\u003eclient)\n - Confirmation with tempId for optimistic UI\n - Sent back to sender\n\n4. dm_typing (both directions)\n - conversationId, userId, userName\n - Debounced, auto-expires after 3s\n\nAgent bridging:\n- If recipientType='agent', forward via daemon WebSocket\n- Store message for history\n- Handle agent responses coming back\n\nLocation: src/cloud/server.ts (presence WebSocket section)\n\nConsider: separate /ws/messages endpoint vs extending presence","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:44.150375+01:00","updated_at":"2026-01-06T00:14:44.150375+01:00","dependencies":[{"issue_id":"agent-relay-469","depends_on_id":"agent-relay-468","type":"blocks","created_at":"2026-01-06T14:07:38.458202+01:00","created_by":"import"}]}
135135
{"id":"agent-relay-470","title":"Build messaging UI components for dashboard","description":"Create React components for messaging:\n\n1. ConversationList\n - Sidebar showing all DM threads\n - Unread count badges\n - Click to select conversation\n - \"New DM\" button\n\n2. MessageThread\n - Main chat view\n - Infinite scroll for history\n - New message indicator\n - Typing indicator display\n\n3. MessageInput\n - Text input with send button\n - Typing indicator emission\n - Markdown/code toggle (optional v1)\n\n4. UserSelector\n - Modal/dropdown to start new DM\n - Show online workspace members + agents\n - Search/filter\n\n5. UnreadBadge\n - Notification count component\n - Used in navigation\n\nIntegration:\n- Add to workspace dashboard layout\n- Connect to WebSocket for real-time\n- Use API for initial data + history\n\nLocation: src/dashboard/react-components/messaging/\nStyling: Follow existing Tailwind patterns","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:56.997803+01:00","updated_at":"2026-01-06T00:14:56.997803+01:00","dependencies":[{"issue_id":"agent-relay-470","depends_on_id":"agent-relay-469","type":"blocks","created_at":"2026-01-06T14:07:38.458868+01:00","created_by":"import"}]}
136+
{"id":"agent-relay-471","title":"Use 1GB instance for free tier workspaces","description":"Reduce free tier Fly.io costs by using smaller instance. Change provisioner to use 1 CPU / 1GB RAM (shared-cpu-1x) for free tier users instead of 2 CPU / 2GB. Saves ~$8/user/month.","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-06T15:47:42.391116+01:00","updated_at":"2026-01-06T15:47:42.391116+01:00"}
137+
{"id":"agent-relay-472","title":"Add cron to enforce free tier 5-hour compute limit","description":"Add enforcement for free tier compute hours. Create a cron job that: 1) Checks all free tier users' compute usage, 2) Stops workspaces when 5 hours reached, 3) Sends notification email. Run every 15 minutes.","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-06T15:47:44.48545+01:00","updated_at":"2026-01-06T15:47:44.48545+01:00"}
138+
{"id":"agent-relay-473","title":"Add remaining compute hours banner for free tier","description":"Show free tier users their remaining compute hours in the dashboard. Display banner with: 'X of 5 hours used this month' and upgrade CTA when approaching limit (\u003e4 hours). Include in workspace header.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T15:47:45.396171+01:00","updated_at":"2026-01-06T15:47:45.396171+01:00"}
136139
{"id":"agent-relay-47z","title":"Express 5 may have breaking changes from Express 4 patterns","description":"package.json uses express@5.2.1 which is a major version with breaking changes from Express 4. Verify: (1) Error handling middleware patterns, (2) Router behavior, (3) Body parsing (express.json vs body-parser).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:49.269841+01:00","updated_at":"2025-12-20T00:18:49.269841+01:00"}
137140
{"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","assignee":"MistyShelter","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"}
138141
{"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","assignee":"Pruner","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"}

src/cloud/api/billing.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@ export const billingRouter = Router();
1919
* Get all available billing plans
2020
*/
2121
billingRouter.get('/plans', (req, res) => {
22-
const plans = getAllPlans();
22+
const rawPlans = getAllPlans();
23+
24+
// Transform plans to frontend format
25+
const plans = rawPlans.map((plan) => ({
26+
tier: plan.id,
27+
name: plan.name,
28+
description: plan.description,
29+
price: {
30+
monthly: plan.priceMonthly / 100, // Convert cents to dollars
31+
yearly: plan.priceYearly / 100,
32+
},
33+
features: plan.features,
34+
limits: plan.limits,
35+
recommended: plan.id === 'pro',
36+
}));
2337

2438
// Add publishable key for frontend
2539
const config = getConfig();

src/cloud/api/daemons.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Router, Request, Response } from 'express';
1313
import { randomBytes, createHash } from 'crypto';
1414
import { requireAuth } from './auth.js';
1515
import { db } from '../db/index.js';
16-
import { vault } from '../vault/index.js';
1716

1817
export const daemonsRouter = Router();
1918

@@ -235,24 +234,29 @@ daemonsRouter.post('/heartbeat', requireDaemonAuth as any, async (req: Request,
235234

236235
/**
237236
* GET /api/daemons/credentials
238-
* Get credentials for the daemon's user (syncs cloud credentials to local)
237+
* Get credentials for the daemon's user
238+
*
239+
* Note: Tokens are no longer stored centrally. CLI tools authenticate directly
240+
* on workspace/local instances. This endpoint returns connected provider info only.
239241
*/
240242
daemonsRouter.get('/credentials', requireDaemonAuth as any, async (req: Request, res: Response) => {
241243
const daemon = (req as any).daemon;
242244

243245
try {
244-
// Get all decrypted credentials for the user via vault
245-
const credentialsMap = await vault.getUserCredentials(daemon.userId);
246-
247-
// Convert Map to array format for API response
248-
const credentials = Array.from(credentialsMap.entries()).map(([provider, cred]) => ({
249-
provider,
250-
accessToken: cred.accessToken,
251-
tokenType: 'bearer',
252-
expiresAt: cred.tokenExpiresAt,
246+
// Get connected providers for this user (no tokens stored centrally)
247+
const credentials = await db.credentials.findByUserId(daemon.userId);
248+
249+
// Return provider info without tokens
250+
const providers = credentials.map((cred) => ({
251+
provider: cred.provider,
252+
providerAccountEmail: cred.providerAccountEmail,
253+
connectedAt: cred.createdAt,
253254
}));
254255

255-
res.json({ credentials });
256+
res.json({
257+
providers,
258+
note: 'Tokens are authenticated locally on workspace instances via CLI.',
259+
});
256260
} catch (error) {
257261
console.error('Error fetching credentials:', error);
258262
res.status(500).json({ error: 'Failed to fetch credentials' });

src/cloud/api/onboarding.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type { IPty } from 'node-pty';
1515
import * as crypto from 'crypto';
1616
import { requireAuth } from './auth.js';
1717
import { db } from '../db/index.js';
18-
import { vault } from '../vault/index.js';
1918

2019
// Import for local use
2120
import {
@@ -414,13 +413,11 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req: Request,
414413
});
415414
}
416415

417-
// Store in vault with refresh token and expiry
418-
await vault.storeCredential({
416+
// Mark provider as connected (tokens are not stored centrally - CLI tools
417+
// authenticate directly on workspace instances)
418+
await db.credentials.upsert({
419419
userId,
420420
provider,
421-
accessToken,
422-
refreshToken,
423-
tokenExpiresAt,
424421
scopes: getProviderScopes(provider),
425422
});
426423

@@ -568,22 +565,23 @@ onboardingRouter.post('/token/:provider', async (req: Request, res: Response) =>
568565
return res.status(400).json({ error: 'Invalid token' });
569566
}
570567

571-
// Store in vault
572-
await vault.storeCredential({
568+
// Mark provider as connected (tokens are not stored centrally - CLI tools
569+
// authenticate directly on workspace instances)
570+
await db.credentials.upsert({
573571
userId,
574572
provider,
575-
accessToken: token,
576573
scopes: getProviderScopes(provider),
577574
providerAccountEmail: email,
578575
});
579576

580577
res.json({
581578
success: true,
582579
message: `${provider} connected successfully`,
580+
note: 'Token validated. Configure this on your workspace for usage.',
583581
});
584582
} catch (error) {
585-
console.error(`Error storing token for ${provider}:`, error);
586-
res.status(500).json({ error: 'Failed to store token' });
583+
console.error(`Error storing provider connection for ${provider}:`, error);
584+
res.status(500).json({ error: 'Failed to store provider connection' });
587585
}
588586
});
589587

src/cloud/api/providers.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { createClient } from 'redis';
1010
import { requireAuth } from './auth.js';
1111
import { getConfig } from '../config.js';
1212
import { db } from '../db/index.js';
13-
import { vault } from '../vault/index.js';
1413

1514
export const providersRouter = Router();
1615

@@ -334,20 +333,19 @@ providersRouter.post('/:provider/verify', async (req: Request, res: Response) =>
334333
// In production, we'd verify by making a test API call with the credentials
335334

336335
try {
337-
// For now, mark as connected (in production, verify credentials exist)
338-
// This would be called after the user's workspace detects valid credentials
336+
// Mark as connected (tokens are not stored centrally - CLI tools
337+
// authenticate directly on workspace instances)
339338
await db.credentials.upsert({
340339
userId,
341340
provider,
342-
accessToken: 'cli-authenticated', // Placeholder - real token from CLI
343341
scopes: [], // CLI auth doesn't use scopes
344342
providerAccountEmail: req.body.email, // User can optionally provide
345343
});
346344

347345
res.json({
348346
success: true,
349347
message: `${providerConfig.displayName} connected via CLI`,
350-
note: 'Credentials will be synced when workspace starts',
348+
note: 'CLI credentials remain on your local machine',
351349
});
352350
} catch (error) {
353351
console.error(`Error verifying ${provider} auth:`, error);
@@ -403,18 +401,19 @@ providersRouter.post('/:provider/api-key', async (req: Request, res: Response) =
403401
return res.status(400).json({ error: 'Invalid API key' });
404402
}
405403

406-
// Store the API key - use scopes from device flow providers, empty for CLI providers
404+
// Mark provider as connected (tokens are not stored centrally - CLI tools
405+
// authenticate directly on workspace instances)
407406
const scopes = isDeviceFlowProvider(providerConfig) ? providerConfig.scopes : [];
408-
await vault.storeCredential({
407+
await db.credentials.upsert({
409408
userId,
410409
provider,
411-
accessToken: apiKey,
412410
scopes,
413411
});
414412

415413
res.json({
416414
success: true,
417415
message: `${providerConfig.displayName} connected`,
416+
note: 'API key validated. Configure this key on your workspace for usage.',
418417
});
419418
} catch (error) {
420419
console.error(`Error connecting ${provider} with API key:`, error);
@@ -592,7 +591,9 @@ async function pollForToken(flowId: string, provider: ProviderType, clientId: st
592591
}
593592

594593
/**
595-
* Store tokens after successful device flow
594+
* Mark provider as connected after successful device flow
595+
* Note: Tokens are not stored centrally - CLI tools authenticate directly
596+
* on workspace instances. We only record the connection status and user info.
596597
*/
597598
async function storeProviderTokens(
598599
userId: string,
@@ -623,15 +624,10 @@ async function storeProviderTokens(
623624
}
624625
}
625626

626-
// Encrypt and store
627-
await vault.storeCredential({
627+
// Mark provider as connected (without storing tokens)
628+
await db.credentials.upsert({
628629
userId,
629630
provider,
630-
accessToken: tokens.accessToken,
631-
refreshToken: tokens.refreshToken,
632-
tokenExpiresAt: tokens.expiresIn
633-
? new Date(Date.now() + tokens.expiresIn * 1000)
634-
: undefined,
635631
scopes: tokens.scope?.split(' '),
636632
providerAccountId: userInfo.id,
637633
providerAccountEmail: userInfo.email,

0 commit comments

Comments
 (0)