agent-relay 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trajectories/agent-relay-322-324.md +17 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
- package/.trajectories/consolidate-settings-panel.md +24 -0
- package/.trajectories/gh-cli-user-token.md +26 -0
- package/.trajectories/index.json +155 -1
- package/deploy/workspace/codex.config.toml +15 -0
- package/deploy/workspace/entrypoint.sh +167 -7
- package/deploy/workspace/git-credential-relay +17 -2
- package/dist/bridge/spawner.d.ts +7 -0
- package/dist/bridge/spawner.js +40 -9
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.js +210 -168
- package/dist/cloud/api/admin.d.ts +8 -0
- package/dist/cloud/api/admin.js +212 -0
- package/dist/cloud/api/auth.js +8 -0
- package/dist/cloud/api/billing.d.ts +0 -10
- package/dist/cloud/api/billing.js +248 -58
- package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
- package/dist/cloud/api/codex-auth-helper.js +215 -8
- package/dist/cloud/api/coordinators.js +402 -0
- package/dist/cloud/api/daemons.js +15 -11
- package/dist/cloud/api/git.js +104 -17
- package/dist/cloud/api/github-app.js +42 -8
- package/dist/cloud/api/nango-auth.js +297 -16
- package/dist/cloud/api/onboarding.js +97 -33
- package/dist/cloud/api/providers.js +12 -16
- package/dist/cloud/api/repos.js +200 -124
- package/dist/cloud/api/test-helpers.js +40 -0
- package/dist/cloud/api/usage.js +13 -0
- package/dist/cloud/api/webhooks.js +1 -1
- package/dist/cloud/api/workspaces.d.ts +18 -0
- package/dist/cloud/api/workspaces.js +945 -15
- package/dist/cloud/config.d.ts +8 -0
- package/dist/cloud/config.js +15 -0
- package/dist/cloud/db/drizzle.d.ts +5 -2
- package/dist/cloud/db/drizzle.js +27 -20
- package/dist/cloud/db/schema.d.ts +19 -51
- package/dist/cloud/db/schema.js +5 -4
- package/dist/cloud/index.d.ts +0 -1
- package/dist/cloud/index.js +0 -1
- package/dist/cloud/provisioner/index.d.ts +93 -1
- package/dist/cloud/provisioner/index.js +608 -63
- package/dist/cloud/server.js +156 -16
- package/dist/cloud/services/compute-enforcement.d.ts +57 -0
- package/dist/cloud/services/compute-enforcement.js +175 -0
- package/dist/cloud/services/index.d.ts +2 -0
- package/dist/cloud/services/index.js +4 -0
- package/dist/cloud/services/intro-expiration.d.ts +55 -0
- package/dist/cloud/services/intro-expiration.js +211 -0
- package/dist/cloud/services/nango.d.ts +14 -0
- package/dist/cloud/services/nango.js +74 -14
- package/dist/cloud/services/ssh-security.d.ts +31 -0
- package/dist/cloud/services/ssh-security.js +63 -0
- package/dist/continuity/manager.d.ts +5 -0
- package/dist/continuity/manager.js +56 -2
- package/dist/daemon/api.d.ts +2 -0
- package/dist/daemon/api.js +214 -5
- package/dist/daemon/cli-auth.d.ts +13 -1
- package/dist/daemon/cli-auth.js +166 -47
- package/dist/daemon/connection.d.ts +7 -1
- package/dist/daemon/connection.js +15 -0
- package/dist/daemon/orchestrator.d.ts +2 -0
- package/dist/daemon/orchestrator.js +26 -0
- package/dist/daemon/repo-manager.d.ts +116 -0
- package/dist/daemon/repo-manager.js +384 -0
- package/dist/daemon/router.d.ts +60 -1
- package/dist/daemon/router.js +281 -20
- package/dist/daemon/user-directory.d.ts +111 -0
- package/dist/daemon/user-directory.js +233 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/{page-abb9ab2d329f56e9.js → page-8c8bed33beb2bf1c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/{page-c22d080201cbd9fb.js → page-16f3b49e55b1e0ed.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-4a5938c18a11a654.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/{page-68d34f50baa8ab6b.js → page-547dd0ca55ecd0ba.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-app-6e8e8d3ef4e0192a.js → main-app-5d692157a8eb1fd9.js} +1 -1
- package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +1 -0
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +3 -3
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +3 -3
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +3 -3
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +3 -3
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +3 -3
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +3 -3
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +3 -3
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +3 -3
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +3 -3
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +3 -3
- package/dist/dashboard-server/server.js +316 -12
- package/dist/dashboard-server/user-bridge.d.ts +103 -0
- package/dist/dashboard-server/user-bridge.js +189 -0
- package/dist/protocol/channels.d.ts +205 -0
- package/dist/protocol/channels.js +154 -0
- package/dist/protocol/types.d.ts +13 -1
- package/dist/resiliency/provider-context.js +2 -0
- package/dist/shared/cli-auth-config.d.ts +19 -0
- package/dist/shared/cli-auth-config.js +58 -2
- package/dist/utils/agent-config.js +1 -1
- package/dist/wrapper/auth-detection.d.ts +49 -0
- package/dist/wrapper/auth-detection.js +192 -0
- package/dist/wrapper/base-wrapper.d.ts +153 -0
- package/dist/wrapper/base-wrapper.js +393 -0
- package/dist/wrapper/client.d.ts +7 -1
- package/dist/wrapper/client.js +3 -0
- package/dist/wrapper/index.d.ts +1 -0
- package/dist/wrapper/index.js +4 -3
- package/dist/wrapper/pty-wrapper.d.ts +62 -84
- package/dist/wrapper/pty-wrapper.js +154 -180
- package/dist/wrapper/tmux-wrapper.d.ts +41 -66
- package/dist/wrapper/tmux-wrapper.js +90 -134
- package/package.json +4 -2
- package/scripts/postinstall.js +11 -155
- package/scripts/test-interactive-terminal.sh +248 -0
- package/dist/cloud/vault/index.d.ts +0 -76
- package/dist/cloud/vault/index.js +0 -219
- package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-3fdfa60e53f2810d.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
- package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
- package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
- /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → T1tgCqVWHFIkV7ClEtzD7}/_buildManifest.js +0 -0
package/dist/cloud/server.js
CHANGED
|
@@ -8,13 +8,14 @@ import helmet from 'helmet';
|
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import http from 'node:http';
|
|
11
|
+
import fs from 'node:fs';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { createClient } from 'redis';
|
|
13
14
|
import { RedisStore } from 'connect-redis';
|
|
14
15
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
15
16
|
import { getConfig } from './config.js';
|
|
16
17
|
import { runMigrations } from './db/index.js';
|
|
17
|
-
import { getScalingOrchestrator } from './services/index.js';
|
|
18
|
+
import { getScalingOrchestrator, getComputeEnforcementService, getIntroExpirationService } from './services/index.js';
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = path.dirname(__filename);
|
|
20
21
|
// API routers
|
|
@@ -35,7 +36,9 @@ import { githubAppRouter } from './api/github-app.js';
|
|
|
35
36
|
import { nangoAuthRouter } from './api/nango-auth.js';
|
|
36
37
|
import { gitRouter } from './api/git.js';
|
|
37
38
|
import { codexAuthHelperRouter } from './api/codex-auth-helper.js';
|
|
39
|
+
import { adminRouter } from './api/admin.js';
|
|
38
40
|
import { db } from './db/index.js';
|
|
41
|
+
import { validateSshSecurityConfig } from './services/ssh-security.js';
|
|
39
42
|
/**
|
|
40
43
|
* Proxy a request to the user's primary running workspace
|
|
41
44
|
*/
|
|
@@ -66,6 +69,8 @@ async function proxyToUserWorkspace(req, res, path) {
|
|
|
66
69
|
}
|
|
67
70
|
export async function createServer() {
|
|
68
71
|
const config = getConfig();
|
|
72
|
+
// Validate security configuration at startup
|
|
73
|
+
validateSshSecurityConfig();
|
|
69
74
|
const app = express();
|
|
70
75
|
app.set('trust proxy', 1);
|
|
71
76
|
// Redis client for sessions
|
|
@@ -226,27 +231,37 @@ export async function createServer() {
|
|
|
226
231
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
227
232
|
});
|
|
228
233
|
// API routes
|
|
229
|
-
|
|
234
|
+
//
|
|
235
|
+
// IMPORTANT: Route order matters! Routes with non-session auth (webhooks, API keys, tokens)
|
|
236
|
+
// must be mounted BEFORE teamsRouter, which catches all /api/* with requireAuth.
|
|
237
|
+
//
|
|
238
|
+
// --- Routes with alternative auth (must be before teamsRouter) ---
|
|
239
|
+
app.use('/api/auth', authRouter); // Login endpoints (public)
|
|
240
|
+
app.use('/api/auth/nango', nangoAuthRouter); // Nango webhook (signature verification)
|
|
241
|
+
app.use('/api/auth/codex-helper', codexAuthHelperRouter);
|
|
242
|
+
app.use('/api/git', gitRouter); // Workspace token auth
|
|
243
|
+
app.use('/api/webhooks', webhooksRouter); // GitHub webhooks (signature verification)
|
|
244
|
+
app.use('/api/monitoring', monitoringRouter); // Daemon API key auth endpoints
|
|
245
|
+
app.use('/api/daemons', daemonsRouter); // Daemon API key auth endpoints
|
|
246
|
+
app.use('/api/admin', adminRouter); // Admin API secret auth
|
|
247
|
+
// --- Routes with session auth ---
|
|
230
248
|
app.use('/api/providers', providersRouter);
|
|
231
249
|
app.use('/api/workspaces', workspacesRouter);
|
|
232
250
|
app.use('/api/repos', reposRouter);
|
|
233
251
|
app.use('/api/onboarding', onboardingRouter);
|
|
234
|
-
app.use('/api/teams', teamsRouter);
|
|
235
252
|
app.use('/api/billing', billingRouter);
|
|
236
253
|
app.use('/api/usage', usageRouter);
|
|
237
254
|
app.use('/api/project-groups', coordinatorsRouter);
|
|
238
|
-
app.use('/api/daemons', daemonsRouter);
|
|
239
|
-
app.use('/api/monitoring', monitoringRouter);
|
|
240
|
-
app.use('/api/webhooks', webhooksRouter);
|
|
241
255
|
app.use('/api/github-app', githubAppRouter);
|
|
242
|
-
app.use('/api/auth/nango', nangoAuthRouter);
|
|
243
|
-
app.use('/api/auth/codex-helper', codexAuthHelperRouter);
|
|
244
|
-
app.use('/api/git', gitRouter);
|
|
245
256
|
// Test helper routes (only available in non-production)
|
|
257
|
+
// MUST be before teamsRouter to avoid auth interception
|
|
246
258
|
if (process.env.NODE_ENV !== 'production') {
|
|
247
259
|
app.use('/api/test', testHelpersRouter);
|
|
248
260
|
console.log('[cloud] Test helper routes enabled (non-production mode)');
|
|
249
261
|
}
|
|
262
|
+
// Teams router - MUST BE LAST among /api routes
|
|
263
|
+
// Handles /workspaces/:id/members and /invites with requireAuth on all routes
|
|
264
|
+
app.use('/api', teamsRouter);
|
|
250
265
|
// Trajectory proxy routes - auto-detect user's workspace and forward
|
|
251
266
|
// These are convenience routes so the dashboard doesn't need to know the workspace ID
|
|
252
267
|
app.get('/api/trajectory', requireAuth, async (req, res) => {
|
|
@@ -264,17 +279,29 @@ export async function createServer() {
|
|
|
264
279
|
// Serve static dashboard files (Next.js static export)
|
|
265
280
|
// Path: dist/cloud/server.js -> ../../src/dashboard/out
|
|
266
281
|
const dashboardPath = path.join(__dirname, '../../src/dashboard/out');
|
|
267
|
-
// Serve static files
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
//
|
|
282
|
+
// Serve static files (JS, CSS, images, etc.)
|
|
283
|
+
app.use(express.static(dashboardPath));
|
|
284
|
+
// Handle clean URLs for Next.js static export
|
|
285
|
+
// When a directory exists (e.g., /app/), express.static won't serve app.html
|
|
286
|
+
// So we need to explicitly check for .html files
|
|
272
287
|
app.get('/{*splat}', (req, res, next) => {
|
|
273
|
-
// Don't
|
|
288
|
+
// Don't handle API routes
|
|
274
289
|
if (req.path.startsWith('/api/')) {
|
|
275
290
|
return next();
|
|
276
291
|
}
|
|
277
|
-
|
|
292
|
+
// Clean the path (remove trailing slash)
|
|
293
|
+
const cleanPath = req.path.replace(/\/$/, '') || '/';
|
|
294
|
+
// Try to serve the corresponding .html file
|
|
295
|
+
const htmlFile = cleanPath === '/' ? 'index.html' : `${cleanPath}.html`;
|
|
296
|
+
const htmlPath = path.join(dashboardPath, htmlFile);
|
|
297
|
+
// Check if the HTML file exists
|
|
298
|
+
if (fs.existsSync(htmlPath)) {
|
|
299
|
+
res.sendFile(htmlPath);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Fallback to index.html for SPA-style routing
|
|
303
|
+
res.sendFile(path.join(dashboardPath, 'index.html'));
|
|
304
|
+
}
|
|
278
305
|
});
|
|
279
306
|
// Error handler
|
|
280
307
|
app.use((err, req, res, _next) => {
|
|
@@ -287,6 +314,8 @@ export async function createServer() {
|
|
|
287
314
|
// Server lifecycle
|
|
288
315
|
let server = null;
|
|
289
316
|
let scalingOrchestrator = null;
|
|
317
|
+
let computeEnforcement = null;
|
|
318
|
+
let introExpiration = null;
|
|
290
319
|
// Create HTTP server for WebSocket upgrade handling
|
|
291
320
|
const httpServer = http.createServer(app);
|
|
292
321
|
// ===== Presence WebSocket =====
|
|
@@ -318,6 +347,77 @@ export async function createServer() {
|
|
|
318
347
|
return false;
|
|
319
348
|
}
|
|
320
349
|
};
|
|
350
|
+
// WebSocket server for agent logs (proxied to workspace daemon)
|
|
351
|
+
const wssLogs = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
352
|
+
// Handle agent logs WebSocket connections
|
|
353
|
+
wssLogs.on('connection', async (clientWs, workspaceId, agentName) => {
|
|
354
|
+
console.log(`[ws/logs] Client connected for workspace=${workspaceId} agent=${agentName}`);
|
|
355
|
+
let daemonWs = null;
|
|
356
|
+
try {
|
|
357
|
+
// Find the workspace
|
|
358
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
359
|
+
if (!workspace || !workspace.publicUrl) {
|
|
360
|
+
clientWs.send(JSON.stringify({ type: 'error', message: 'Workspace not found or not running' }));
|
|
361
|
+
clientWs.close();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Connect to workspace daemon WebSocket
|
|
365
|
+
// The workspace runs the dashboard server which expects /ws/logs path
|
|
366
|
+
const baseUrl = workspace.publicUrl.replace(/^http/, 'ws').replace(/\/$/, '');
|
|
367
|
+
const daemonWsUrl = `${baseUrl}/ws/logs/${encodeURIComponent(agentName)}`;
|
|
368
|
+
console.log(`[ws/logs] Connecting to daemon: ${daemonWsUrl}`);
|
|
369
|
+
daemonWs = new WebSocket(daemonWsUrl, { perMessageDeflate: false });
|
|
370
|
+
daemonWs.on('open', () => {
|
|
371
|
+
console.log(`[ws/logs] Connected to daemon for ${agentName}`);
|
|
372
|
+
// Note: No need to send subscribe message - the agent name in the URL path
|
|
373
|
+
// triggers auto-subscription in the dashboard server
|
|
374
|
+
});
|
|
375
|
+
daemonWs.on('message', (data) => {
|
|
376
|
+
// Forward daemon messages to client
|
|
377
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
378
|
+
clientWs.send(data.toString());
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
daemonWs.on('close', () => {
|
|
382
|
+
console.log(`[ws/logs] Daemon connection closed for ${agentName}`);
|
|
383
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
384
|
+
clientWs.close();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
daemonWs.on('error', (err) => {
|
|
388
|
+
console.error(`[ws/logs] Daemon WebSocket error:`, err);
|
|
389
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
390
|
+
clientWs.send(JSON.stringify({ type: 'error', message: 'Daemon connection error' }));
|
|
391
|
+
clientWs.close();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// Forward client messages to daemon (for user input)
|
|
395
|
+
clientWs.on('message', (data) => {
|
|
396
|
+
if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
|
|
397
|
+
daemonWs.send(data.toString());
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
clientWs.on('close', () => {
|
|
401
|
+
console.log(`[ws/logs] Client disconnected for ${agentName}`);
|
|
402
|
+
if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
|
|
403
|
+
daemonWs.close();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
clientWs.on('error', (err) => {
|
|
407
|
+
console.error(`[ws/logs] Client WebSocket error:`, err);
|
|
408
|
+
if (daemonWs && daemonWs.readyState === WebSocket.OPEN) {
|
|
409
|
+
daemonWs.close();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
console.error(`[ws/logs] Setup error:`, err);
|
|
415
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
416
|
+
clientWs.send(JSON.stringify({ type: 'error', message: 'Failed to connect to workspace' }));
|
|
417
|
+
clientWs.close();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
321
421
|
// Handle HTTP upgrade for WebSocket
|
|
322
422
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
323
423
|
const pathname = new URL(request.url || '', `http://${request.headers.host}`).pathname;
|
|
@@ -326,6 +426,20 @@ export async function createServer() {
|
|
|
326
426
|
wssPresence.emit('connection', ws, request);
|
|
327
427
|
});
|
|
328
428
|
}
|
|
429
|
+
else if (pathname.startsWith('/ws/logs/')) {
|
|
430
|
+
// Parse /ws/logs/:workspaceId/:agentName
|
|
431
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
432
|
+
if (parts.length >= 4) {
|
|
433
|
+
const workspaceId = decodeURIComponent(parts[2]);
|
|
434
|
+
const agentName = decodeURIComponent(parts[3]);
|
|
435
|
+
wssLogs.handleUpgrade(request, socket, head, (ws) => {
|
|
436
|
+
wssLogs.emit('connection', ws, workspaceId, agentName);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
socket.destroy();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
329
443
|
else {
|
|
330
444
|
// Unknown WebSocket path - destroy socket
|
|
331
445
|
socket.destroy();
|
|
@@ -512,6 +626,24 @@ export async function createServer() {
|
|
|
512
626
|
console.warn('[cloud] Failed to initialize scaling orchestrator:', error);
|
|
513
627
|
// Non-fatal - server can run without auto-scaling
|
|
514
628
|
}
|
|
629
|
+
// Start compute enforcement service (checks every 15 min)
|
|
630
|
+
try {
|
|
631
|
+
computeEnforcement = getComputeEnforcementService();
|
|
632
|
+
computeEnforcement.start();
|
|
633
|
+
console.log('[cloud] Compute enforcement service started');
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
console.warn('[cloud] Failed to start compute enforcement:', error);
|
|
637
|
+
}
|
|
638
|
+
// Start intro expiration service (checks every hour for expired intro periods)
|
|
639
|
+
try {
|
|
640
|
+
introExpiration = getIntroExpirationService();
|
|
641
|
+
introExpiration.start();
|
|
642
|
+
console.log('[cloud] Intro expiration service started');
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
console.warn('[cloud] Failed to start intro expiration:', error);
|
|
646
|
+
}
|
|
515
647
|
}
|
|
516
648
|
return new Promise((resolve) => {
|
|
517
649
|
server = httpServer.listen(config.port, () => {
|
|
@@ -527,6 +659,14 @@ export async function createServer() {
|
|
|
527
659
|
if (scalingOrchestrator) {
|
|
528
660
|
await scalingOrchestrator.shutdown();
|
|
529
661
|
}
|
|
662
|
+
// Stop compute enforcement service
|
|
663
|
+
if (computeEnforcement) {
|
|
664
|
+
computeEnforcement.stop();
|
|
665
|
+
}
|
|
666
|
+
// Stop intro expiration service
|
|
667
|
+
if (introExpiration) {
|
|
668
|
+
introExpiration.stop();
|
|
669
|
+
}
|
|
530
670
|
// Close WebSocket server
|
|
531
671
|
wssPresence.close();
|
|
532
672
|
if (server) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute Enforcement Service
|
|
3
|
+
*
|
|
4
|
+
* Enforces compute hour limits for free tier users.
|
|
5
|
+
* Runs periodically to check usage and stop workspaces that have exceeded limits.
|
|
6
|
+
*/
|
|
7
|
+
import { PlanType } from '../db/index.js';
|
|
8
|
+
export interface ComputeEnforcementConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
checkIntervalMs: number;
|
|
11
|
+
warningThresholdPercent: number;
|
|
12
|
+
}
|
|
13
|
+
export interface EnforcementResult {
|
|
14
|
+
userId: string;
|
|
15
|
+
plan: PlanType;
|
|
16
|
+
computeHoursUsed: number;
|
|
17
|
+
computeHoursLimit: number;
|
|
18
|
+
action: 'none' | 'warning' | 'stopped';
|
|
19
|
+
workspacesStopped: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare class ComputeEnforcementService {
|
|
22
|
+
private config;
|
|
23
|
+
private checkTimer;
|
|
24
|
+
private isRunning;
|
|
25
|
+
constructor(config?: Partial<ComputeEnforcementConfig>);
|
|
26
|
+
/**
|
|
27
|
+
* Start the enforcement service
|
|
28
|
+
*/
|
|
29
|
+
start(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Stop the enforcement service
|
|
32
|
+
*/
|
|
33
|
+
stop(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Run enforcement check for all free tier users
|
|
36
|
+
*/
|
|
37
|
+
runEnforcement(): Promise<EnforcementResult[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Enforce limits for a specific user
|
|
40
|
+
*/
|
|
41
|
+
enforceUserLimits(userId: string): Promise<EnforcementResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Manually trigger enforcement for a specific user
|
|
44
|
+
*/
|
|
45
|
+
enforceUser(userId: string): Promise<EnforcementResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Get service status
|
|
48
|
+
*/
|
|
49
|
+
getStatus(): {
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
isRunning: boolean;
|
|
52
|
+
checkIntervalMs: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export declare function getComputeEnforcementService(): ComputeEnforcementService;
|
|
56
|
+
export declare function createComputeEnforcementService(config?: Partial<ComputeEnforcementConfig>): ComputeEnforcementService;
|
|
57
|
+
//# sourceMappingURL=compute-enforcement.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute Enforcement Service
|
|
3
|
+
*
|
|
4
|
+
* Enforces compute hour limits for free tier users.
|
|
5
|
+
* Runs periodically to check usage and stop workspaces that have exceeded limits.
|
|
6
|
+
*/
|
|
7
|
+
import { db } from '../db/index.js';
|
|
8
|
+
import { getProvisioner } from '../provisioner/index.js';
|
|
9
|
+
import { getUserUsage, PLAN_LIMITS } from './planLimits.js';
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
checkIntervalMs: 15 * 60 * 1000, // 15 minutes
|
|
13
|
+
warningThresholdPercent: 80,
|
|
14
|
+
};
|
|
15
|
+
export class ComputeEnforcementService {
|
|
16
|
+
config;
|
|
17
|
+
checkTimer = null;
|
|
18
|
+
isRunning = false;
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Start the enforcement service
|
|
24
|
+
*/
|
|
25
|
+
start() {
|
|
26
|
+
if (!this.config.enabled) {
|
|
27
|
+
console.log('[compute-enforcement] Service disabled');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (this.isRunning) {
|
|
31
|
+
console.warn('[compute-enforcement] Service already running');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.isRunning = true;
|
|
35
|
+
console.log(`[compute-enforcement] Started (checking every ${this.config.checkIntervalMs / 1000}s)`);
|
|
36
|
+
// Run immediately on start
|
|
37
|
+
this.runEnforcement().catch((err) => {
|
|
38
|
+
console.error('[compute-enforcement] Initial run failed:', err);
|
|
39
|
+
});
|
|
40
|
+
// Then run periodically
|
|
41
|
+
this.checkTimer = setInterval(() => {
|
|
42
|
+
this.runEnforcement().catch((err) => {
|
|
43
|
+
console.error('[compute-enforcement] Periodic run failed:', err);
|
|
44
|
+
});
|
|
45
|
+
}, this.config.checkIntervalMs);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop the enforcement service
|
|
49
|
+
*/
|
|
50
|
+
stop() {
|
|
51
|
+
if (this.checkTimer) {
|
|
52
|
+
clearInterval(this.checkTimer);
|
|
53
|
+
this.checkTimer = null;
|
|
54
|
+
}
|
|
55
|
+
this.isRunning = false;
|
|
56
|
+
console.log('[compute-enforcement] Stopped');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run enforcement check for all free tier users
|
|
60
|
+
*/
|
|
61
|
+
async runEnforcement() {
|
|
62
|
+
const results = [];
|
|
63
|
+
try {
|
|
64
|
+
// Get all users on free tier
|
|
65
|
+
const freeUsers = await db.users.findByPlan('free');
|
|
66
|
+
console.log(`[compute-enforcement] Checking ${freeUsers.length} free tier users`);
|
|
67
|
+
for (const user of freeUsers) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.enforceUserLimits(user.id);
|
|
70
|
+
results.push(result);
|
|
71
|
+
if (result.action !== 'none') {
|
|
72
|
+
console.log(`[compute-enforcement] User ${user.id.substring(0, 8)}: ${result.action} ` +
|
|
73
|
+
`(${result.computeHoursUsed.toFixed(2)}/${result.computeHoursLimit}h)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`[compute-enforcement] Error for user ${user.id}:`, err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const stopped = results.filter((r) => r.action === 'stopped').length;
|
|
81
|
+
const warned = results.filter((r) => r.action === 'warning').length;
|
|
82
|
+
if (stopped > 0 || warned > 0) {
|
|
83
|
+
console.log(`[compute-enforcement] Summary: ${stopped} stopped, ${warned} warned, ` +
|
|
84
|
+
`${results.length - stopped - warned} ok`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error('[compute-enforcement] Failed to run enforcement:', err);
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Enforce limits for a specific user
|
|
94
|
+
*/
|
|
95
|
+
async enforceUserLimits(userId) {
|
|
96
|
+
const user = await db.users.findById(userId);
|
|
97
|
+
const plan = user?.plan || 'free';
|
|
98
|
+
const limits = PLAN_LIMITS[plan];
|
|
99
|
+
const usage = await getUserUsage(userId);
|
|
100
|
+
const result = {
|
|
101
|
+
userId,
|
|
102
|
+
plan,
|
|
103
|
+
computeHoursUsed: usage.computeHoursThisMonth,
|
|
104
|
+
computeHoursLimit: limits.maxComputeHoursPerMonth,
|
|
105
|
+
action: 'none',
|
|
106
|
+
workspacesStopped: [],
|
|
107
|
+
};
|
|
108
|
+
// Skip if user has unlimited compute (paid plans may have high limits)
|
|
109
|
+
if (limits.maxComputeHoursPerMonth === Infinity) {
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
// Check if user has exceeded limit
|
|
113
|
+
if (usage.computeHoursThisMonth >= limits.maxComputeHoursPerMonth) {
|
|
114
|
+
// Stop all running workspaces
|
|
115
|
+
const workspaces = await db.workspaces.findByUserId(userId);
|
|
116
|
+
const runningWorkspaces = workspaces.filter((w) => w.status === 'running');
|
|
117
|
+
if (runningWorkspaces.length > 0) {
|
|
118
|
+
const provisioner = getProvisioner();
|
|
119
|
+
for (const workspace of runningWorkspaces) {
|
|
120
|
+
try {
|
|
121
|
+
await provisioner.stop(workspace.id);
|
|
122
|
+
result.workspacesStopped.push(workspace.id);
|
|
123
|
+
console.log(`[compute-enforcement] Stopped workspace ${workspace.id.substring(0, 8)} ` +
|
|
124
|
+
`for user ${userId.substring(0, 8)} (limit exceeded)`);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(`[compute-enforcement] Failed to stop workspace ${workspace.id}:`, err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
result.action = 'stopped';
|
|
131
|
+
// TODO: Send notification email to user
|
|
132
|
+
// await sendLimitReachedEmail(userId, usage.computeHoursThisMonth, limits.maxComputeHoursPerMonth);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Check if approaching limit (warning)
|
|
137
|
+
const usagePercent = (usage.computeHoursThisMonth / limits.maxComputeHoursPerMonth) * 100;
|
|
138
|
+
if (usagePercent >= this.config.warningThresholdPercent) {
|
|
139
|
+
result.action = 'warning';
|
|
140
|
+
// TODO: Send warning email to user (once per day)
|
|
141
|
+
// await sendLimitWarningEmail(userId, usage.computeHoursThisMonth, limits.maxComputeHoursPerMonth);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Manually trigger enforcement for a specific user
|
|
148
|
+
*/
|
|
149
|
+
async enforceUser(userId) {
|
|
150
|
+
return this.enforceUserLimits(userId);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get service status
|
|
154
|
+
*/
|
|
155
|
+
getStatus() {
|
|
156
|
+
return {
|
|
157
|
+
enabled: this.config.enabled,
|
|
158
|
+
isRunning: this.isRunning,
|
|
159
|
+
checkIntervalMs: this.config.checkIntervalMs,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Singleton instance
|
|
164
|
+
let _computeEnforcement = null;
|
|
165
|
+
export function getComputeEnforcementService() {
|
|
166
|
+
if (!_computeEnforcement) {
|
|
167
|
+
_computeEnforcement = new ComputeEnforcementService();
|
|
168
|
+
}
|
|
169
|
+
return _computeEnforcement;
|
|
170
|
+
}
|
|
171
|
+
export function createComputeEnforcementService(config = {}) {
|
|
172
|
+
_computeEnforcement = new ComputeEnforcementService(config);
|
|
173
|
+
return _computeEnforcement;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=compute-enforcement.js.map
|
|
@@ -9,4 +9,6 @@ export { CapacityManager, CapacityManagerConfig, WorkspaceCapacity, PlacementRec
|
|
|
9
9
|
export { ScalingOrchestrator, OrchestratorConfig, ScalingEvent, getScalingOrchestrator, createScalingOrchestrator, } from './scaling-orchestrator.js';
|
|
10
10
|
export { spawnCIFixAgent, notifyAgentOfCIFailure, completeFixAttempt, getFailureHistory, getPRFailureHistory, } from './ci-agent-spawner.js';
|
|
11
11
|
export { handleMention, handleIssueAssignment, getPendingMentions, getPendingIssueAssignments, processPendingMentions, processPendingIssueAssignments, KNOWN_AGENTS, isKnownAgent, } from './mention-handler.js';
|
|
12
|
+
export { ComputeEnforcementService, ComputeEnforcementConfig, EnforcementResult, getComputeEnforcementService, createComputeEnforcementService, } from './compute-enforcement.js';
|
|
13
|
+
export { IntroExpirationService, IntroExpirationConfig, IntroStatus, ExpirationResult as IntroExpirationResult, INTRO_PERIOD_DAYS, getIntroStatus, getIntroExpirationService, startIntroExpirationService, stopIntroExpirationService, } from './intro-expiration.js';
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -12,4 +12,8 @@ export { ScalingOrchestrator, getScalingOrchestrator, createScalingOrchestrator,
|
|
|
12
12
|
export { spawnCIFixAgent, notifyAgentOfCIFailure, completeFixAttempt, getFailureHistory, getPRFailureHistory, } from './ci-agent-spawner.js';
|
|
13
13
|
// Issue and mention handling
|
|
14
14
|
export { handleMention, handleIssueAssignment, getPendingMentions, getPendingIssueAssignments, processPendingMentions, processPendingIssueAssignments, KNOWN_AGENTS, isKnownAgent, } from './mention-handler.js';
|
|
15
|
+
// Compute enforcement (free tier limits)
|
|
16
|
+
export { ComputeEnforcementService, getComputeEnforcementService, createComputeEnforcementService, } from './compute-enforcement.js';
|
|
17
|
+
// Intro expiration (auto-resize after free tier intro period)
|
|
18
|
+
export { IntroExpirationService, INTRO_PERIOD_DAYS, getIntroStatus, getIntroExpirationService, startIntroExpirationService, stopIntroExpirationService, } from './intro-expiration.js';
|
|
15
19
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intro Expiration Service
|
|
3
|
+
*
|
|
4
|
+
* Handles auto-resize of workspaces when the free tier introductory period expires.
|
|
5
|
+
* Free users get Pro-level resources (2 CPU / 4GB) for the first 14 days,
|
|
6
|
+
* then get automatically downsized to standard free tier (1 CPU / 2GB).
|
|
7
|
+
*/
|
|
8
|
+
export declare const INTRO_PERIOD_DAYS = 14;
|
|
9
|
+
export interface IntroExpirationConfig {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
checkIntervalMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface IntroStatus {
|
|
14
|
+
isIntroPeriod: boolean;
|
|
15
|
+
daysRemaining: number;
|
|
16
|
+
introPeriodDays: number;
|
|
17
|
+
expiresAt: Date | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ExpirationResult {
|
|
20
|
+
userId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
workspaceName: string;
|
|
23
|
+
action: 'resized' | 'skipped' | 'error';
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get intro period status for a user
|
|
28
|
+
*/
|
|
29
|
+
export declare function getIntroStatus(userCreatedAt: Date | string | null, plan: string): IntroStatus;
|
|
30
|
+
export declare class IntroExpirationService {
|
|
31
|
+
private config;
|
|
32
|
+
private checkTimer;
|
|
33
|
+
private isRunning;
|
|
34
|
+
constructor(config?: Partial<IntroExpirationConfig>);
|
|
35
|
+
/**
|
|
36
|
+
* Start the expiration service
|
|
37
|
+
*/
|
|
38
|
+
start(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Stop the expiration service
|
|
41
|
+
*/
|
|
42
|
+
stop(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Run expiration check for all free tier users with expired intro periods
|
|
45
|
+
*/
|
|
46
|
+
runExpirationCheck(): Promise<ExpirationResult[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Check and resize workspaces for a user whose intro period has expired
|
|
49
|
+
*/
|
|
50
|
+
private checkAndResizeUserWorkspaces;
|
|
51
|
+
}
|
|
52
|
+
export declare function getIntroExpirationService(): IntroExpirationService;
|
|
53
|
+
export declare function startIntroExpirationService(): void;
|
|
54
|
+
export declare function stopIntroExpirationService(): void;
|
|
55
|
+
//# sourceMappingURL=intro-expiration.d.ts.map
|