rivet-design 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/config/evaluateFlags.d.ts +7 -0
  2. package/dist/config/evaluateFlags.d.ts.map +1 -0
  3. package/dist/config/evaluateFlags.js +27 -0
  4. package/dist/config/evaluateFlags.js.map +1 -0
  5. package/dist/config/flags.d.ts +25 -5
  6. package/dist/config/flags.d.ts.map +1 -1
  7. package/dist/config/flags.js +22 -18
  8. package/dist/config/flags.js.map +1 -1
  9. package/dist/demo/sessionRuntime.d.ts +2 -0
  10. package/dist/demo/sessionRuntime.d.ts.map +1 -0
  11. package/dist/demo/sessionRuntime.js +48 -0
  12. package/dist/demo/sessionRuntime.js.map +1 -0
  13. package/dist/hosted-demo.d.ts +2 -0
  14. package/dist/hosted-demo.d.ts.map +1 -0
  15. package/dist/hosted-demo.js +80 -0
  16. package/dist/hosted-demo.js.map +1 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +110 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp/server.d.ts.map +1 -1
  22. package/dist/mcp/server.js +35 -28
  23. package/dist/mcp/server.js.map +1 -1
  24. package/dist/proxy-middleware/proxy-config.js +1 -1
  25. package/dist/routes/demo.d.ts +33 -0
  26. package/dist/routes/demo.d.ts.map +1 -0
  27. package/dist/routes/demo.js +180 -0
  28. package/dist/routes/demo.js.map +1 -0
  29. package/dist/routes/git.d.ts +3 -1
  30. package/dist/routes/git.d.ts.map +1 -1
  31. package/dist/routes/git.js +15 -1
  32. package/dist/routes/git.js.map +1 -1
  33. package/dist/routes/modifications.d.ts +7 -0
  34. package/dist/routes/modifications.d.ts.map +1 -1
  35. package/dist/routes/modifications.js +81 -4
  36. package/dist/routes/modifications.js.map +1 -1
  37. package/dist/routes/static.d.ts.map +1 -1
  38. package/dist/routes/static.js +17 -2
  39. package/dist/routes/static.js.map +1 -1
  40. package/dist/server.d.ts +20 -1
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +530 -35
  43. package/dist/server.js.map +1 -1
  44. package/dist/services/AuthService.d.ts +1 -1
  45. package/dist/services/AuthService.js +1 -1
  46. package/dist/services/CSSTokenWriter.js +1 -1
  47. package/dist/services/CommentVariationService.js +1 -1
  48. package/dist/services/ConfigManager.d.ts.map +1 -1
  49. package/dist/services/ConfigManager.js +13 -0
  50. package/dist/services/ConfigManager.js.map +1 -1
  51. package/dist/services/FeatureFlagService.d.ts +18 -0
  52. package/dist/services/FeatureFlagService.d.ts.map +1 -0
  53. package/dist/services/FeatureFlagService.js +62 -0
  54. package/dist/services/FeatureFlagService.js.map +1 -0
  55. package/dist/services/HostedDemoAuthSessionService.d.ts +42 -0
  56. package/dist/services/HostedDemoAuthSessionService.d.ts.map +1 -0
  57. package/dist/services/HostedDemoAuthSessionService.js +179 -0
  58. package/dist/services/HostedDemoAuthSessionService.js.map +1 -0
  59. package/dist/services/HostedDemoAuthSessionStore.d.ts +43 -0
  60. package/dist/services/HostedDemoAuthSessionStore.d.ts.map +1 -0
  61. package/dist/services/HostedDemoAuthSessionStore.js +90 -0
  62. package/dist/services/HostedDemoAuthSessionStore.js.map +1 -0
  63. package/dist/services/HostedDemoSessionService.d.ts +91 -0
  64. package/dist/services/HostedDemoSessionService.d.ts.map +1 -0
  65. package/dist/services/HostedDemoSessionService.js +568 -0
  66. package/dist/services/HostedDemoSessionService.js.map +1 -0
  67. package/dist/services/HostedDemoSessionStore.d.ts +49 -0
  68. package/dist/services/HostedDemoSessionStore.d.ts.map +1 -0
  69. package/dist/services/HostedDemoSessionStore.js +90 -0
  70. package/dist/services/HostedDemoSessionStore.js.map +1 -0
  71. package/dist/services/ProjectDetectionService.d.ts +2 -2
  72. package/dist/services/ProjectDetectionService.d.ts.map +1 -1
  73. package/dist/services/ProjectDetectionService.js +26 -9
  74. package/dist/services/ProjectDetectionService.js.map +1 -1
  75. package/dist/services/RequestAuthContext.d.ts +8 -0
  76. package/dist/services/RequestAuthContext.d.ts.map +1 -0
  77. package/dist/services/RequestAuthContext.js +14 -0
  78. package/dist/services/RequestAuthContext.js.map +1 -0
  79. package/dist/services/SessionBridgeService.d.ts +3 -2
  80. package/dist/services/SessionBridgeService.d.ts.map +1 -1
  81. package/dist/services/SessionBridgeService.js +6 -8
  82. package/dist/services/SessionBridgeService.js.map +1 -1
  83. package/dist/services/TailwindConfigWriter.js +1 -1
  84. package/dist/services/TelemetryService.d.ts +95 -0
  85. package/dist/services/TelemetryService.d.ts.map +1 -1
  86. package/dist/services/TelemetryService.js +198 -0
  87. package/dist/services/TelemetryService.js.map +1 -1
  88. package/dist/services/accessTokenRefresh.d.ts +36 -0
  89. package/dist/services/accessTokenRefresh.d.ts.map +1 -0
  90. package/dist/services/accessTokenRefresh.js +102 -0
  91. package/dist/services/accessTokenRefresh.js.map +1 -0
  92. package/dist/services/agent/AgentCore.d.ts +1 -1
  93. package/dist/services/agent/AgentCore.js +2 -2
  94. package/dist/services/agent/AgentCore.js.map +1 -1
  95. package/dist/services/agent/AgentModService.d.ts +1 -1
  96. package/dist/services/agent/AgentModService.js +1 -1
  97. package/dist/services/hostedDemoSessionAuthRefresh.d.ts +18 -0
  98. package/dist/services/hostedDemoSessionAuthRefresh.d.ts.map +1 -0
  99. package/dist/services/hostedDemoSessionAuthRefresh.js +39 -0
  100. package/dist/services/hostedDemoSessionAuthRefresh.js.map +1 -0
  101. package/dist/utils/shouldRecordHostedDemoSessionAction.d.ts +8 -0
  102. package/dist/utils/shouldRecordHostedDemoSessionAction.d.ts.map +1 -0
  103. package/dist/utils/shouldRecordHostedDemoSessionAction.js +27 -0
  104. package/dist/utils/shouldRecordHostedDemoSessionAction.js.map +1 -0
  105. package/dist/utils/skills/claude-skill.d.ts +2 -2
  106. package/dist/utils/skills/claude-skill.d.ts.map +1 -1
  107. package/dist/utils/skills/claude-skill.js +29 -10
  108. package/dist/utils/skills/claude-skill.js.map +1 -1
  109. package/dist/utils/skills/cursor-rules.d.ts +2 -2
  110. package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
  111. package/dist/utils/skills/cursor-rules.js +12 -8
  112. package/dist/utils/skills/cursor-rules.js.map +1 -1
  113. package/package.json +4 -2
  114. package/src/ui/dist/assets/logo.png +0 -0
  115. package/src/ui/dist/assets/main-BxL1kNtz.css +1 -0
  116. package/src/ui/dist/assets/{main-Dnm69Obb.js → main-BxYkrTpy.js} +138 -134
  117. package/src/ui/dist/assets/rivet.svg +3 -0
  118. package/src/ui/dist/fonts/Goldman-Bold.woff2 +0 -0
  119. package/src/ui/dist/fonts/Goldman-Regular.woff2 +0 -0
  120. package/src/ui/dist/index.html +3 -3
  121. package/src/ui/dist/assets/main-BsJYpJMo.css +0 -1
package/dist/server.js CHANGED
@@ -54,8 +54,18 @@ const proxy_config_1 = require("./proxy-middleware/proxy-config");
54
54
  const StaticFileService_1 = require("./services/StaticFileService");
55
55
  const static_1 = require("./routes/static");
56
56
  const ConfigManager_1 = require("./services/ConfigManager");
57
- const flags_1 = require("./config/flags");
57
+ const evaluateFlags_1 = require("./config/evaluateFlags");
58
+ const FeatureFlagService_1 = require("./services/FeatureFlagService");
58
59
  const mcp_1 = require("./routes/mcp");
60
+ const demo_1 = require("./routes/demo");
61
+ const HostedDemoSessionService_1 = require("./services/HostedDemoSessionService");
62
+ const HostedDemoSessionStore_1 = require("./services/HostedDemoSessionStore");
63
+ const http_proxy_middleware_1 = require("http-proxy-middleware");
64
+ const HostedDemoAuthSessionService_1 = require("./services/HostedDemoAuthSessionService");
65
+ const HostedDemoAuthSessionStore_1 = require("./services/HostedDemoAuthSessionStore");
66
+ const shouldRecordHostedDemoSessionAction_1 = require("./utils/shouldRecordHostedDemoSessionAction");
67
+ const hostedDemoSessionAuthRefresh_1 = require("./services/hostedDemoSessionAuthRefresh");
68
+ const RequestAuthContext_1 = require("./services/RequestAuthContext");
59
69
  const log = (0, index_core_1.createLogger)('RivetServer');
60
70
  /**
61
71
  * Resolve UI dist path for both development and packaged (desktop app) environments
@@ -85,14 +95,17 @@ exports.DIST_UI_PATH = resolveUiPath();
85
95
  const startServer = async (options) => {
86
96
  const { userPort, userHost, telemetry, projectPath, framework, staticEntry, styleFramework, tailwindConfig, sessionBridge, mcpEditor, skipProcessHandlers, } = options;
87
97
  const userPortWithFallback = userPort ?? _1.DEFAULT_USER_PORT;
88
- const userHostWithFallback = userHost ?? '127.0.0.1';
98
+ const userHostWithFallback = userHost ?? 'localhost';
99
+ const isDemoModeEnabled = options.demoMode?.enabled ?? false;
100
+ const requiresDemoAuth = options.demoMode?.requireSignedInUsers ?? true;
101
+ const isGitEnabled = options.isGitEnabled ?? !isDemoModeEnabled;
89
102
  if (process.env.SENTRY_DSN) {
90
103
  Sentry.init({
91
104
  dsn: process.env.SENTRY_DSN,
92
105
  environment: process.env.NODE_ENV ?? 'development',
93
106
  });
94
107
  }
95
- const sessionService = new index_core_1.SessionService(projectPath, true);
108
+ const sessionService = new index_core_1.SessionService(projectPath, isGitEnabled);
96
109
  // Initialize git session
97
110
  try {
98
111
  await sessionService.initializeSession();
@@ -111,12 +124,14 @@ const startServer = async (options) => {
111
124
  if (!skipProcessHandlers) {
112
125
  const shutdownAndExit = async (code) => {
113
126
  log.info('Server shutting down, cleaning up...');
114
- if (telemetry) {
115
- try {
116
- await telemetry.shutdown();
117
- }
118
- catch (error) {
119
- log.error('Failed to cleanup telemetry:', error);
127
+ const results = await Promise.allSettled([
128
+ telemetry?.shutdown(),
129
+ (0, FeatureFlagService_1.getFeatureFlagService)().shutdown(),
130
+ ]);
131
+ const labels = ['telemetry', 'feature flags'];
132
+ for (let i = 0; i < results.length; i++) {
133
+ if (results[i].status === 'rejected') {
134
+ log.error(`Failed to cleanup ${labels[i]}:`, results[i].reason);
120
135
  }
121
136
  }
122
137
  process.exit(code);
@@ -136,24 +151,43 @@ const startServer = async (options) => {
136
151
  }
137
152
  const listenPort = options.rivetPort ?? _1.DEFAULT_PORT;
138
153
  const app = (0, express_1.default)();
154
+ const corsOrigins = new Set([
155
+ `http://localhost:${listenPort}`,
156
+ `http://127.0.0.1:${listenPort}`,
157
+ ]);
158
+ const rawCorsOrigins = process.env.RIVET_CORS_ORIGINS;
159
+ if (rawCorsOrigins) {
160
+ rawCorsOrigins
161
+ .split(',')
162
+ .map((origin) => origin.trim())
163
+ .filter((origin) => origin.length > 0)
164
+ .forEach((origin) => corsOrigins.add(origin));
165
+ }
166
+ if (isDemoModeEnabled) {
167
+ corsOrigins.add('https://rivet.design');
168
+ corsOrigins.add('https://demo.rivet.design');
169
+ }
139
170
  // CORS configuration for local development
140
171
  app.use((0, cors_1.default)({
141
- origin: [
142
- `http://localhost:${listenPort}`,
143
- `http://127.0.0.1:${listenPort}`,
144
- ],
172
+ origin: [...corsOrigins],
145
173
  credentials: true,
146
174
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
147
175
  allowedHeaders: ['Content-Type', 'Authorization'],
148
176
  }));
149
- app.use(express_1.default.json({ limit: '50mb' }));
177
+ const jsonBodyParser = express_1.default.json({ limit: '50mb' });
178
+ app.use((req, res, next) => {
179
+ if (req.originalUrl.startsWith('/try/')) {
180
+ next();
181
+ return;
182
+ }
183
+ jsonBodyParser(req, res, next);
184
+ });
150
185
  // Request logging middleware
151
186
  app.use((req, res, next) => {
152
187
  log.debug(`${req.method} ${req.originalUrl}`);
153
188
  next();
154
189
  });
155
- // Serve Rivet UI at /rivet
156
- app.use('/rivet', express_1.default.static(exports.DIST_UI_PATH, {
190
+ const uiStaticOptions = {
157
191
  setHeaders: (res) => {
158
192
  // Prevent aggressive caching of the UI bundle during development
159
193
  if (process.env.NODE_ENV !== 'production') {
@@ -162,21 +196,376 @@ const startServer = async (options) => {
162
196
  res.setHeader('Expires', '0');
163
197
  }
164
198
  },
165
- }));
199
+ };
200
+ // Serve hashed UI assets from root so both / and /rivet entrypoints can load them.
201
+ app.use('/assets', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'assets'), uiStaticOptions));
202
+ // Serve public font files used by auth and hosted demo UI.
203
+ app.use('/fonts', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'fonts'), uiStaticOptions));
204
+ // Serve favicon at root for hosted/static deployments.
205
+ app.get('/favicon.ico', (_req, res) => {
206
+ res.sendFile('favicon.ico', { root: exports.DIST_UI_PATH });
207
+ });
208
+ // Serve Rivet UI at /rivet for non-static modes.
209
+ if (framework !== 'static') {
210
+ app.use('/rivet', express_1.default.static(exports.DIST_UI_PATH, uiStaticOptions));
211
+ }
166
212
  log.info(`Serving UI assets from: ${exports.DIST_UI_PATH}`);
167
- // Static mode - redirect root to /rivet (no dev server to proxy to)
213
+ // Static mode - use root entrypoint for Rivet UI (no dev server to proxy to)
168
214
  if (framework === 'static') {
169
215
  app.get('/', (req, res) => {
170
- log.debug(`${framework} mode - redirecting to /rivet`);
171
- res.redirect('/rivet');
216
+ log.debug(`${framework} mode - serving Rivet at root`);
217
+ res.sendFile('index.html', { root: exports.DIST_UI_PATH });
172
218
  });
173
219
  }
174
220
  // For web frameworks, root route falls through to catch-all proxy (no special handling needed)
221
+ const getBearerTokenFromHeader = (authorizationHeader) => {
222
+ if (typeof authorizationHeader !== 'string') {
223
+ return undefined;
224
+ }
225
+ if (!authorizationHeader.startsWith('Bearer ')) {
226
+ return undefined;
227
+ }
228
+ const token = authorizationHeader.slice('Bearer '.length).trim();
229
+ return token.length > 0 ? token : undefined;
230
+ };
231
+ /**
232
+ * @effect Hydrates request auth context from Authorization header for all modes.
233
+ * @deps Runs per request and reads req.headers.authorization.
234
+ */
235
+ app.use((req, _res, next) => {
236
+ const headerToken = getBearerTokenFromHeader(req.headers.authorization);
237
+ if (!headerToken) {
238
+ next();
239
+ return;
240
+ }
241
+ (0, RequestAuthContext_1.runWithRequestAuthContext)({ token: headerToken }, () => {
242
+ next();
243
+ });
244
+ });
245
+ const demoAuthSessionService = isDemoModeEnabled
246
+ ? new HostedDemoAuthSessionService_1.HostedDemoAuthSessionService({
247
+ store: options.demoMode?.authRedisUrl
248
+ ? new HostedDemoAuthSessionStore_1.RedisHostedDemoAuthSessionStore({
249
+ redisUrl: options.demoMode.authRedisUrl,
250
+ keyPrefix: options.demoMode.authRedisKeyPrefix ??
251
+ 'rivet:hosted-demo:auth-session',
252
+ })
253
+ : undefined,
254
+ })
255
+ : null;
256
+ if (demoAuthSessionService) {
257
+ await demoAuthSessionService.initialize();
258
+ }
259
+ if (demoAuthSessionService) {
260
+ app.use(async (req, res, next) => {
261
+ try {
262
+ const sessionId = demoAuthSessionService.getOrCreateSessionId(req, res);
263
+ const auth = await (0, hostedDemoSessionAuthRefresh_1.resolveHostedDemoSessionAuthWithRefresh)({
264
+ demoAuthSessionService,
265
+ sessionId,
266
+ res,
267
+ getProxyUrl: () => (0, ConfigManager_1.getConfigManager)().getProxyUrl(),
268
+ });
269
+ const headerToken = getBearerTokenFromHeader(req.headers.authorization);
270
+ res.locals.demoAuthSessionId = sessionId;
271
+ (0, RequestAuthContext_1.runWithRequestAuthContext)({
272
+ token: auth?.token ?? headerToken,
273
+ refreshToken: auth?.refreshToken,
274
+ email: auth?.email,
275
+ }, () => {
276
+ next();
277
+ });
278
+ }
279
+ catch (error) {
280
+ next(error);
281
+ }
282
+ });
283
+ }
284
+ const isDemoRequestAuthenticated = (req) => {
285
+ if (!isDemoModeEnabled || !requiresDemoAuth) {
286
+ return true;
287
+ }
288
+ return Boolean(demoAuthSessionService?.getAuthForRequest(req));
289
+ };
290
+ const getDemoRequestOwnerUserId = (req) => {
291
+ if (!requiresDemoAuth) {
292
+ return null;
293
+ }
294
+ const cookieAuthEmail = demoAuthSessionService
295
+ ?.getAuthForRequest(req)
296
+ ?.email?.trim()
297
+ .toLowerCase();
298
+ if (cookieAuthEmail) {
299
+ return cookieAuthEmail;
300
+ }
301
+ const email = (0, ConfigManager_1.getConfigManager)().getEmail();
302
+ if (!email) {
303
+ return null;
304
+ }
305
+ return email.trim().toLowerCase();
306
+ };
307
+ const isDemoAuthExemptApiPath = (pathName) => {
308
+ if (pathName === '/config' || pathName === '/health' || pathName === '/auth/store') {
309
+ return true;
310
+ }
311
+ if (pathName === '/demo/session' || pathName.startsWith('/demo/session/')) {
312
+ return true;
313
+ }
314
+ return false;
315
+ };
316
+ const isHostedRootApiPath = (pathName) => {
317
+ if (pathName === '/config' ||
318
+ pathName === '/health' ||
319
+ pathName === '/auth/store' ||
320
+ pathName === '/support') {
321
+ return true;
322
+ }
323
+ if (pathName === '/demo/session' || pathName.startsWith('/demo/session/')) {
324
+ return true;
325
+ }
326
+ return false;
327
+ };
328
+ let demoSessionService = null;
329
+ if (isDemoModeEnabled && options.demoMode?.allowSessionProvisioning !== false) {
330
+ const sessionMetadataRedisUrl = options.demoMode?.sessionMetadataRedisUrl ??
331
+ options.demoMode?.authRedisUrl;
332
+ const sessionMetadataStore = sessionMetadataRedisUrl
333
+ ? new HostedDemoSessionStore_1.RedisHostedDemoSessionStore({
334
+ redisUrl: sessionMetadataRedisUrl,
335
+ keyPrefix: options.demoMode?.sessionMetadataRedisKeyPrefix ??
336
+ 'rivet:hosted-demo:demo-session',
337
+ })
338
+ : undefined;
339
+ demoSessionService = new HostedDemoSessionService_1.HostedDemoSessionService({
340
+ repoPath: projectPath,
341
+ telemetry,
342
+ templateProjectPath: options.demoMode?.templateProjectPath ?? 'examples/microsoft-paint',
343
+ worktreesRootPath: options.demoMode?.worktreesRootPath,
344
+ sessionTtlMs: options.demoMode?.sessionTtlMs,
345
+ sessionIdleTtlMs: options.demoMode?.sessionIdleTtlMs,
346
+ maxActiveSessions: options.demoMode?.maxActiveSessions,
347
+ baseRivetPort: options.demoMode?.baseRivetPort,
348
+ publicBaseUrl: options.demoMode?.publicBaseUrl,
349
+ sessionMetadataStore,
350
+ });
351
+ await demoSessionService.initialize();
352
+ }
353
+ if (isDemoModeEnabled && requiresDemoAuth) {
354
+ app.use('/api', (req, res, next) => {
355
+ if (isDemoAuthExemptApiPath(req.path)) {
356
+ next();
357
+ return;
358
+ }
359
+ if (!isDemoRequestAuthenticated(req)) {
360
+ res.status(401).json({ error: 'Authentication required' });
361
+ return;
362
+ }
363
+ next();
364
+ });
365
+ }
366
+ if (demoSessionService) {
367
+ const activeSessionApiProxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
368
+ changeOrigin: true,
369
+ pathRewrite: (pathName) => `/api${pathName}`,
370
+ on: {
371
+ proxyReq: (proxyReq, req) => {
372
+ (0, http_proxy_middleware_1.fixRequestBody)(proxyReq, req);
373
+ },
374
+ },
375
+ router: (req) => {
376
+ const activeDemoSessionId = demoAuthSessionService?.getActiveDemoSessionIdForRequest(req) ?? null;
377
+ if (!activeDemoSessionId) {
378
+ return undefined;
379
+ }
380
+ return demoSessionService.getSessionProxyTarget(activeDemoSessionId) ?? undefined;
381
+ },
382
+ });
383
+ app.use('/api', (req, res, next) => {
384
+ if (isHostedRootApiPath(req.path)) {
385
+ next();
386
+ return;
387
+ }
388
+ const activeDemoSessionId = demoAuthSessionService?.getActiveDemoSessionIdForRequest(req) ?? null;
389
+ if (!activeDemoSessionId) {
390
+ res.status(409).json({
391
+ error: 'No active demo session. Call /api/demo/session first.',
392
+ });
393
+ return;
394
+ }
395
+ const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(activeDemoSessionId);
396
+ const requestOwnerUserId = getDemoRequestOwnerUserId(req);
397
+ if (requiresDemoAuth &&
398
+ sessionOwnerUserId &&
399
+ requestOwnerUserId &&
400
+ sessionOwnerUserId !== requestOwnerUserId) {
401
+ res.status(403).json({ error: 'Session access denied' });
402
+ return;
403
+ }
404
+ const auth = demoAuthSessionService?.getAuthForRequest(req);
405
+ if (auth?.token) {
406
+ req.headers.authorization = `Bearer ${auth.token}`;
407
+ }
408
+ void (async () => {
409
+ try {
410
+ await demoSessionService.ensureRuntimeReady(activeDemoSessionId);
411
+ }
412
+ catch (error) {
413
+ const message = error instanceof Error ? error.message : '';
414
+ if (message === 'Session not found') {
415
+ demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
416
+ if (!res.headersSent) {
417
+ res.status(409).json({
418
+ error: 'No active demo session. Call /api/demo/session first.',
419
+ });
420
+ }
421
+ return;
422
+ }
423
+ log.warn('Hosted demo runtime rehydration failed for active session API', error);
424
+ if (!res.headersSent) {
425
+ res.status(503).json({
426
+ error: 'Demo session runtime unavailable',
427
+ });
428
+ }
429
+ return;
430
+ }
431
+ const target = demoSessionService.getSessionProxyTarget(activeDemoSessionId);
432
+ if (!target) {
433
+ demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
434
+ if (!res.headersSent) {
435
+ res.status(409).json({
436
+ error: 'No active demo session. Call /api/demo/session first.',
437
+ });
438
+ }
439
+ return;
440
+ }
441
+ if ((0, shouldRecordHostedDemoSessionAction_1.shouldRecordHostedDemoSessionAction)(req)) {
442
+ try {
443
+ await demoSessionService.recordSessionAction(activeDemoSessionId);
444
+ }
445
+ catch (error) {
446
+ log.warn('Failed to persist hosted demo session action timestamp', error);
447
+ }
448
+ }
449
+ activeSessionApiProxy(req, res, next);
450
+ })();
451
+ });
452
+ }
175
453
  // API routes for Rivet functionality
176
454
  app.use('/api', (0, components_1.createComponentRouter)(projectPath, telemetry));
177
455
  app.use('/api', (0, modifications_1.createModificationRouter)(projectPath, telemetry));
178
456
  app.use('/api', (0, design_1.createDesignRouter)(projectPath));
179
- app.use('/api', (0, git_1.createGitRouter)(sessionService, projectPath));
457
+ if (!isGitEnabled) {
458
+ log.info('Git routes disabled for this server instance');
459
+ }
460
+ app.use('/api', (0, git_1.createGitRouter)(sessionService, projectPath, { isGitEnabled }));
461
+ let trySessionProxy = null;
462
+ if (demoSessionService) {
463
+ app.use('/api', (0, demo_1.createDemoRouter)(demoSessionService, {
464
+ requireSignedInUsers: requiresDemoAuth,
465
+ getActiveDemoSessionIdForRequest: demoAuthSessionService
466
+ ? (req) => demoAuthSessionService.getActiveDemoSessionIdForRequest(req)
467
+ : undefined,
468
+ storeActiveDemoSessionIdForRequest: demoAuthSessionService
469
+ ? (req, res, demoSessionId) => demoAuthSessionService.storeActiveDemoSessionIdForRequest(req, res, demoSessionId)
470
+ : undefined,
471
+ clearActiveDemoSessionIdForRequest: demoAuthSessionService
472
+ ? (req, res) => demoAuthSessionService.clearActiveDemoSessionIdForRequest(req, res)
473
+ : undefined,
474
+ telemetry,
475
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
476
+ }));
477
+ const mountedTrySessionProxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
478
+ changeOrigin: true,
479
+ ws: true,
480
+ router: (req) => {
481
+ const sessionIdParam = req.params.sessionId;
482
+ const sessionId = Array.isArray(sessionIdParam)
483
+ ? sessionIdParam[0]
484
+ : sessionIdParam;
485
+ return demoSessionService.getSessionProxyTarget(sessionId) ?? undefined;
486
+ },
487
+ });
488
+ trySessionProxy = mountedTrySessionProxy;
489
+ // Browsers resolve default favicon relative to the document URL under /try/:id/,
490
+ // so requests hit this path instead of /. Serve the Rivet UI favicon here too.
491
+ app.get('/try/:sessionId/favicon.ico', (_req, res) => {
492
+ res.sendFile('favicon.ico', { root: exports.DIST_UI_PATH });
493
+ });
494
+ app.use('/try/:sessionId', (req, res, next) => {
495
+ if (!isDemoRequestAuthenticated(req)) {
496
+ if (req.accepts('html')) {
497
+ res.redirect('/');
498
+ return;
499
+ }
500
+ res.status(401).json({ error: 'Authentication required' });
501
+ return;
502
+ }
503
+ const sessionIdParam = req.params.sessionId;
504
+ const sessionId = Array.isArray(sessionIdParam)
505
+ ? sessionIdParam[0]
506
+ : sessionIdParam;
507
+ const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(sessionId);
508
+ const requestOwnerUserId = getDemoRequestOwnerUserId(req);
509
+ if (requiresDemoAuth &&
510
+ sessionOwnerUserId &&
511
+ requestOwnerUserId &&
512
+ sessionOwnerUserId !== requestOwnerUserId) {
513
+ res.status(403).json({ error: 'Session access denied' });
514
+ return;
515
+ }
516
+ const auth = demoAuthSessionService?.getAuthForRequest(req);
517
+ if (auth?.token) {
518
+ req.headers.authorization = `Bearer ${auth.token}`;
519
+ }
520
+ void (async () => {
521
+ try {
522
+ await demoSessionService.ensureRuntimeReady(sessionId);
523
+ }
524
+ catch (error) {
525
+ const message = error instanceof Error ? error.message : '';
526
+ if (message === 'Session not found') {
527
+ demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
528
+ telemetry?.trackTryoutProxySessionMissing({
529
+ demoSessionId: sessionId,
530
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
531
+ });
532
+ if (!res.headersSent) {
533
+ res.status(404).json({ error: 'Session not found' });
534
+ }
535
+ return;
536
+ }
537
+ log.warn('Hosted demo runtime rehydration failed for try session', error);
538
+ if (!res.headersSent) {
539
+ res.status(503).json({
540
+ error: 'Demo session runtime unavailable',
541
+ });
542
+ }
543
+ return;
544
+ }
545
+ const target = demoSessionService.getSessionProxyTarget(sessionId);
546
+ if (!target) {
547
+ demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
548
+ telemetry?.trackTryoutProxySessionMissing({
549
+ demoSessionId: sessionId,
550
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
551
+ });
552
+ if (!res.headersSent) {
553
+ res.status(404).json({ error: 'Session not found' });
554
+ }
555
+ return;
556
+ }
557
+ if ((0, shouldRecordHostedDemoSessionAction_1.shouldRecordHostedDemoSessionAction)(req)) {
558
+ try {
559
+ await demoSessionService.recordSessionAction(sessionId);
560
+ }
561
+ catch (error) {
562
+ log.warn('Failed to persist hosted demo session action timestamp', error);
563
+ }
564
+ }
565
+ mountedTrySessionProxy(req, res, next);
566
+ })();
567
+ });
568
+ }
180
569
  app.use('/api', (0, tokens_1.createTokenRouter)(projectPath, styleFramework, tailwindConfig));
181
570
  app.use('/api', (0, support_1.createSupportRouter)(telemetry));
182
571
  // Static routes (only for static projects)
@@ -197,28 +586,80 @@ const startServer = async (options) => {
197
586
  });
198
587
  });
199
588
  // Config endpoint
200
- app.get('/api/config', (req, res) => {
589
+ app.get('/api/config', async (req, res) => {
201
590
  const configManager = (0, ConfigManager_1.getConfigManager)();
591
+ const demoSessionId = res.locals.demoAuthSessionId;
592
+ const demoAuth = demoAuthSessionService?.getAuthForSessionId(demoSessionId ?? '');
593
+ const isAuthenticatedInDemoMode = Boolean(demoAuth?.token);
594
+ const isAuthenticated = demoAuthSessionService
595
+ ? isAuthenticatedInDemoMode
596
+ : configManager.isAuthenticated();
597
+ const email = demoAuthSessionService ? demoAuth?.email : configManager.getEmail();
598
+ const activeDemoSessionId = demoAuth?.activeDemoSessionId ?? null;
599
+ const activeSession = activeDemoSessionId
600
+ ? demoSessionService?.getSession(activeDemoSessionId)
601
+ : null;
602
+ const featureFlags = await (0, evaluateFlags_1.getFeatureFlags)();
202
603
  res.json({
203
- agentModeAvailable: configManager.isAuthenticated(),
204
- email: configManager.getEmail(),
604
+ agentModeAvailable: isAuthenticated,
605
+ email,
606
+ activeDemoSessionId,
205
607
  isMCPSession: sessionBridge?.isActive() ?? false,
206
608
  mcpEditor: mcpEditor ?? null,
207
- featureFlags: (0, flags_1.getFeatureFlags)(),
208
- projectPath,
609
+ featureFlags,
610
+ projectPath: activeSession?.projectPath ?? projectPath,
611
+ demoMode: isDemoModeEnabled,
209
612
  });
210
613
  });
211
- // Store auth credentials locally after browser-based OAuth completion.
212
- // The OAuth callback in the UI sends tokens to the proxy for validation,
213
- // but the local ConfigManager also needs the email and tokens so that
214
- // /api/config returns the email on subsequent page loads.
614
+ // Store auth credentials after browser-based OAuth completion.
615
+ // Hosted demo mode stores auth by browser session; non-demo keeps local config behavior.
215
616
  app.post('/api/auth/store', (req, res) => {
216
617
  const { email, token, refreshToken } = req.body ?? {};
217
618
  if (!email || !token) {
619
+ if (demoAuthSessionService) {
620
+ telemetry?.trackTryoutAuthStoreFailed({
621
+ reason: 'missing_email_or_token',
622
+ statusCode: 400,
623
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
624
+ });
625
+ }
218
626
  return res.status(400).json({ error: 'Missing email or token' });
219
627
  }
220
- const configManager = (0, ConfigManager_1.getConfigManager)();
221
- configManager.setAuth(token, email, refreshToken);
628
+ try {
629
+ if (demoAuthSessionService) {
630
+ const sessionId = res.locals.demoAuthSessionId;
631
+ const demoSessionId = sessionId
632
+ ? demoAuthSessionService.storeAuthForSessionId(sessionId, res, {
633
+ token,
634
+ email,
635
+ refreshToken,
636
+ })
637
+ : demoAuthSessionService.storeAuthForRequest(req, res, {
638
+ token,
639
+ email,
640
+ refreshToken,
641
+ });
642
+ telemetry?.trackTryoutAuthStoreCompleted({
643
+ demoSessionId,
644
+ hasRefreshToken: Boolean(refreshToken),
645
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
646
+ });
647
+ }
648
+ else {
649
+ const configManager = (0, ConfigManager_1.getConfigManager)();
650
+ configManager.setAuth(token, email, refreshToken);
651
+ }
652
+ }
653
+ catch (error) {
654
+ if (demoAuthSessionService) {
655
+ telemetry?.trackTryoutAuthStoreFailed({
656
+ reason: error instanceof Error ? error.message : 'auth_store_failed',
657
+ statusCode: 500,
658
+ deploymentEnv: process.env.NODE_ENV ?? 'development',
659
+ });
660
+ }
661
+ return res.status(500).json({ error: 'Failed to store auth credentials' });
662
+ }
222
663
  telemetry?.identifyUser();
223
664
  log.info(`Stored auth for ${email} via browser OAuth`);
224
665
  res.json({ success: true });
@@ -253,6 +694,12 @@ const startServer = async (options) => {
253
694
  }
254
695
  // Global error handler — catches errors passed via next(err) in route handlers
255
696
  app.use((err, req, res, _next) => {
697
+ const requestAborted = err.code === 'ECONNABORTED' ||
698
+ err.type === 'request.aborted';
699
+ if (requestAborted) {
700
+ log.debug('Request aborted by client');
701
+ return;
702
+ }
256
703
  log.error('Unhandled route error:', err);
257
704
  telemetry?.captureException(err, 'express_error_handler');
258
705
  if (!res.headersSent) {
@@ -279,8 +726,46 @@ const startServer = async (options) => {
279
726
  });
280
727
  // Handle WebSocket upgrade requests
281
728
  server.on('upgrade', (req, socket, head) => {
282
- const pathname = req.url;
729
+ const pathname = req.url?.split('?')[0] ?? '';
283
730
  log.debug(`WebSocket upgrade request for: ${pathname}`);
731
+ const trySessionMatch = /^\/try\/([^/]+)/.exec(pathname);
732
+ if (trySessionMatch && trySessionProxy?.upgrade && demoSessionService) {
733
+ const sessionId = trySessionMatch[1];
734
+ void (async () => {
735
+ try {
736
+ await demoSessionService.ensureRuntimeReady(sessionId);
737
+ }
738
+ catch {
739
+ socket.destroy();
740
+ return;
741
+ }
742
+ const target = demoSessionService.getSessionProxyTarget(sessionId);
743
+ if (!target) {
744
+ socket.destroy();
745
+ return;
746
+ }
747
+ if (requiresDemoAuth) {
748
+ const auth = demoAuthSessionService?.getAuthForRequest(req);
749
+ if (!auth?.token) {
750
+ log.debug('Rejecting unauthenticated hosted demo upgrade request');
751
+ socket.destroy();
752
+ return;
753
+ }
754
+ const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(sessionId);
755
+ const requestOwnerUserId = auth.email?.trim().toLowerCase() ?? null;
756
+ if (sessionOwnerUserId &&
757
+ requestOwnerUserId &&
758
+ sessionOwnerUserId !== requestOwnerUserId) {
759
+ log.debug('Rejecting non-owner hosted demo upgrade request');
760
+ socket.destroy();
761
+ return;
762
+ }
763
+ }
764
+ log.debug('Proxying upgrade to hosted demo session for HMR');
765
+ trySessionProxy.upgrade(req, socket, head);
766
+ })();
767
+ return;
768
+ }
284
769
  // Proxy to user's dev server for HMR
285
770
  if (framework !== 'static' && userDevProxy?.upgrade) {
286
771
  log.debug('Proxying upgrade to user dev server for HMR');
@@ -295,10 +780,20 @@ const startServer = async (options) => {
295
780
  return {
296
781
  close: () => new Promise((resolve, reject) => {
297
782
  httpServer.close((err) => {
298
- if (err)
783
+ if (err) {
299
784
  reject(err);
300
- else
785
+ return;
786
+ }
787
+ void (async () => {
788
+ if (demoSessionService) {
789
+ await demoSessionService.shutdown();
790
+ }
791
+ demoAuthSessionService?.shutdown();
792
+ if (telemetry) {
793
+ await telemetry.shutdown();
794
+ }
301
795
  resolve();
796
+ })();
302
797
  });
303
798
  // Destroy all active connections so close() resolves immediately
304
799
  for (const socket of activeSockets) {