@wopr-network/platform-ui-core 1.27.2 → 1.27.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.27.2",
3
+ "version": "1.27.3",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,62 +1,51 @@
1
1
  /**
2
2
  * Centralized API base URL configuration.
3
3
  *
4
- * Set NEXT_PUBLIC_API_URL to the platform root (e.g. "http://localhost:3001").
5
- * All API clients derive their paths from this single value.
4
+ * The API URL is derived at runtime from the hostname no NEXT_PUBLIC_API_URL
5
+ * env var needed. Convention: UI at `<domain>` API at `api.<domain>`.
6
+ * Staging: `staging.<domain>` → `staging.api.<domain>`.
6
7
  *
7
- * In production runtime, validates that the URL is a public HTTPS endpoint.
8
- * This prevents internal Docker hostnames from leaking into the browser bundle.
8
+ * Falls back to NEXT_PUBLIC_API_URL if set (local dev), then localhost.
9
9
  */
10
10
 
11
11
  import { getBrandConfig } from "./brand-config";
12
12
 
13
- const INTERNAL_HOSTNAME_RE =
14
- /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|.*\.local$)/i;
15
-
16
- function isInternalHostname(hostname: string): boolean {
17
- return INTERNAL_HOSTNAME_RE.test(hostname) || !hostname.includes(".");
18
- }
19
-
20
- function validateProductionApiUrl(url: string | undefined): void {
21
- const isProductionRuntime =
22
- process.env.NODE_ENV === "production" &&
23
- process.env.NEXT_RUNTIME === "nodejs" &&
24
- process.env.NEXT_PHASE !== "phase-production-build" &&
25
- !process.env.E2E_MOCK_API;
26
-
27
- if (!isProductionRuntime) return;
28
-
29
- if (!url) {
30
- throw new Error(
31
- "NEXT_PUBLIC_API_URL is not set. In production it must be a public HTTPS URL. An internal hostname or localhost will not work in the browser.",
32
- );
33
- }
34
-
35
- let parsed: URL;
36
- try {
37
- parsed = new URL(url);
38
- } catch {
39
- throw new Error(
40
- `NEXT_PUBLIC_API_URL ("${url}") is not a valid URL. In production it must be a public HTTPS URL.`,
41
- );
13
+ /**
14
+ * Derive the platform API URL from the current hostname.
15
+ * Works on both client (window.location) and server (brand config domain).
16
+ */
17
+ function resolveApiUrl(): string {
18
+ // 1. Explicit env var — local dev, tests, or override
19
+ const envUrl = process.env.NEXT_PUBLIC_API_URL;
20
+ if (envUrl) return envUrl;
21
+
22
+ // 2. Client-side: derive from browser hostname
23
+ if (typeof window !== "undefined") {
24
+ const host = window.location.hostname;
25
+ const proto = window.location.protocol;
26
+ // localhost → local dev
27
+ if (host === "localhost" || host === "127.0.0.1") {
28
+ return "http://localhost:3001";
29
+ }
30
+ // staging.X.com → staging.api.X.com
31
+ if (host.startsWith("staging.")) {
32
+ const base = host.replace(/^staging\./, "");
33
+ return `${proto}//staging.api.${base}`;
34
+ }
35
+ // X.com → api.X.com
36
+ return `${proto}//api.${host}`;
42
37
  }
43
38
 
44
- if (isInternalHostname(parsed.hostname)) {
45
- throw new Error(
46
- `NEXT_PUBLIC_API_URL ("${url}") contains an internal hostname ("${parsed.hostname}"). In production it must be a public HTTPS URL.`,
47
- );
39
+ // 3. Server-side: derive from brand config domain
40
+ const domain = getBrandConfig().domain;
41
+ if (domain && domain !== "localhost" && domain.includes(".")) {
42
+ return `https://api.${domain}`;
48
43
  }
49
44
 
50
- if (parsed.protocol !== "https:") {
51
- throw new Error(
52
- `NEXT_PUBLIC_API_URL ("${url}") uses ${parsed.protocol} but production requires https. Set it to the public HTTPS URL.`,
53
- );
54
- }
45
+ return "http://localhost:3001";
55
46
  }
56
47
 
57
- validateProductionApiUrl(process.env.NEXT_PUBLIC_API_URL);
58
-
59
- export const PLATFORM_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
48
+ export const PLATFORM_BASE_URL = resolveApiUrl();
60
49
 
61
50
  /** Base URL for REST API calls (platform root + /api). */
62
51
  export const API_BASE_URL = `${PLATFORM_BASE_URL}/api`;
package/src/proxy.ts CHANGED
@@ -5,9 +5,24 @@ import { sanitizeRedirectUrl } from "@/lib/utils";
5
5
 
6
6
  const log = logger("middleware");
7
7
 
8
- const apiOrigin = process.env.NEXT_PUBLIC_API_URL
9
- ? new URL(process.env.NEXT_PUBLIC_API_URL).origin
10
- : "";
8
+ /** Derive API origin from request hostname. Convention: api.<domain>. */
9
+ function getApiOrigin(host: string): string {
10
+ // Explicit override for local dev
11
+ if (process.env.NEXT_PUBLIC_API_URL) {
12
+ try {
13
+ return new URL(process.env.NEXT_PUBLIC_API_URL).origin;
14
+ } catch {
15
+ /* fall through */
16
+ }
17
+ }
18
+ if (!host || host === "localhost" || host.startsWith("localhost:")) return "";
19
+ const hostname = host.split(":")[0];
20
+ if (hostname.startsWith("staging.")) {
21
+ const base = hostname.replace(/^staging\./, "");
22
+ return `https://staging.api.${base}`;
23
+ }
24
+ return `https://api.${hostname}`;
25
+ }
11
26
 
12
27
  /**
13
28
  * Only add upgrade-insecure-requests when actually serving over HTTPS.
@@ -25,8 +40,9 @@ const apiOrigin = process.env.NEXT_PUBLIC_API_URL
25
40
  const NONCE_STYLES_ENABLED = true;
26
41
 
27
42
  /** Build the CSP header value with a per-request nonce. */
28
- function buildCsp(nonce: string, requestUrl?: string): string {
43
+ function buildCsp(nonce: string, requestUrl?: string, requestHost?: string): string {
29
44
  const isHttps = requestUrl ? requestUrl.startsWith("https://") : false;
45
+ const api = getApiOrigin(requestHost ?? "");
30
46
  return [
31
47
  "default-src 'self'",
32
48
  `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://js.stripe.com`,
@@ -35,7 +51,7 @@ function buildCsp(nonce: string, requestUrl?: string): string {
35
51
  : ["style-src 'self' 'unsafe-inline'"]),
36
52
  "img-src 'self' data: blob:",
37
53
  "font-src 'self'",
38
- `connect-src 'self' https://api.stripe.com${apiOrigin ? ` ${apiOrigin}` : ""}`,
54
+ `connect-src 'self' https://api.stripe.com${api ? ` ${api}` : ""}`,
39
55
  "frame-src https://js.stripe.com",
40
56
  "frame-ancestors 'none'",
41
57
  "base-uri 'self'",
@@ -165,7 +181,7 @@ export default async function middleware(request: NextRequest) {
165
181
 
166
182
  // Generate a per-request nonce for CSP
167
183
  const nonce = crypto.randomUUID();
168
- const cspHeaderValue = buildCsp(nonce, request.url);
184
+ const cspHeaderValue = buildCsp(nonce, request.url, request.headers.get("host") ?? "");
169
185
 
170
186
  /** Apply CSP and cache-busting headers to any response before returning it. */
171
187
  function withCsp(response: NextResponse): NextResponse {