@neuralnomads/codenomad-dev 0.15.0-dev-20260509-71531697 → 0.15.0-dev-20260511-efe3f505

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 (43) hide show
  1. package/dist/index.js +4 -0
  2. package/dist/previews/manager.js +63 -0
  3. package/dist/server/http-server.js +372 -14
  4. package/dist/server/routes/previews.js +25 -0
  5. package/package.json +1 -1
  6. package/public/assets/{ChangesTab-CnpuqxE4.js → ChangesTab-DGWzwppj.js} +2 -2
  7. package/public/assets/{DiffToolbar-CNfiD6cp.js → DiffToolbar-dXDVyXwL.js} +1 -1
  8. package/public/assets/{FilesTab-Bkn2Glaq.js → FilesTab-IN-3vEym.js} +2 -2
  9. package/public/assets/{GitChangesTab-D5kJ45cq.js → GitChangesTab-yhDvCETZ.js} +2 -2
  10. package/public/assets/{SplitFilePanel-DMnUqWnk.js → SplitFilePanel-01YOnTOG.js} +1 -1
  11. package/public/assets/StatusTab-CI5m-66G.js +1 -0
  12. package/public/assets/{align-justify-pBFgPIct.js → align-justify-DRTNZj9A.js} +1 -1
  13. package/public/assets/{bundle-full-CAX-gm_x.js → bundle-full-D0o5M_wa.js} +1 -1
  14. package/public/assets/{diff-viewer-hE2nGS5H.js → diff-viewer-jXMkhi-L.js} +1 -1
  15. package/public/assets/{index-Dsw4vOfl.css → index-BBdin4zl.css} +1 -1
  16. package/public/assets/{index-DPZxQS72.js → index-Be11ZFEb.js} +1 -1
  17. package/public/assets/index-CNG3sDWM.js +1 -0
  18. package/public/assets/index-CZr9LwXP.js +1 -0
  19. package/public/assets/{index-BJHLw6jN.js → index-Cb-FGBAx.js} +1 -1
  20. package/public/assets/index-CfmQyy0c.js +1 -0
  21. package/public/assets/index-CmWJRIhG.js +1 -0
  22. package/public/assets/index-Cpe_O_IY.js +1 -0
  23. package/public/assets/index-PvzRwXKl.js +2 -0
  24. package/public/assets/index-fZ-H5e-Q.js +1 -0
  25. package/public/assets/{loading-aqR3QZbb.js → loading-BNcShOZ7.js} +1 -1
  26. package/public/assets/main-CkAUlWH0.js +62 -0
  27. package/public/assets/{markdown-CpWe8Ysv.js → markdown-BO-eqU7M.js} +3 -3
  28. package/public/assets/{monaco-viewer-DephYCRo.js → monaco-viewer-CUKSC3oL.js} +10 -10
  29. package/public/assets/{tool-call-DE6Sz4v2.js → tool-call-B2LR4TW4.js} +3 -3
  30. package/public/assets/{unified-picker-Drs_u9Rq.js → unified-picker-hHK-At_1.js} +1 -1
  31. package/public/assets/{wrap-text-DATH1miG.js → wrap-text-y6UsswWF.js} +1 -1
  32. package/public/index.html +4 -4
  33. package/public/loading.html +4 -4
  34. package/public/sw.js +1 -1
  35. package/public/assets/StatusTab-DgvQQ67G.js +0 -1
  36. package/public/assets/index-BHFt2rWX.js +0 -1
  37. package/public/assets/index-Bv35Fv-v.js +0 -2
  38. package/public/assets/index-CFwfPYqZ.js +0 -1
  39. package/public/assets/index-CNMyfcAf.js +0 -1
  40. package/public/assets/index-CNcpLvjh.js +0 -1
  41. package/public/assets/index-DazH9JZS.js +0 -1
  42. package/public/assets/index-Z75kPRX1.js +0 -1
  43. package/public/assets/main-DhuDexWn.js +0 -57
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/networ
25
25
  import { startDevReleaseMonitor } from "./releases/dev-release-monitor";
26
26
  import { SpeechService } from "./speech/service";
27
27
  import { SideCarManager } from "./sidecars/manager";
28
+ import { PreviewManager } from "./previews/manager";
28
29
  import { ClientConnectionManager } from "./clients/connection-manager";
29
30
  import { PluginChannelManager } from "./plugins/channel";
30
31
  import { VoiceModeManager } from "./plugins/voice-mode";
@@ -230,6 +231,7 @@ async function main() {
230
231
  eventBus,
231
232
  logger: logger.child({ component: "sidecars" }),
232
233
  });
234
+ const previewManager = new PreviewManager();
233
235
  const instanceEventBridge = new InstanceEventBridge({
234
236
  workspaceManager,
235
237
  eventBus,
@@ -313,6 +315,7 @@ async function main() {
313
315
  instanceStore,
314
316
  speechService,
315
317
  sidecarManager,
318
+ previewManager,
316
319
  authManager,
317
320
  clientConnectionManager,
318
321
  pluginChannel,
@@ -338,6 +341,7 @@ async function main() {
338
341
  instanceStore,
339
342
  speechService,
340
343
  sidecarManager,
344
+ previewManager,
341
345
  authManager,
342
346
  clientConnectionManager,
343
347
  pluginChannel,
@@ -0,0 +1,63 @@
1
+ import { randomUUID } from "crypto";
2
+ export class PreviewManager {
3
+ constructor() {
4
+ this.previews = new Map();
5
+ }
6
+ create(sessionId, rawUrl) {
7
+ const target = this.normalizeTargetUrl(rawUrl);
8
+ const token = randomUUID();
9
+ const record = {
10
+ token,
11
+ sessionId,
12
+ target,
13
+ createdAt: new Date().toISOString(),
14
+ };
15
+ this.previews.set(token, record);
16
+ return this.toPreviewSession(record);
17
+ }
18
+ get(token) {
19
+ const record = this.previews.get(token);
20
+ return record ? this.toPreviewSession(record) : undefined;
21
+ }
22
+ delete(token) {
23
+ return this.previews.delete(token);
24
+ }
25
+ buildTargetUrl(token, incomingPath, search = "") {
26
+ const record = this.previews.get(token);
27
+ if (!record)
28
+ return undefined;
29
+ const publicBase = this.buildProxyBasePath(token);
30
+ let targetPath = incomingPath.startsWith(publicBase) ? incomingPath.slice(publicBase.length) : incomingPath;
31
+ if (!targetPath || targetPath === "/") {
32
+ targetPath = record.target.pathname || "/";
33
+ }
34
+ else if (!targetPath.startsWith("/")) {
35
+ targetPath = `/${targetPath}`;
36
+ }
37
+ return new URL(`${targetPath}${search}`, record.target.origin);
38
+ }
39
+ buildProxyBasePath(token) {
40
+ return `/previews/${encodeURIComponent(token)}`;
41
+ }
42
+ normalizeTargetUrl(rawUrl) {
43
+ const trimmed = rawUrl.trim();
44
+ const withProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
45
+ const target = new URL(withProtocol);
46
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
47
+ throw new Error("Preview URL must use HTTP or HTTPS");
48
+ }
49
+ if (target.username || target.password) {
50
+ throw new Error("Preview URL cannot include credentials");
51
+ }
52
+ return target;
53
+ }
54
+ toPreviewSession(record) {
55
+ return {
56
+ token: record.token,
57
+ sessionId: record.sessionId,
58
+ targetUrl: record.target.toString(),
59
+ proxyUrl: `${this.buildProxyBasePath(record.token)}${record.target.pathname}${record.target.search}${record.target.hash}`,
60
+ createdAt: record.createdAt,
61
+ };
62
+ }
63
+ }
@@ -22,6 +22,7 @@ import { registerSpeechRoutes } from "./routes/speech";
22
22
  import { registerRemoteServerRoutes } from "./routes/remote-servers";
23
23
  import { registerRemoteProxyRoutes } from "./routes/remote-proxy";
24
24
  import { registerSideCarRoutes } from "./routes/sidecars";
25
+ import { registerPreviewRoutes } from "./routes/previews";
25
26
  import { BackgroundProcessManager } from "../background-processes/manager";
26
27
  import { registerAuthRoutes } from "./routes/auth";
27
28
  import { sendUnauthorized, wantsHtml } from "../auth/http-auth";
@@ -148,7 +149,7 @@ export function createHttpServer(deps) {
148
149
  return;
149
150
  }
150
151
  const session = deps.authManager.getSessionFromRequest(request);
151
- const requiresAuthForApi = pathname.startsWith("/api/") || pathname.startsWith("/workspaces/") || pathname.startsWith("/sidecars/");
152
+ const requiresAuthForApi = pathname.startsWith("/api/") || pathname.startsWith("/workspaces/") || pathname.startsWith("/sidecars/") || pathname.startsWith("/previews/");
152
153
  if (requiresAuthForApi && !session) {
153
154
  // Allow OpenCode plugin -> CodeNomad calls with per-instance basic auth.
154
155
  const pluginMatch = pathname.match(/^\/workspaces\/([^/]+)\/plugin(?:\/|$)/);
@@ -210,12 +211,19 @@ export function createHttpServer(deps) {
210
211
  registerRemoteProxyRoutes(app, { logger: proxyLogger, sessionManager: deps.remoteProxySessionManager });
211
212
  registerSpeechRoutes(app, { speechService: deps.speechService });
212
213
  registerSideCarRoutes(app, { sidecarManager: deps.sidecarManager });
214
+ registerPreviewRoutes(app, { previewManager: deps.previewManager });
213
215
  registerSideCarProxyRoutes(app, { sidecarManager: deps.sidecarManager, logger: proxyLogger });
216
+ registerPreviewProxyRoutes(app, { previewManager: deps.previewManager, logger: proxyLogger });
214
217
  setupSideCarWebSocketProxy(app, {
215
218
  sidecarManager: deps.sidecarManager,
216
219
  authManager: deps.authManager,
217
220
  logger: proxyLogger,
218
221
  });
222
+ setupPreviewWebSocketProxy(app, {
223
+ previewManager: deps.previewManager,
224
+ authManager: deps.authManager,
225
+ logger: proxyLogger,
226
+ });
219
227
  registerPluginRoutes(app, {
220
228
  workspaceManager: deps.workspaceManager,
221
229
  eventBus: deps.eventBus,
@@ -226,10 +234,10 @@ export function createHttpServer(deps) {
226
234
  registerBackgroundProcessRoutes(app, { backgroundProcessManager });
227
235
  registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger });
228
236
  if (deps.uiDevServerUrl) {
229
- setupDevProxy(app, deps.uiDevServerUrl, deps.authManager);
237
+ setupDevProxy(app, deps.uiDevServerUrl, deps.authManager, deps.previewManager, proxyLogger);
230
238
  }
231
239
  else {
232
- setupStaticUi(app, deps.uiStaticDir, deps.authManager);
240
+ setupStaticUi(app, deps.uiStaticDir, deps.authManager, deps.previewManager, proxyLogger);
233
241
  }
234
242
  return {
235
243
  instance: app,
@@ -306,6 +314,28 @@ function registerSideCarProxyRoutes(app, deps) {
306
314
  app.all("/sidecars/:id", proxyBaseHandler);
307
315
  app.all("/sidecars/:id/*", proxyWildcardHandler);
308
316
  }
317
+ function registerPreviewProxyRoutes(app, deps) {
318
+ const proxyBaseHandler = async (request, reply) => {
319
+ await proxyPreviewRequest({
320
+ request,
321
+ reply,
322
+ previewManager: deps.previewManager,
323
+ logger: deps.logger,
324
+ pathSuffix: "",
325
+ });
326
+ };
327
+ const proxyWildcardHandler = async (request, reply) => {
328
+ await proxyPreviewRequest({
329
+ request,
330
+ reply,
331
+ previewManager: deps.previewManager,
332
+ logger: deps.logger,
333
+ pathSuffix: request.params["*"] ?? "",
334
+ });
335
+ };
336
+ app.all("/previews/:token", proxyBaseHandler);
337
+ app.all("/previews/:token/*", proxyWildcardHandler);
338
+ }
309
339
  function setupSideCarWebSocketProxy(app, deps) {
310
340
  app.server.on("upgrade", (request, socket, head) => {
311
341
  const rawUrl = request.url ?? "/";
@@ -326,6 +356,26 @@ function setupSideCarWebSocketProxy(app, deps) {
326
356
  });
327
357
  });
328
358
  }
359
+ function setupPreviewWebSocketProxy(app, deps) {
360
+ app.server.on("upgrade", (request, socket, head) => {
361
+ const rawUrl = request.url ?? "/";
362
+ const parsed = parsePreviewUpgradePath(rawUrl);
363
+ if (!parsed) {
364
+ return;
365
+ }
366
+ void proxyPreviewWebSocketUpgrade({
367
+ request,
368
+ socket: socket,
369
+ head,
370
+ token: parsed.token,
371
+ incomingPath: parsed.pathname,
372
+ search: parsed.search,
373
+ previewManager: deps.previewManager,
374
+ authManager: deps.authManager,
375
+ logger: deps.logger,
376
+ });
377
+ });
378
+ }
329
379
  function registerInstanceProxyRoutes(app, deps) {
330
380
  app.register(async (instance) => {
331
381
  instance.removeAllContentTypeParsers();
@@ -608,7 +658,7 @@ function normalizeInstanceSuffix(pathSuffix) {
608
658
  const trimmed = pathSuffix.replace(/^\/+/, "");
609
659
  return trimmed.length === 0 ? "/" : `/${trimmed}`;
610
660
  }
611
- function setupStaticUi(app, uiDir, authManager) {
661
+ function setupStaticUi(app, uiDir, authManager, previewManager, logger) {
612
662
  if (!uiDir) {
613
663
  app.log.warn("UI static directory not provided; API endpoints only");
614
664
  return;
@@ -617,6 +667,13 @@ function setupStaticUi(app, uiDir, authManager) {
617
667
  app.log.warn({ uiDir }, "UI static directory missing; API endpoints only");
618
668
  return;
619
669
  }
670
+ app.addHook("preHandler", (request, reply, done) => {
671
+ const session = authManager.getSessionFromRequest(request);
672
+ if (session && proxyPreviewFallbackFromReferer(request, reply, previewManager, logger)) {
673
+ return;
674
+ }
675
+ done();
676
+ });
620
677
  app.register(fastifyStatic, {
621
678
  root: uiDir,
622
679
  prefix: "/",
@@ -630,6 +687,9 @@ function setupStaticUi(app, uiDir, authManager) {
630
687
  return;
631
688
  }
632
689
  const session = authManager.getSessionFromRequest(request);
690
+ if (session && proxyPreviewFallbackFromReferer(request, reply, previewManager, logger)) {
691
+ return;
692
+ }
633
693
  if (!session && wantsHtml(request)) {
634
694
  reply.redirect("/login");
635
695
  return;
@@ -642,7 +702,7 @@ function setupStaticUi(app, uiDir, authManager) {
642
702
  }
643
703
  });
644
704
  }
645
- function setupDevProxy(app, upstreamBase, authManager) {
705
+ function setupDevProxy(app, upstreamBase, authManager, previewManager, logger) {
646
706
  app.log.info({ upstreamBase }, "Proxying UI requests to development server");
647
707
  app.setNotFoundHandler((request, reply) => {
648
708
  const url = request.raw.url ?? "";
@@ -651,6 +711,9 @@ function setupDevProxy(app, upstreamBase, authManager) {
651
711
  return;
652
712
  }
653
713
  const session = authManager.getSessionFromRequest(request);
714
+ if (session && proxyPreviewFallbackFromReferer(request, reply, previewManager, logger)) {
715
+ return;
716
+ }
654
717
  if (!session && wantsHtml(request)) {
655
718
  reply.redirect("/login");
656
719
  return;
@@ -658,6 +721,45 @@ function setupDevProxy(app, upstreamBase, authManager) {
658
721
  void proxyToDevServer(request, reply, upstreamBase);
659
722
  });
660
723
  }
724
+ function proxyPreviewFallbackFromReferer(request, reply, previewManager, logger) {
725
+ const rawUrl = request.raw.url ?? request.url ?? "";
726
+ const pathname = rawUrl.split("?")[0] ?? "";
727
+ if (!isPreviewFallbackPath(pathname)) {
728
+ return false;
729
+ }
730
+ const refererHeader = request.headers.referer ?? request.headers.referrer;
731
+ const referer = Array.isArray(refererHeader) ? refererHeader[0] : refererHeader;
732
+ if (!referer) {
733
+ return false;
734
+ }
735
+ const parsed = parsePreviewUpgradePath(referer);
736
+ if (!parsed) {
737
+ return false;
738
+ }
739
+ void proxyPreviewAssetRequest({
740
+ request,
741
+ reply,
742
+ previewManager,
743
+ logger,
744
+ token: parsed.token,
745
+ });
746
+ return true;
747
+ }
748
+ function isPreviewFallbackPath(pathname) {
749
+ if (!pathname || pathname === "/")
750
+ return false;
751
+ if (pathname.startsWith("/api/") || pathname === "/api")
752
+ return false;
753
+ if (pathname.startsWith("/workspaces/"))
754
+ return false;
755
+ if (pathname.startsWith("/sidecars/"))
756
+ return false;
757
+ if (pathname.startsWith("/previews/"))
758
+ return false;
759
+ if (pathname.startsWith("/auth/") || pathname === "/login")
760
+ return false;
761
+ return true;
762
+ }
661
763
  async function proxyToDevServer(request, reply, upstreamBase) {
662
764
  try {
663
765
  const targetUrl = new URL(request.raw.url ?? "/", upstreamBase);
@@ -698,6 +800,69 @@ function buildProxyHeaders(headers) {
698
800
  }
699
801
  return result;
700
802
  }
803
+ function buildFetchProxyHeaders(headers, targetOrigin) {
804
+ const sanitized = sanitizeSideCarProxyRequestHeaders(headers, targetOrigin);
805
+ const result = {};
806
+ for (const [key, value] of Object.entries(sanitized)) {
807
+ if (!value)
808
+ continue;
809
+ if (key.toLowerCase() === "cookie")
810
+ continue;
811
+ result[key] = Array.isArray(value) ? value.join(",") : value;
812
+ }
813
+ return result;
814
+ }
815
+ function headersToRecord(headers) {
816
+ const result = {};
817
+ headers.forEach((value, key) => {
818
+ result[key.toLowerCase()] = value;
819
+ });
820
+ return result;
821
+ }
822
+ function getHeaderValue(headers, key) {
823
+ const value = headers[key.toLowerCase()];
824
+ return Array.isArray(value) ? value[0] : value;
825
+ }
826
+ function shouldForwardRequestBody(method) {
827
+ const normalized = method.toUpperCase();
828
+ return normalized !== "GET" && normalized !== "HEAD";
829
+ }
830
+ function isHtmlContentType(contentType) {
831
+ const normalized = contentType.toLowerCase();
832
+ return normalized.includes("text/html") || normalized.includes("application/xhtml+xml");
833
+ }
834
+ function isCssContentType(contentType) {
835
+ return contentType.toLowerCase().includes("text/css");
836
+ }
837
+ function rewritePreviewBodyUrls(body, publicBase, kind) {
838
+ if (kind === "css") {
839
+ return rewriteCssPreviewUrls(body, publicBase);
840
+ }
841
+ return rewriteCssPreviewUrls(body
842
+ .replace(/\b(src|href|action|poster|data)=(["'])\/(?!\/)([^"']*)\2/gi, (_match, attr, quote, pathValue) => {
843
+ return `${attr}=${quote}${publicBase}/${pathValue}${quote}`;
844
+ })
845
+ .replace(/\bsrcset=(["'])([^"']*)\1/gi, (_match, quote, value) => {
846
+ return `srcset=${quote}${rewriteSrcsetPreviewUrls(value, publicBase)}${quote}`;
847
+ }), publicBase);
848
+ }
849
+ function rewriteCssPreviewUrls(body, publicBase) {
850
+ return body.replace(/url\((\s*)(["']?)\/(?!\/)([^"')]+)\2(\s*)\)/gi, (_match, before, quote, pathValue, after) => {
851
+ return `url(${before}${quote}${publicBase}/${pathValue}${quote}${after})`;
852
+ });
853
+ }
854
+ function rewriteSrcsetPreviewUrls(value, publicBase) {
855
+ return value
856
+ .split(",")
857
+ .map((entry) => {
858
+ const trimmed = entry.trimStart();
859
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//"))
860
+ return entry;
861
+ const leading = entry.slice(0, entry.length - trimmed.length);
862
+ return `${leading}${publicBase}${trimmed}`;
863
+ })
864
+ .join(",");
865
+ }
701
866
  async function proxySideCarRequest(args) {
702
867
  const sidecarId = args.request.params.id ?? "";
703
868
  const sidecar = await args.sidecarManager.get(sidecarId);
@@ -714,13 +879,113 @@ async function proxySideCarRequest(args) {
714
879
  const targetOrigin = args.sidecarManager.buildTargetOrigin(sidecar);
715
880
  const targetUrl = `${targetOrigin}${targetPath}`;
716
881
  args.logger.debug({ sidecarId: sidecar.id, targetUrl, pathname, prefixMode: sidecar.prefixMode }, "Proxying request to SideCar");
717
- await args.reply.from(targetUrl, {
718
- rewriteRequestHeaders: (_originalRequest, headers) => sanitizeSideCarProxyRequestHeaders(headers, targetOrigin),
882
+ await proxyTargetRequest({
883
+ reply: args.reply,
884
+ logger: args.logger,
885
+ targetUrl,
886
+ targetOrigin,
887
+ logContext: { sidecarId: sidecar.id },
888
+ errorMessage: "SideCar proxy failed",
719
889
  rewriteHeaders: (headers) => rewriteSideCarResponseHeaders(headers, sidecarId, targetOrigin, sidecar.prefixMode),
890
+ });
891
+ }
892
+ async function proxyPreviewRequest(args) {
893
+ const token = args.request.params.token ?? "";
894
+ const preview = args.previewManager.get(token);
895
+ if (!preview) {
896
+ args.reply.code(404).send({ error: "Preview not found" });
897
+ return;
898
+ }
899
+ const rawUrl = args.request.raw.url ?? args.request.url ?? "";
900
+ const queryIndex = rawUrl.indexOf("?");
901
+ const search = queryIndex >= 0 ? rawUrl.slice(queryIndex) : "";
902
+ const pathSuffix = args.pathSuffix ?? "";
903
+ const requestPath = pathSuffix ? `${args.previewManager.buildProxyBasePath(token)}/${pathSuffix.replace(/^\/+/, "")}` : args.previewManager.buildProxyBasePath(token);
904
+ const targetUrl = args.previewManager.buildTargetUrl(token, requestPath, search);
905
+ if (!targetUrl) {
906
+ args.reply.code(404).send({ error: "Preview not found" });
907
+ return;
908
+ }
909
+ args.logger.debug({ previewToken: token, targetUrl: targetUrl.toString() }, "Proxying request to preview");
910
+ await proxyPreviewTargetRequest({
911
+ request: args.request,
912
+ reply: args.reply,
913
+ logger: args.logger,
914
+ targetUrl: targetUrl.toString(),
915
+ targetOrigin: targetUrl.origin,
916
+ publicBase: args.previewManager.buildProxyBasePath(token),
917
+ logContext: { previewToken: token },
918
+ errorMessage: "Preview proxy failed",
919
+ rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, token, targetUrl.origin),
920
+ });
921
+ }
922
+ async function proxyPreviewAssetRequest(args) {
923
+ const rawUrl = args.request.raw.url ?? args.request.url ?? "";
924
+ const queryIndex = rawUrl.indexOf("?");
925
+ const search = queryIndex >= 0 ? rawUrl.slice(queryIndex) : "";
926
+ const pathname = rawUrl.split("?")[0] ?? "/";
927
+ const targetUrl = args.previewManager.buildTargetUrl(args.token, pathname, search);
928
+ if (!targetUrl) {
929
+ args.reply.code(404).send({ error: "Preview not found" });
930
+ return;
931
+ }
932
+ args.logger.debug({ previewToken: args.token, targetUrl: targetUrl.toString() }, "Proxying preview fallback asset");
933
+ await proxyPreviewTargetRequest({
934
+ request: args.request,
935
+ reply: args.reply,
936
+ logger: args.logger,
937
+ targetUrl: targetUrl.toString(),
938
+ targetOrigin: targetUrl.origin,
939
+ publicBase: args.previewManager.buildProxyBasePath(args.token),
940
+ logContext: { previewToken: args.token, previewFallback: true },
941
+ errorMessage: "Preview proxy failed",
942
+ rewriteHeaders: (headers) => rewritePreviewResponseHeaders(headers, args.token, targetUrl.origin),
943
+ });
944
+ }
945
+ async function proxyPreviewTargetRequest(args) {
946
+ try {
947
+ const response = await fetch(args.targetUrl, {
948
+ method: args.request.method,
949
+ headers: buildFetchProxyHeaders(args.request.headers, args.targetOrigin),
950
+ body: shouldForwardRequestBody(args.request.method) ? args.request.raw : undefined,
951
+ duplex: shouldForwardRequestBody(args.request.method) ? "half" : undefined,
952
+ redirect: "manual",
953
+ });
954
+ const headers = args.rewriteHeaders(headersToRecord(response.headers));
955
+ const contentType = getHeaderValue(headers, "content-type") ?? response.headers.get("content-type") ?? "";
956
+ delete headers["content-length"];
957
+ delete headers["content-encoding"];
958
+ for (const [key, value] of Object.entries(headers)) {
959
+ if (value !== undefined)
960
+ args.reply.header(key, value);
961
+ }
962
+ args.reply.code(response.status);
963
+ if (!response.body || args.request.method === "HEAD") {
964
+ args.reply.send();
965
+ return;
966
+ }
967
+ if (isHtmlContentType(contentType) || isCssContentType(contentType)) {
968
+ const text = await response.text();
969
+ args.reply.send(rewritePreviewBodyUrls(text, args.publicBase, isCssContentType(contentType) ? "css" : "html"));
970
+ return;
971
+ }
972
+ args.reply.send(Buffer.from(await response.arrayBuffer()));
973
+ }
974
+ catch (error) {
975
+ args.logger.error({ ...args.logContext, err: error, targetUrl: args.targetUrl }, args.errorMessage);
976
+ if (!args.reply.sent) {
977
+ args.reply.code(502).send({ error: args.errorMessage });
978
+ }
979
+ }
980
+ }
981
+ async function proxyTargetRequest(args) {
982
+ await args.reply.from(args.targetUrl, {
983
+ rewriteRequestHeaders: (_originalRequest, headers) => sanitizeSideCarProxyRequestHeaders(headers, args.targetOrigin),
984
+ rewriteHeaders: args.rewriteHeaders,
720
985
  onError: (reply, { error }) => {
721
- args.logger.error({ sidecarId: sidecar.id, err: error, targetUrl }, "Failed to proxy SideCar request");
986
+ args.logger.error({ ...args.logContext, err: error, targetUrl: args.targetUrl }, args.errorMessage);
722
987
  if (!reply.sent) {
723
- reply.code(502).send({ error: "SideCar proxy failed" });
988
+ reply.code(502).send({ error: args.errorMessage });
724
989
  }
725
990
  },
726
991
  });
@@ -748,6 +1013,29 @@ function parseSideCarUpgradePath(rawUrl) {
748
1013
  return null;
749
1014
  }
750
1015
  }
1016
+ function parsePreviewUpgradePath(rawUrl) {
1017
+ let parsed;
1018
+ try {
1019
+ parsed = new URL(rawUrl, "http://localhost");
1020
+ }
1021
+ catch {
1022
+ return null;
1023
+ }
1024
+ const match = parsed.pathname.match(/^\/previews\/([^/]+)(?:\/.*)?$/);
1025
+ if (!match) {
1026
+ return null;
1027
+ }
1028
+ try {
1029
+ return {
1030
+ token: decodeURIComponent(match[1] ?? ""),
1031
+ pathname: parsed.pathname,
1032
+ search: parsed.search,
1033
+ };
1034
+ }
1035
+ catch {
1036
+ return null;
1037
+ }
1038
+ }
751
1039
  async function proxySideCarWebSocketUpgrade(args) {
752
1040
  const { request, socket, head, sidecarId, incomingPath, search, sidecarManager, authManager, logger } = args;
753
1041
  if (!isWebSocketUpgradeRequest(request)) {
@@ -768,6 +1056,46 @@ async function proxySideCarWebSocketUpgrade(args) {
768
1056
  const targetPath = sidecarManager.buildTargetPath(sidecarId, incomingPath, search);
769
1057
  const targetUrl = new URL(`${targetOrigin}${targetPath}`);
770
1058
  logger.debug({ sidecarId, targetUrl: targetUrl.toString(), prefixMode: sidecar.prefixMode }, "Proxying websocket to SideCar");
1059
+ proxyTargetWebSocketUpgrade({
1060
+ request,
1061
+ socket,
1062
+ head,
1063
+ targetUrl,
1064
+ logger,
1065
+ logContext: { sidecarId },
1066
+ proxyLabel: "SideCar",
1067
+ });
1068
+ }
1069
+ async function proxyPreviewWebSocketUpgrade(args) {
1070
+ const { request, socket, head, token, incomingPath, search, previewManager, authManager, logger } = args;
1071
+ if (!isWebSocketUpgradeRequest(request)) {
1072
+ rejectUpgrade(socket, 400, "Bad Request");
1073
+ return;
1074
+ }
1075
+ const session = authManager.getSessionFromHeaders(request.headers);
1076
+ if (!session) {
1077
+ rejectUpgrade(socket, 401, "Unauthorized");
1078
+ return;
1079
+ }
1080
+ const targetUrl = previewManager.buildTargetUrl(token, incomingPath, search);
1081
+ if (!targetUrl) {
1082
+ rejectUpgrade(socket, 404, "Not Found");
1083
+ return;
1084
+ }
1085
+ logger.debug({ previewToken: token, targetUrl: targetUrl.toString() }, "Proxying websocket to preview");
1086
+ proxyTargetWebSocketUpgrade({
1087
+ request,
1088
+ socket,
1089
+ head,
1090
+ targetUrl,
1091
+ logger,
1092
+ logContext: { previewToken: token },
1093
+ proxyLabel: "preview",
1094
+ stripCookies: true,
1095
+ });
1096
+ }
1097
+ function proxyTargetWebSocketUpgrade(args) {
1098
+ const { request, socket, head, targetUrl, logger, logContext, proxyLabel, stripCookies } = args;
771
1099
  const { socket: upstream, readyEvent } = createSideCarUpstreamSocket(targetUrl);
772
1100
  const closeBoth = () => {
773
1101
  if (!socket.destroyed) {
@@ -778,21 +1106,21 @@ async function proxySideCarWebSocketUpgrade(args) {
778
1106
  }
779
1107
  };
780
1108
  upstream.once("error", (error) => {
781
- logger.error({ sidecarId, err: error, targetUrl: targetUrl.toString() }, "Failed to proxy SideCar websocket");
1109
+ logger.error({ ...logContext, err: error, targetUrl: targetUrl.toString() }, `Failed to proxy ${proxyLabel} websocket`);
782
1110
  rejectUpgrade(socket, 502, "Bad Gateway");
783
1111
  if (!upstream.destroyed) {
784
1112
  upstream.destroy();
785
1113
  }
786
1114
  });
787
1115
  socket.once("error", (error) => {
788
- logger.debug({ sidecarId, err: error }, "SideCar websocket client socket errored");
1116
+ logger.debug({ ...logContext, err: error }, `${proxyLabel} websocket client socket errored`);
789
1117
  if (!upstream.destroyed) {
790
1118
  upstream.destroy();
791
1119
  }
792
1120
  });
793
1121
  upstream.once(readyEvent, () => {
794
1122
  try {
795
- upstream.write(buildSideCarWebSocketRequest(request, targetUrl));
1123
+ upstream.write(buildSideCarWebSocketRequest(request, targetUrl, { stripCookies }));
796
1124
  if (head.length > 0) {
797
1125
  upstream.write(head);
798
1126
  }
@@ -800,7 +1128,7 @@ async function proxySideCarWebSocketUpgrade(args) {
800
1128
  socket.pipe(upstream);
801
1129
  }
802
1130
  catch (error) {
803
- logger.error({ sidecarId, err: error, targetUrl: targetUrl.toString() }, "Failed to forward SideCar websocket upgrade");
1131
+ logger.error({ ...logContext, err: error, targetUrl: targetUrl.toString() }, `Failed to forward ${proxyLabel} websocket upgrade`);
804
1132
  closeBoth();
805
1133
  }
806
1134
  });
@@ -832,7 +1160,7 @@ function createSideCarUpstreamSocket(targetUrl) {
832
1160
  readyEvent: "connect",
833
1161
  };
834
1162
  }
835
- function buildSideCarWebSocketRequest(request, targetUrl) {
1163
+ function buildSideCarWebSocketRequest(request, targetUrl, options) {
836
1164
  const pathWithQuery = `${targetUrl.pathname}${targetUrl.search}`;
837
1165
  const requestLine = `${request.method ?? "GET"} ${pathWithQuery} HTTP/${request.httpVersion}\r\n`;
838
1166
  const headerLines = [];
@@ -846,6 +1174,8 @@ function buildSideCarWebSocketRequest(request, targetUrl) {
846
1174
  const lower = key.toLowerCase();
847
1175
  if (blockedHeaders.has(lower))
848
1176
  continue;
1177
+ if (options?.stripCookies && lower === "cookie")
1178
+ continue;
849
1179
  if (lower === "origin") {
850
1180
  headerLines.push(`Origin: ${targetUrl.origin}\r\n`);
851
1181
  continue;
@@ -899,6 +1229,34 @@ function rewriteSideCarResponseHeaders(headers, sidecarId, targetOrigin, prefixM
899
1229
  }
900
1230
  return next;
901
1231
  }
1232
+ function rewritePreviewResponseHeaders(headers, token, targetOrigin) {
1233
+ const next = { ...headers };
1234
+ delete next["x-frame-options"];
1235
+ delete next["content-security-policy"];
1236
+ delete next["content-security-policy-report-only"];
1237
+ delete next["set-cookie"];
1238
+ delete next["set-cookie2"];
1239
+ const locationHeader = next.location;
1240
+ const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
1241
+ if (!location) {
1242
+ return next;
1243
+ }
1244
+ const publicBase = `/previews/${encodeURIComponent(token)}`;
1245
+ if (location.startsWith("/")) {
1246
+ next.location = `${publicBase}${location}`;
1247
+ return next;
1248
+ }
1249
+ try {
1250
+ const parsed = new URL(location);
1251
+ if (parsed.origin === targetOrigin) {
1252
+ next.location = `${publicBase}${parsed.pathname}${parsed.search}${parsed.hash}`;
1253
+ }
1254
+ }
1255
+ catch {
1256
+ // Relative redirects should continue to resolve against the current preview path.
1257
+ }
1258
+ return next;
1259
+ }
902
1260
  function sanitizeSideCarProxyRequestHeaders(headers, targetOrigin) {
903
1261
  const blockedHeaders = getBlockedSideCarRequestHeaders();
904
1262
  const next = {};
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ const PreviewCreateSchema = z.object({
3
+ sessionId: z.string().trim().min(1),
4
+ url: z.string().trim().min(1),
5
+ });
6
+ export function registerPreviewRoutes(app, deps) {
7
+ app.post("/api/previews", async (request, reply) => {
8
+ try {
9
+ const body = PreviewCreateSchema.parse(request.body ?? {});
10
+ return deps.previewManager.create(body.sessionId, body.url);
11
+ }
12
+ catch (error) {
13
+ reply.code(400);
14
+ return { error: error instanceof Error ? error.message : "Failed to create preview" };
15
+ }
16
+ });
17
+ app.delete("/api/previews/:token", async (request, reply) => {
18
+ const removed = deps.previewManager.delete(request.params.token);
19
+ if (!removed) {
20
+ reply.code(404);
21
+ return { error: "Preview not found" };
22
+ }
23
+ reply.code(204);
24
+ });
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralnomads/codenomad-dev",
3
- "version": "0.15.0-dev-20260509-71531697",
3
+ "version": "0.15.0-dev-20260511-efe3f505",
4
4
  "description": "CodeNomad Server",
5
5
  "license": "MIT",
6
6
  "author": {