@hotmeshio/long-tail 0.4.16 → 0.4.18

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.
@@ -117,6 +117,10 @@ class LTExpressAdapter {
117
117
  // Attach NATS WebSocket proxy (if configured)
118
118
  const natsAdapter = events_1.eventRegistry.getAdapter(nats_1.NatsEventAdapter);
119
119
  if (natsAdapter?.wsProxyTarget) {
120
+ // Store basePath so the settings endpoint can derive the correct wsUrl
121
+ if (this.basePath) {
122
+ natsAdapter.setWsProxyBasePath(this.basePath);
123
+ }
120
124
  (0, nats_ws_proxy_1.attachNatsWsProxy)(server, natsAdapter.wsProxyTarget, {
121
125
  basePath: this.basePath,
122
126
  onWsUrlDerived: (url) => {
@@ -1,3 +1,4 @@
1
+ import type { IncomingMessage } from 'http';
1
2
  import type { LTApiResult } from '../types/sdk';
2
3
  /**
3
4
  * Return platform settings for the current deployment.
@@ -5,6 +6,7 @@ import type { LTApiResult } from '../types/sdk';
5
6
  * Includes telemetry configuration (trace URL), escalation claim duration
6
7
  * options, and event transport details (socket.io, NATS, or none).
7
8
  *
9
+ * @param req — the incoming HTTP request, used to derive NATS WS URL from headers when proxying
8
10
  * @returns `{ status: 200, data: { telemetry, escalation, events } }`
9
11
  */
10
- export declare function getSettings(): Promise<LTApiResult>;
12
+ export declare function getSettings(req?: IncomingMessage): Promise<LTApiResult>;
@@ -5,18 +5,39 @@ const telemetry_1 = require("../lib/telemetry");
5
5
  const events_1 = require("../lib/events");
6
6
  const nats_1 = require("../lib/events/nats");
7
7
  const socketio_1 = require("../lib/events/socketio");
8
+ const nats_ws_proxy_1 = require("../lib/events/nats-ws-proxy");
8
9
  const config_1 = require("../modules/config");
9
10
  const defaults_1 = require("../modules/defaults");
10
11
  const llm_1 = require("../services/llm");
12
+ /**
13
+ * Resolve the NATS WebSocket URL for the browser.
14
+ *
15
+ * Priority:
16
+ * 1. Explicit wsUrl on the adapter (set via config or auto-derived from a prior request)
17
+ * 2. When a wsProxy is configured, derive from the current request's headers
18
+ * 3. null — no NATS WS available
19
+ */
20
+ function resolveNatsWsUrl(adapter, req) {
21
+ if (adapter.wsUrl)
22
+ return adapter.wsUrl;
23
+ if (adapter.wsProxyTarget && req) {
24
+ const derived = (0, nats_ws_proxy_1.deriveWsUrlFromRequest)(req, adapter.wsProxyBasePath);
25
+ // Cache for future requests and for the proxy's onWsUrlDerived
26
+ adapter.setWsUrl(derived);
27
+ return derived;
28
+ }
29
+ return null;
30
+ }
11
31
  /**
12
32
  * Return platform settings for the current deployment.
13
33
  *
14
34
  * Includes telemetry configuration (trace URL), escalation claim duration
15
35
  * options, and event transport details (socket.io, NATS, or none).
16
36
  *
37
+ * @param req — the incoming HTTP request, used to derive NATS WS URL from headers when proxying
17
38
  * @returns `{ status: 200, data: { telemetry, escalation, events } }`
18
39
  */
19
- async function getSettings() {
40
+ async function getSettings(req) {
20
41
  try {
21
42
  const hasSocketIO = !!events_1.eventRegistry.getAdapter(socketio_1.SocketIOEventAdapter);
22
43
  const natsAdapter = events_1.eventRegistry.getAdapter(nats_1.NatsEventAdapter);
@@ -37,7 +58,7 @@ async function getSettings() {
37
58
  },
38
59
  events: {
39
60
  transport,
40
- natsWsUrl: natsAdapter?.wsUrl ?? null,
61
+ natsWsUrl: natsAdapter ? resolveNatsWsUrl(natsAdapter, req) : null,
41
62
  },
42
63
  ai: {
43
64
  enabled: (0, llm_1.hasLLMApiKey)(),
@@ -1,6 +1,15 @@
1
1
  import type { Server as HttpServer } from 'http';
2
2
  /** Default path for the NATS WebSocket proxy endpoint. */
3
3
  export declare const NATS_WS_PROXY_PATH = "/nats-ws";
4
+ /**
5
+ * Derive the public NATS WebSocket URL from an HTTP request's headers.
6
+ *
7
+ * Respects `X-Forwarded-Proto` and `X-Forwarded-Host` so the correct
8
+ * `wss://` scheme is used behind TLS-terminating load balancers.
9
+ */
10
+ export declare function deriveWsUrlFromRequest(req: {
11
+ headers: Record<string, string | string[] | undefined>;
12
+ }, basePath?: string): string;
4
13
  /**
5
14
  * Attach a WebSocket proxy to an HTTP server that bridges browser
6
15
  * connections to an internal NATS WebSocket endpoint.
@@ -1,11 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NATS_WS_PROXY_PATH = void 0;
4
+ exports.deriveWsUrlFromRequest = deriveWsUrlFromRequest;
4
5
  exports.attachNatsWsProxy = attachNatsWsProxy;
5
6
  const ws_1 = require("ws");
6
7
  const logger_1 = require("../logger");
7
8
  /** Default path for the NATS WebSocket proxy endpoint. */
8
9
  exports.NATS_WS_PROXY_PATH = '/nats-ws';
10
+ /**
11
+ * Derive the public NATS WebSocket URL from an HTTP request's headers.
12
+ *
13
+ * Respects `X-Forwarded-Proto` and `X-Forwarded-Host` so the correct
14
+ * `wss://` scheme is used behind TLS-terminating load balancers.
15
+ */
16
+ function deriveWsUrlFromRequest(req, basePath = '') {
17
+ const proto = req.headers['x-forwarded-proto'] || 'ws';
18
+ const scheme = proto === 'https' ? 'wss' : 'ws';
19
+ const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost';
20
+ return `${scheme}://${host}${basePath}${exports.NATS_WS_PROXY_PATH}`;
21
+ }
9
22
  /**
10
23
  * Attach a WebSocket proxy to an HTTP server that bridges browser
11
24
  * connections to an internal NATS WebSocket endpoint.
@@ -28,10 +41,7 @@ function attachNatsWsProxy(server, target, options = {}) {
28
41
  return;
29
42
  // Derive the public wsUrl from the first request's headers
30
43
  if (!derived && options.onWsUrlDerived) {
31
- const proto = req.headers['x-forwarded-proto'] || 'ws';
32
- const scheme = proto === 'https' ? 'wss' : 'ws';
33
- const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost';
34
- options.onWsUrlDerived(`${scheme}://${host}${proxyPath}`);
44
+ options.onWsUrlDerived(deriveWsUrlFromRequest(req, basePath));
35
45
  derived = true;
36
46
  }
37
47
  wss.handleUpgrade(req, socket, head, (clientWs) => {
@@ -25,6 +25,7 @@ export declare class NatsEventAdapter implements LTEventAdapter {
25
25
  private url;
26
26
  private _wsUrl;
27
27
  private _wsProxyTarget;
28
+ private _wsProxyBasePath;
28
29
  private subjectPrefix;
29
30
  private token?;
30
31
  private originId;
@@ -46,6 +47,10 @@ export declare class NatsEventAdapter implements LTEventAdapter {
46
47
  setWsUrl(url: string): void;
47
48
  /** Internal NATS WS target for the proxy to bridge to (e.g. ws://nats:9222). */
48
49
  get wsProxyTarget(): string | null;
50
+ /** BasePath the proxy is mounted at (e.g. /longtail). Set by LTExpressAdapter. */
51
+ get wsProxyBasePath(): string;
52
+ /** Set the proxy basePath (called by LTExpressAdapter.attachServer). */
53
+ setWsProxyBasePath(basePath: string): void;
49
54
  /** NATS auth token for browser connections. */
50
55
  get authToken(): string | null;
51
56
  /**
@@ -30,6 +30,7 @@ class NatsEventAdapter {
30
30
  constructor(options) {
31
31
  this.nc = null;
32
32
  this.sub = null;
33
+ this._wsProxyBasePath = '';
33
34
  this.originId = (0, crypto_1.randomUUID)();
34
35
  this.callbackAdapter = null;
35
36
  this.url = options?.url || config_1.config.NATS_URL;
@@ -54,6 +55,14 @@ class NatsEventAdapter {
54
55
  get wsProxyTarget() {
55
56
  return this._wsProxyTarget;
56
57
  }
58
+ /** BasePath the proxy is mounted at (e.g. /longtail). Set by LTExpressAdapter. */
59
+ get wsProxyBasePath() {
60
+ return this._wsProxyBasePath;
61
+ }
62
+ /** Set the proxy basePath (called by LTExpressAdapter.attachServer). */
63
+ setWsProxyBasePath(basePath) {
64
+ this._wsProxyBasePath = basePath;
65
+ }
57
66
  /** NATS auth token for browser connections. */
58
67
  get authToken() {
59
68
  return this.token || null;
@@ -3,19 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const express_1 = require("express");
4
4
  const events_1 = require("../lib/events");
5
5
  const nats_1 = require("../lib/events/nats");
6
+ const nats_ws_proxy_1 = require("../lib/events/nats-ws-proxy");
6
7
  const router = (0, express_1.Router)();
7
8
  /**
8
9
  * GET /api/nats-credentials
9
10
  * Returns NATS WebSocket URL and auth token.
10
11
  * Mounted behind requireAuth — only authenticated users receive the token.
11
12
  */
12
- router.get('/', async (_req, res) => {
13
+ router.get('/', async (req, res) => {
13
14
  const natsAdapter = events_1.eventRegistry.getAdapter(nats_1.NatsEventAdapter);
14
15
  if (!natsAdapter) {
15
16
  return res.json({ natsWsUrl: null, natsToken: null });
16
17
  }
18
+ // Derive wsUrl from request headers when proxy is active but no URL cached yet
19
+ let wsUrl = natsAdapter.wsUrl;
20
+ if (!wsUrl && natsAdapter.wsProxyTarget) {
21
+ wsUrl = (0, nats_ws_proxy_1.deriveWsUrlFromRequest)(req, natsAdapter.wsProxyBasePath);
22
+ natsAdapter.setWsUrl(wsUrl);
23
+ }
17
24
  res.json({
18
- natsWsUrl: natsAdapter.wsUrl,
25
+ natsWsUrl: wsUrl,
19
26
  natsToken: natsAdapter.authToken,
20
27
  });
21
28
  });
@@ -40,8 +40,8 @@ const router = (0, express_1.Router)();
40
40
  * GET /api/settings
41
41
  * Returns frontend-relevant configuration (no secrets).
42
42
  */
43
- router.get('/', async (_req, res) => {
44
- const result = await api.getSettings();
43
+ router.get('/', async (req, res) => {
44
+ const result = await api.getSettings(req);
45
45
  res.status(result.status).json(result.data ?? { error: result.error });
46
46
  });
47
47
  exports.default = router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/long-tail",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Long Tail Workflows — Durable AI workflows with human-in-the-loop escalation. Powered by PostgreSQL.",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",