veryfront 0.0.86 → 0.0.89

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 (163) hide show
  1. package/esm/_dnt.shims.d.ts +14 -14
  2. package/esm/_dnt.shims.d.ts.map +1 -1
  3. package/esm/deno.d.ts +0 -1
  4. package/esm/deno.js +8 -9
  5. package/esm/proxy/main.d.ts +2 -0
  6. package/esm/proxy/main.d.ts.map +1 -0
  7. package/esm/proxy/main.js +400 -0
  8. package/esm/src/cli/commands/init/config-generator.js +1 -1
  9. package/esm/src/cli/index/arg-parser.d.ts.map +1 -1
  10. package/esm/src/cli/index/arg-parser.js +1 -0
  11. package/esm/src/cli/index/command-router.d.ts.map +1 -1
  12. package/esm/src/cli/index/command-router.js +54 -39
  13. package/esm/src/cli/index/types.d.ts +4 -0
  14. package/esm/src/cli/index/types.d.ts.map +1 -1
  15. package/esm/src/cli/mcp/advanced-tools.d.ts +2 -2
  16. package/esm/src/cli/templates/manifest.d.ts +448 -448
  17. package/esm/src/cli/templates/manifest.js +480 -480
  18. package/esm/src/config/loader.d.ts.map +1 -1
  19. package/esm/src/config/loader.js +5 -4
  20. package/esm/src/html/utils.js +2 -2
  21. package/esm/src/modules/import-map/default-import-map.d.ts +1 -11
  22. package/esm/src/modules/import-map/default-import-map.d.ts.map +1 -1
  23. package/esm/src/modules/import-map/default-import-map.js +3 -20
  24. package/esm/src/modules/import-map/loader.d.ts.map +1 -1
  25. package/esm/src/modules/import-map/loader.js +7 -22
  26. package/esm/src/modules/import-map/resolver.d.ts.map +1 -1
  27. package/esm/src/modules/import-map/resolver.js +12 -8
  28. package/esm/src/modules/react-loader/component-loader.d.ts.map +1 -1
  29. package/esm/src/modules/react-loader/component-loader.js +2 -0
  30. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts +1 -6
  31. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  32. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +40 -32
  33. package/esm/src/modules/react-loader/ssr-module-loader/types.d.ts +2 -0
  34. package/esm/src/modules/react-loader/ssr-module-loader/types.d.ts.map +1 -1
  35. package/esm/src/modules/react-loader/types.d.ts +2 -0
  36. package/esm/src/modules/react-loader/types.d.ts.map +1 -1
  37. package/esm/src/modules/react-loader/unified-loader.d.ts.map +1 -1
  38. package/esm/src/modules/react-loader/unified-loader.js +7 -4
  39. package/esm/src/modules/server/module-batch-handler.d.ts +2 -0
  40. package/esm/src/modules/server/module-batch-handler.d.ts.map +1 -1
  41. package/esm/src/modules/server/module-batch-handler.js +3 -1
  42. package/esm/src/modules/server/module-server.d.ts +2 -0
  43. package/esm/src/modules/server/module-server.d.ts.map +1 -1
  44. package/esm/src/modules/server/module-server.js +4 -2
  45. package/esm/src/modules/server/ssr-import-rewriter.d.ts.map +1 -1
  46. package/esm/src/modules/server/ssr-import-rewriter.js +9 -17
  47. package/esm/src/platform/compat/path-helper.d.ts +7 -7
  48. package/esm/src/platform/compat/path-helper.d.ts.map +1 -1
  49. package/esm/src/react/compat/ssr-adapter/string-renderer.js +1 -1
  50. package/esm/src/react/components/Head.d.ts.map +1 -1
  51. package/esm/src/react/components/Head.js +6 -2
  52. package/esm/src/react/components/ai/agent-card.d.ts +1 -1
  53. package/esm/src/react/components/ai/agent-card.d.ts.map +1 -1
  54. package/esm/src/react/components/ai/chat/composition/api.d.ts +5 -4
  55. package/esm/src/react/components/ai/chat/composition/api.d.ts.map +1 -1
  56. package/esm/src/react/components/ai/chat/index.d.ts +7 -2
  57. package/esm/src/react/components/ai/chat/index.d.ts.map +1 -1
  58. package/esm/src/react/components/ai/message.d.ts +2 -2
  59. package/esm/src/react/components/ai/message.d.ts.map +1 -1
  60. package/esm/src/react/primitives/agent-primitives.d.ts +3 -3
  61. package/esm/src/react/primitives/agent-primitives.d.ts.map +1 -1
  62. package/esm/src/react/primitives/chat-container.d.ts +1 -1
  63. package/esm/src/react/primitives/chat-container.d.ts.map +1 -1
  64. package/esm/src/react/primitives/input-box.d.ts +3 -3
  65. package/esm/src/react/primitives/input-box.d.ts.map +1 -1
  66. package/esm/src/react/primitives/input-box.js +6 -2
  67. package/esm/src/react/primitives/message-list.d.ts +4 -4
  68. package/esm/src/react/primitives/message-list.d.ts.map +1 -1
  69. package/esm/src/react/primitives/tool-primitives.d.ts +3 -3
  70. package/esm/src/react/primitives/tool-primitives.d.ts.map +1 -1
  71. package/esm/src/rendering/component-handling.d.ts +2 -0
  72. package/esm/src/rendering/component-handling.d.ts.map +1 -1
  73. package/esm/src/rendering/component-handling.js +4 -2
  74. package/esm/src/rendering/layouts/layout-applicator.d.ts.map +1 -1
  75. package/esm/src/rendering/layouts/layout-applicator.js +2 -0
  76. package/esm/src/rendering/orchestrator/module-loader/index.d.ts +3 -0
  77. package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
  78. package/esm/src/rendering/orchestrator/module-loader/index.js +74 -19
  79. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  80. package/esm/src/rendering/orchestrator/pipeline.js +2 -0
  81. package/esm/src/rendering/rsc/server-renderer/tree-processor.d.ts.map +1 -1
  82. package/esm/src/rendering/rsc/server-renderer/tree-processor.js +3 -1
  83. package/esm/src/rendering/ssr-globals/context.d.ts +6 -1
  84. package/esm/src/rendering/ssr-globals/context.d.ts.map +1 -1
  85. package/esm/src/transforms/esm/http-bundler.d.ts +15 -4
  86. package/esm/src/transforms/esm/http-bundler.d.ts.map +1 -1
  87. package/esm/src/transforms/esm/http-bundler.js +105 -12
  88. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  89. package/esm/src/transforms/esm/http-cache.js +26 -27
  90. package/esm/src/transforms/esm/import-rewriter.d.ts.map +1 -1
  91. package/esm/src/transforms/esm/import-rewriter.js +3 -3
  92. package/esm/src/transforms/esm/package-registry.d.ts +15 -13
  93. package/esm/src/transforms/esm/package-registry.d.ts.map +1 -1
  94. package/esm/src/transforms/esm/package-registry.js +45 -32
  95. package/esm/src/transforms/esm/react-imports.d.ts.map +1 -1
  96. package/esm/src/transforms/esm/react-imports.js +3 -7
  97. package/esm/src/transforms/esm/types.d.ts +2 -0
  98. package/esm/src/transforms/esm/types.d.ts.map +1 -1
  99. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts +14 -0
  100. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts.map +1 -1
  101. package/esm/src/transforms/mdx/esm-module-loader/cache/index.js +67 -0
  102. package/esm/src/transforms/mdx/esm-module-loader/loader.d.ts.map +1 -1
  103. package/esm/src/transforms/mdx/esm-module-loader/loader.js +1 -1
  104. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts +1 -0
  105. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
  106. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +1 -0
  107. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts +4 -0
  108. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts.map +1 -1
  109. package/esm/src/transforms/pipeline/context.d.ts.map +1 -1
  110. package/esm/src/transforms/pipeline/context.js +7 -15
  111. package/esm/src/transforms/pipeline/stages/finalize.d.ts.map +1 -1
  112. package/esm/src/transforms/pipeline/stages/finalize.js +1 -1
  113. package/esm/src/utils/constants/cdn.d.ts.map +1 -1
  114. package/esm/src/utils/constants/cdn.js +22 -11
  115. package/esm/src/utils/hash-utils.d.ts +2 -0
  116. package/esm/src/utils/hash-utils.d.ts.map +1 -1
  117. package/esm/src/utils/hash-utils.js +4 -0
  118. package/package.json +1 -1
  119. package/src/deno.js +8 -9
  120. package/src/proxy/main.ts +471 -0
  121. package/src/src/cli/commands/init/config-generator.ts +1 -1
  122. package/src/src/cli/index/arg-parser.ts +1 -0
  123. package/src/src/cli/index/command-router.ts +57 -40
  124. package/src/src/cli/index/types.ts +5 -0
  125. package/src/src/cli/templates/manifest.js +480 -480
  126. package/src/src/config/loader.ts +5 -4
  127. package/src/src/html/utils.ts +2 -2
  128. package/src/src/modules/import-map/default-import-map.ts +3 -25
  129. package/src/src/modules/import-map/loader.ts +7 -23
  130. package/src/src/modules/import-map/resolver.ts +13 -8
  131. package/src/src/modules/react-loader/component-loader.ts +2 -0
  132. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +51 -37
  133. package/src/src/modules/react-loader/ssr-module-loader/types.ts +2 -0
  134. package/src/src/modules/react-loader/types.ts +2 -0
  135. package/src/src/modules/react-loader/unified-loader.ts +7 -4
  136. package/src/src/modules/server/module-batch-handler.ts +7 -0
  137. package/src/src/modules/server/module-server.ts +6 -1
  138. package/src/src/modules/server/ssr-import-rewriter.ts +9 -17
  139. package/src/src/react/compat/ssr-adapter/stream-renderer.ts +1 -1
  140. package/src/src/react/compat/ssr-adapter/string-renderer.ts +2 -2
  141. package/src/src/react/components/Head.tsx +6 -2
  142. package/src/src/react/primitives/input-box.tsx +4 -2
  143. package/src/src/rendering/component-handling.ts +6 -0
  144. package/src/src/rendering/layouts/layout-applicator.ts +4 -5
  145. package/src/src/rendering/orchestrator/module-loader/index.ts +91 -20
  146. package/src/src/rendering/orchestrator/pipeline.ts +2 -0
  147. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +1 -1
  148. package/src/src/rendering/rsc/server-renderer/tree-processor.ts +13 -3
  149. package/src/src/transforms/esm/http-bundler.ts +101 -11
  150. package/src/src/transforms/esm/http-cache.ts +27 -28
  151. package/src/src/transforms/esm/import-rewriter.ts +5 -3
  152. package/src/src/transforms/esm/package-registry.ts +46 -32
  153. package/src/src/transforms/esm/react-imports.ts +3 -7
  154. package/src/src/transforms/esm/types.ts +2 -0
  155. package/src/src/transforms/mdx/esm-module-loader/cache/index.ts +84 -0
  156. package/src/src/transforms/mdx/esm-module-loader/loader.ts +1 -0
  157. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +2 -1
  158. package/src/src/transforms/mdx/esm-module-loader/types.ts +4 -0
  159. package/src/src/transforms/pipeline/context.ts +7 -18
  160. package/src/src/transforms/pipeline/stages/finalize.ts +6 -1
  161. package/src/src/transforms/plugins/babel-node-positions.ts +2 -2
  162. package/src/src/utils/constants/cdn.ts +21 -12
  163. package/src/src/utils/hash-utils.ts +5 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Veryfront Proxy Server (Split Mode)
3
+ *
4
+ * Standalone proxy server that forwards requests to a separate renderer process.
5
+ * Used in production for security isolation of OAuth credentials.
6
+ *
7
+ * For combined mode (single process), use the renderer with --proxy flag instead.
8
+ *
9
+ * Environment Variables:
10
+ * - API_CLIENT_ID_VERYFRONT_RENDERER_PROXY: OAuth client ID (from 1Password)
11
+ * - API_CLIENT_SECRET_VERYFRONT_RENDERER_PROXY: OAuth client secret (from 1Password)
12
+ * - RENDERER_URL: URL of the renderer service
13
+ * - LOCAL_PROJECTS: JSON map of slug → filesystem path (for dev)
14
+ * - CACHE_TYPE: "memory" (default) or "redis"
15
+ * - REDIS_URL: Redis connection URL (required if CACHE_TYPE=redis)
16
+ */
17
+ import * as dntShim from "../_dnt.shims.js";
18
+
19
+
20
+ import { createProxyHandler, type ProxyConfig } from "./handler.js";
21
+ import { createCacheFromEnv } from "./cache/index.js";
22
+ import {
23
+ endSpan,
24
+ extractContext,
25
+ initializeOTLPWithApis,
26
+ injectContext,
27
+ ProxySpanNames,
28
+ shutdownOTLP,
29
+ startServerSpan,
30
+ withContext,
31
+ withSpan,
32
+ } from "./tracing.js";
33
+ import { proxyLogger } from "./logger.js";
34
+ import { parseProjectDomain } from "../src/server/utils/domain-parser.js";
35
+ import { exit, getEnv, onSignal } from "../src/platform/compat/process.js";
36
+ import { createHttpServer, upgradeWebSocket } from "../src/platform/compat/http/index.js";
37
+
38
+ // Configuration from environment variables
39
+ const config: ProxyConfig = {
40
+ apiBaseUrl: getEnv("VERYFRONT_API_BASE_URL") ||
41
+ "http://api.lvh.me:4000",
42
+ clientId: getEnv("API_CLIENT_ID_VERYFRONT_RENDERER_PROXY") || "",
43
+ clientSecret: getEnv("API_CLIENT_SECRET_VERYFRONT_RENDERER_PROXY") || "",
44
+ // Preview uses same service account (scopes determine access)
45
+ previewClientId: getEnv("API_CLIENT_ID_VERYFRONT_RENDERER_PROXY") || "",
46
+ previewClientSecret: getEnv("API_CLIENT_SECRET_VERYFRONT_RENDERER_PROXY") || "",
47
+ localProjects: getEnv("LOCAL_PROJECTS") ? JSON.parse(getEnv("LOCAL_PROJECTS")!) : {},
48
+ };
49
+
50
+ const RENDERER_URL = getEnv("RENDERER_URL") || "http://localhost:3001";
51
+ const PORT = parseInt(getEnv("PORT") || "8080");
52
+ const HOST = getEnv("HOST") || "0.0.0.0"; // Default to 0.0.0.0 for Kubernetes
53
+ const WS_CONNECT_TIMEOUT_MS = 30000;
54
+ // Timeout for forwarding requests to renderer (SSR can take time on cold start)
55
+ const RENDERER_REQUEST_TIMEOUT_MS = parseInt(getEnv("RENDERER_REQUEST_TIMEOUT_MS") || "90000");
56
+
57
+ // Initialize cache and proxy handler
58
+ const cache = await createCacheFromEnv();
59
+ const proxyHandler = createProxyHandler({
60
+ config,
61
+ cache,
62
+ logger: {
63
+ debug: (msg, extra) => proxyLogger.debug(msg, extra),
64
+ info: (msg, extra) => proxyLogger.info(msg, extra),
65
+ warn: (msg, extra) => proxyLogger.warn(msg, extra),
66
+ error: (msg, error, extra) => proxyLogger.error(msg, extra ?? {}, error),
67
+ },
68
+ });
69
+
70
+ // Validate configuration on startup
71
+ const missingCredentials = proxyHandler.validateConfig();
72
+ if (missingCredentials.length > 0) {
73
+ proxyLogger.warn("Missing OAuth credentials", { missingCredentials });
74
+ proxyLogger.warn("Proxy will forward requests without authentication");
75
+ }
76
+
77
+ // Log local projects if configured
78
+ if (Object.keys(proxyHandler.localProjects).length > 0) {
79
+ proxyLogger.info("Local projects configured", {
80
+ projects: Object.keys(proxyHandler.localProjects),
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Handle WebSocket upgrade requests.
86
+ * Bridges browser WebSocket to renderer's HMR WebSocket endpoint.
87
+ */
88
+ function handleWebSocketUpgrade(req: dntShim.Request): dntShim.Response {
89
+ const url = new URL(req.url);
90
+ const host = req.headers.get("host") || "";
91
+
92
+ // Parse domain to extract project slug and environment
93
+ const parsed = parseProjectDomain(host);
94
+ const scope = parsed.environment === "preview" ? "preview" : "production";
95
+ const projectSlug = parsed.slug || undefined;
96
+
97
+ // Build renderer WebSocket URL
98
+ const rendererWsUrl = RENDERER_URL.replace(/^http/, "ws");
99
+ const targetUrl = new URL(`${rendererWsUrl}${url.pathname}${url.search}`);
100
+ targetUrl.searchParams.set("x-project-slug", projectSlug || "");
101
+ targetUrl.searchParams.set("x-environment", scope);
102
+
103
+ proxyLogger.info("[WebSocket] Upgrade request received", {
104
+ host,
105
+ path: url.pathname,
106
+ projectSlug,
107
+ environment: scope,
108
+ parsedEnvironment: parsed.environment,
109
+ targetUrl: targetUrl.toString(),
110
+ });
111
+
112
+ const { socket: clientSocket, response } = upgradeWebSocket(req);
113
+
114
+ let rendererSocket: WebSocket | null = null;
115
+ let connectTimeoutId: ReturnType<typeof dntShim.setTimeout> | null = null;
116
+ let timedOut = false;
117
+
118
+ const clearConnectTimeout = () => {
119
+ if (connectTimeoutId) {
120
+ clearTimeout(connectTimeoutId);
121
+ connectTimeoutId = null;
122
+ }
123
+ };
124
+
125
+ clientSocket.onopen = () => {
126
+ proxyLogger.info("[WebSocket] Client connected, bridging to renderer", {
127
+ targetUrl: targetUrl.toString(),
128
+ });
129
+
130
+ try {
131
+ rendererSocket = new WebSocket(targetUrl.toString());
132
+ } catch (error) {
133
+ proxyLogger.error("[WebSocket] Failed to create renderer WebSocket", {
134
+ error: error instanceof Error ? error.message : String(error),
135
+ targetUrl: targetUrl.toString(),
136
+ });
137
+ clientSocket.close(1011, "Failed to connect to renderer");
138
+ return;
139
+ }
140
+
141
+ connectTimeoutId = dntShim.setTimeout(() => {
142
+ timedOut = true;
143
+ proxyLogger.error("[WebSocket] Renderer connection timeout", {
144
+ targetUrl: targetUrl.toString(),
145
+ timeoutMs: WS_CONNECT_TIMEOUT_MS,
146
+ });
147
+ rendererSocket?.close();
148
+ if (clientSocket.readyState === WebSocket.OPEN) {
149
+ clientSocket.close(1001, "Renderer connection timeout");
150
+ }
151
+ }, WS_CONNECT_TIMEOUT_MS);
152
+
153
+ rendererSocket.onopen = () => {
154
+ clearConnectTimeout();
155
+ if (timedOut) {
156
+ rendererSocket?.close();
157
+ return;
158
+ }
159
+ proxyLogger.info("[WebSocket] Renderer connected, bridge established", {
160
+ projectSlug,
161
+ environment: scope,
162
+ });
163
+ };
164
+
165
+ rendererSocket.onmessage = (event) => {
166
+ if (clientSocket.readyState === WebSocket.OPEN) {
167
+ clientSocket.send(event.data);
168
+ }
169
+ };
170
+
171
+ rendererSocket.onerror = (event) => {
172
+ clearConnectTimeout();
173
+ proxyLogger.error("[WebSocket] Renderer connection error", {
174
+ projectSlug,
175
+ environment: scope,
176
+ targetUrl: targetUrl.toString(),
177
+ error: event instanceof ErrorEvent ? event.message : "Unknown error",
178
+ });
179
+ };
180
+
181
+ rendererSocket.onclose = (event) => {
182
+ clearConnectTimeout();
183
+ proxyLogger.info("[WebSocket] Renderer connection closed", {
184
+ code: event.code,
185
+ reason: event.reason,
186
+ wasClean: event.wasClean,
187
+ });
188
+ if (clientSocket.readyState === WebSocket.OPEN) {
189
+ clientSocket.close(event.code, event.reason);
190
+ }
191
+ };
192
+ };
193
+
194
+ clientSocket.onmessage = (event) => {
195
+ if (rendererSocket?.readyState === WebSocket.OPEN) {
196
+ rendererSocket.send(event.data);
197
+ }
198
+ };
199
+
200
+ clientSocket.onerror = (event) => {
201
+ clearConnectTimeout();
202
+ proxyLogger.error("[WebSocket] Client connection error", {
203
+ error: event instanceof ErrorEvent ? event.message : "Unknown error",
204
+ });
205
+ };
206
+
207
+ clientSocket.onclose = (event) => {
208
+ clearConnectTimeout();
209
+ proxyLogger.info("[WebSocket] Client connection closed", {
210
+ code: event.code,
211
+ reason: event.reason,
212
+ wasClean: event.wasClean,
213
+ });
214
+ if (rendererSocket?.readyState === WebSocket.OPEN) {
215
+ rendererSocket.close();
216
+ }
217
+ };
218
+
219
+ return response;
220
+ }
221
+
222
+ function jsonErrorResponse(status: number, body: Record<string, unknown>): dntShim.Response {
223
+ return new dntShim.Response(JSON.stringify(body), {
224
+ status,
225
+ headers: { "Content-Type": "application/json" },
226
+ });
227
+ }
228
+
229
+ function forwardToRenderer(req: dntShim.Request): Promise<dntShim.Response> {
230
+ const startTime = performance.now();
231
+ const url = new URL(req.url);
232
+
233
+ const parentContext = extractContext(req.headers);
234
+ const spanInfo = startServerSpan(req.method, url.pathname, parentContext);
235
+
236
+ const execute = async (): Promise<dntShim.Response> => {
237
+ try {
238
+ const ctx = await proxyHandler.processRequest(req);
239
+
240
+ if (ctx.error) {
241
+ const ms = Math.round(performance.now() - startTime);
242
+ proxyLogger.error(`${ctx.error.status} ${req.method} ${url.pathname}`, {
243
+ ms,
244
+ domain: ctx.host,
245
+ });
246
+ endSpan(spanInfo?.span, ctx.error.status);
247
+
248
+ // Handle redirect for protected environments
249
+ if (ctx.error.redirectUrl) {
250
+ return new dntShim.Response(null, {
251
+ status: 302,
252
+ headers: { Location: ctx.error.redirectUrl },
253
+ });
254
+ }
255
+
256
+ return jsonErrorResponse(ctx.error.status, {
257
+ error: ctx.error.message,
258
+ status: ctx.error.status,
259
+ });
260
+ }
261
+
262
+ const reqLogger = proxyLogger.child({
263
+ ...(ctx.projectSlug && { project: ctx.projectSlug }),
264
+ env: ctx.environment,
265
+ });
266
+
267
+ const newHeaders = new dntShim.Headers(req.headers);
268
+ if (ctx.token) newHeaders.set("x-token", ctx.token);
269
+ newHeaders.set("x-project-slug", ctx.projectSlug || "");
270
+ newHeaders.set("x-environment", ctx.environment);
271
+ newHeaders.set("x-forwarded-host", ctx.host);
272
+ if (ctx.localPath) newHeaders.set("x-project-path", ctx.localPath);
273
+ // Forward project/release context for cache keying
274
+ if (ctx.projectId) newHeaders.set("x-project-id", ctx.projectId);
275
+ if (ctx.releaseId) newHeaders.set("x-release-id", ctx.releaseId);
276
+ if (ctx.branchId) newHeaders.set("x-branch-id", ctx.branchId);
277
+ if (ctx.branchName) newHeaders.set("x-branch-name", ctx.branchName);
278
+ newHeaders.delete("host");
279
+
280
+ injectContext(newHeaders);
281
+
282
+ const rendererUrl = new URL(url.pathname + url.search, RENDERER_URL);
283
+
284
+ // Create abort controller for timeout
285
+ const abortController = new AbortController();
286
+ const timeoutId = dntShim.setTimeout(() => {
287
+ abortController.abort();
288
+ }, RENDERER_REQUEST_TIMEOUT_MS);
289
+
290
+ let response: dntShim.Response;
291
+ try {
292
+ response = await withSpan(
293
+ ProxySpanNames.HTTP_CLIENT_FETCH,
294
+ () =>
295
+ dntShim.fetch(rendererUrl.toString(), {
296
+ method: req.method,
297
+ headers: newHeaders,
298
+ body: req.body,
299
+ redirect: "manual",
300
+ signal: abortController.signal,
301
+ }),
302
+ {
303
+ "http.method": req.method,
304
+ "http.url": rendererUrl.toString(),
305
+ "http.host": rendererUrl.host,
306
+ "proxy.target": "renderer",
307
+ "proxy.project_slug": ctx.projectSlug || "",
308
+ "proxy.timeout_ms": RENDERER_REQUEST_TIMEOUT_MS,
309
+ },
310
+ );
311
+ } finally {
312
+ clearTimeout(timeoutId);
313
+ }
314
+
315
+ const ms = Math.round(performance.now() - startTime);
316
+ reqLogger.info(`${response.status} ${req.method} ${url.pathname}`, { ms });
317
+
318
+ endSpan(spanInfo?.span, response.status);
319
+
320
+ return new dntShim.Response(response.body, {
321
+ status: response.status,
322
+ statusText: response.statusText,
323
+ headers: response.headers,
324
+ });
325
+ } catch (error) {
326
+ const ms = Math.round(performance.now() - startTime);
327
+
328
+ // Handle timeout specifically
329
+ if (error instanceof Error && error.name === "AbortError") {
330
+ proxyLogger.error(`504 ${req.method} ${url.pathname}`, {
331
+ ms,
332
+ timeoutMs: RENDERER_REQUEST_TIMEOUT_MS,
333
+ });
334
+ endSpan(spanInfo?.span, 504, error);
335
+ return jsonErrorResponse(504, {
336
+ error: "Gateway Timeout",
337
+ message: `Renderer request timed out after ${RENDERER_REQUEST_TIMEOUT_MS}ms`,
338
+ });
339
+ }
340
+
341
+ proxyLogger.error(`502 ${req.method} ${url.pathname}`, { ms }, error as Error);
342
+ endSpan(spanInfo?.span, 502, error as Error);
343
+ return jsonErrorResponse(502, {
344
+ error: "Proxy Error",
345
+ message: error instanceof Error ? error.message : "Unknown error",
346
+ });
347
+ }
348
+ };
349
+
350
+ return spanInfo?.context ? withContext(spanInfo.context, execute) : execute();
351
+ }
352
+
353
+ /**
354
+ * Handle stats endpoint for monitoring.
355
+ */
356
+ async function handleStats(): Promise<dntShim.Response> {
357
+ const stats = await proxyHandler.getStats();
358
+ return new dntShim.Response(JSON.stringify(stats, null, 2), {
359
+ headers: { "Content-Type": "application/json" },
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Proxy API requests directly to Veryfront API (BFF pattern).
365
+ * Routes: /_vf/api/* -> api.veryfront.com/*
366
+ */
367
+ async function handleApiProxy(req: dntShim.Request): Promise<dntShim.Response> {
368
+ const url = new URL(req.url);
369
+
370
+ const token = await proxyHandler.getTokenForApi(req);
371
+ if (!token) {
372
+ return jsonErrorResponse(401, { error: "No authentication token" });
373
+ }
374
+
375
+ // Strip /_vf/api prefix and forward to API
376
+ const apiPath = url.pathname.replace(/^\/_vf\/api/, "");
377
+ const apiUrl = `${config.apiBaseUrl}${apiPath}${url.search}`;
378
+ const apiUrlObj = new URL(apiUrl);
379
+
380
+ try {
381
+ const response = await withSpan(
382
+ ProxySpanNames.HTTP_CLIENT_FETCH,
383
+ () =>
384
+ dntShim.fetch(apiUrl, {
385
+ method: req.method,
386
+ headers: {
387
+ Authorization: `Bearer ${token}`,
388
+ Accept: "application/json",
389
+ "Content-Type": req.headers.get("Content-Type") || "application/json",
390
+ },
391
+ body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
392
+ }),
393
+ {
394
+ "http.method": req.method,
395
+ "http.url": apiUrl,
396
+ "http.host": apiUrlObj.host,
397
+ "proxy.target": "api",
398
+ "proxy.api_path": apiPath,
399
+ },
400
+ );
401
+
402
+ return new dntShim.Response(response.body, {
403
+ status: response.status,
404
+ statusText: response.statusText,
405
+ headers: {
406
+ "Content-Type": response.headers.get("Content-Type") ||
407
+ "application/json",
408
+ "Cache-Control": "no-cache",
409
+ },
410
+ });
411
+ } catch (error) {
412
+ proxyLogger.error("API proxy error", error as Error);
413
+ return jsonErrorResponse(502, {
414
+ error: error instanceof Error ? error.message : "API request failed",
415
+ });
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Main router.
421
+ */
422
+ function router(req: dntShim.Request): Promise<dntShim.Response> {
423
+ const url = new URL(req.url);
424
+
425
+ // WebSocket upgrade
426
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
427
+ return Promise.resolve(handleWebSocketUpgrade(req));
428
+ }
429
+
430
+ // Proxy endpoints
431
+ if (url.pathname === "/_proxy/stats") {
432
+ return handleStats();
433
+ }
434
+
435
+ if (url.pathname === "/_proxy/health") {
436
+ return Promise.resolve(new dntShim.Response("OK", { status: 200 }));
437
+ }
438
+
439
+ // BFF: Proxy API requests directly to Veryfront API
440
+ if (url.pathname.startsWith("/_vf/api/")) {
441
+ return handleApiProxy(req);
442
+ }
443
+
444
+ // Forward all other requests to renderer
445
+ return forwardToRenderer(req);
446
+ }
447
+
448
+ // Graceful shutdown
449
+ async function shutdown(): Promise<void> {
450
+ proxyLogger.info("Shutting down");
451
+ await proxyHandler.close();
452
+ await shutdownOTLP();
453
+ proxyLogger.info("Closed connections");
454
+ exit(0);
455
+ }
456
+
457
+ onSignal("SIGINT", shutdown);
458
+ onSignal("SIGTERM", shutdown);
459
+
460
+ // Initialize tracing and start server
461
+ await initializeOTLPWithApis();
462
+
463
+ proxyLogger.debug("Starting proxy server (split mode)", {
464
+ port: PORT,
465
+ rendererUrl: RENDERER_URL,
466
+ apiBaseUrl: config.apiBaseUrl,
467
+ });
468
+
469
+ // Create and start the HTTP server
470
+ const server = createHttpServer();
471
+ await server.serve(router, { port: PORT, hostname: HOST });
@@ -18,7 +18,7 @@ export async function createPackageJson(
18
18
  dependencies: {
19
19
  react: "^19.0.0",
20
20
  "react-dom": "^19.0.0",
21
- veryfront: "^0.0.63",
21
+ veryfront: "^0.0.88",
22
22
  zod: "^3.24.0",
23
23
  },
24
24
  };
@@ -95,6 +95,7 @@ export function parseCliArgs(args: string[]): ParsedArgs {
95
95
  t: "template",
96
96
  j: "json",
97
97
  w: "with",
98
+ m: "mode",
98
99
  },
99
100
  default: { port: DEFAULT_PORT },
100
101
  }) as ParsedArgs;
@@ -204,51 +204,68 @@ export async function routeCommand(args: ParsedArgs): Promise<void> {
204
204
 
205
205
  case "preview":
206
206
  case "serve": {
207
- showLogo();
208
-
209
- const { runtime } = await import("../../platform/adapters/detect.js");
210
- const adapter = await runtime.get();
211
- const { startUniversalServer } = await import("../../server/production-server.js");
212
-
213
- const projectDir = cwd();
207
+ const mode = (args.mode || args.m || "renderer") as "combined" | "proxy" | "renderer";
214
208
  const port = args.port ?? DEFAULT_DEV_SERVER_PORT;
215
209
  const bindAddress = String(args.hostname || args.host || "0.0.0.0");
216
- const debug = Boolean(args.debug);
217
- const shutdownController = new AbortController();
218
-
219
- const server = await startUniversalServer({
220
- projectDir,
221
- port,
222
- bindAddress,
223
- debug,
224
- adapter,
225
- signal: shutdownController.signal,
226
- });
227
- await server.ready;
228
210
 
229
- let shuttingDown = false;
230
- const shutdown = async (signal: "SIGINT" | "SIGTERM"): Promise<void> => {
231
- if (shuttingDown) return;
232
- shuttingDown = true;
233
-
234
- cliLogger.info(`Received ${signal}, shutting down production server...`);
235
- try {
236
- shutdownController.abort();
237
- await server.stop();
238
- } catch (error) {
239
- cliLogger.warn("Error while shutting down production server:", error);
240
- } finally {
241
- exitProcess(0);
242
- }
243
- };
211
+ if (mode === "proxy") {
212
+ // Proxy-only mode: run OAuth token proxy
213
+ showLogo();
214
+ cliLogger.info(`Starting proxy server on ${bindAddress}:${port}`);
215
+
216
+ // Set environment variables for proxy
217
+ const { setEnv } = await import("../../platform/compat/process.js");
218
+ setEnv("PORT", String(port));
219
+ setEnv("HOST", bindAddress);
220
+
221
+ // Import and run proxy main
222
+ await import("../../../proxy/main.js");
223
+ } else if (mode === "renderer" || mode === "combined") {
224
+ // Renderer mode: run SSR production server
225
+ showLogo();
226
+
227
+ const { runtime } = await import("../../platform/adapters/detect.js");
228
+ const adapter = await runtime.get();
229
+ const { startUniversalServer } = await import("../../server/production-server.js");
230
+
231
+ const projectDir = cwd();
232
+ const debug = Boolean(args.debug);
233
+ const shutdownController = new AbortController();
234
+
235
+ const server = await startUniversalServer({
236
+ projectDir,
237
+ port,
238
+ bindAddress,
239
+ debug,
240
+ adapter,
241
+ signal: shutdownController.signal,
242
+ });
243
+ await server.ready;
244
+
245
+ let shuttingDown = false;
246
+ const shutdown = async (signal: "SIGINT" | "SIGTERM"): Promise<void> => {
247
+ if (shuttingDown) return;
248
+ shuttingDown = true;
249
+
250
+ cliLogger.info(`Received ${signal}, shutting down production server...`);
251
+ try {
252
+ shutdownController.abort();
253
+ await server.stop();
254
+ } catch (error) {
255
+ cliLogger.warn("Error while shutting down production server:", error);
256
+ } finally {
257
+ exitProcess(0);
258
+ }
259
+ };
244
260
 
245
- registerTerminationSignals((signal) => {
246
- void shutdown(signal);
247
- });
261
+ registerTerminationSignals((signal) => {
262
+ void shutdown(signal);
263
+ });
248
264
 
249
- await new Promise(() => {
250
- /* never resolve */
251
- });
265
+ await new Promise(() => {
266
+ /* never resolve */
267
+ });
268
+ }
252
269
  break;
253
270
  }
254
271
 
@@ -18,6 +18,8 @@ export interface GenerateCommandArgs {
18
18
  _: (string | number)[];
19
19
  }
20
20
 
21
+ export type ServerMode = "combined" | "proxy" | "renderer";
22
+
21
23
  export interface ParsedArgs {
22
24
  _: (string | number)[];
23
25
  port?: number;
@@ -41,5 +43,8 @@ export interface ParsedArgs {
41
43
  j?: boolean;
42
44
  with?: string[];
43
45
  w?: string[];
46
+ /** Server mode: combined (default), proxy, or renderer */
47
+ mode?: ServerMode;
48
+ m?: ServerMode;
44
49
  [key: string]: unknown;
45
50
  }