@wopr-network/platform-ui-core 1.1.9 → 1.1.11

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.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,11 +2,12 @@ import { render, screen } from "@testing-library/react";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { ChatMessage } from "@/components/chat/chat-message";
4
4
  import type { ChatMessage as ChatMessageType } from "@/lib/chat/types";
5
+ import { uuid } from "@/lib/uuid";
5
6
 
6
7
  function msg(
7
8
  overrides: Partial<ChatMessageType> & { role: ChatMessageType["role"]; content: string },
8
9
  ): ChatMessageType {
9
- return { id: crypto.randomUUID(), timestamp: Date.now(), ...overrides };
10
+ return { id: uuid(), timestamp: Date.now(), ...overrides };
10
11
  }
11
12
 
12
13
  describe("ChatMessage", () => {
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
3
3
  import { beforeAll, describe, expect, it, vi } from "vitest";
4
4
  import { ChatPanel } from "@/components/chat/chat-panel";
5
5
  import type { ChatMessage as ChatMessageType } from "@/lib/chat/types";
6
+ import { uuid } from "@/lib/uuid";
6
7
 
7
8
  // jsdom does not implement scrollIntoView — polyfill it for ChatPanel's auto-scroll useEffect
8
9
  beforeAll(() => {
@@ -12,7 +13,7 @@ beforeAll(() => {
12
13
  function msg(
13
14
  overrides: Partial<ChatMessageType> & { role: ChatMessageType["role"]; content: string },
14
15
  ): ChatMessageType {
15
- return { id: crypto.randomUUID(), timestamp: Date.now(), ...overrides };
16
+ return { id: uuid(), timestamp: Date.now(), ...overrides };
16
17
  }
17
18
 
18
19
  const baseProps = {
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { apiFetch, apiFetchRaw } from "@/lib/api";
5
5
  import type { ChatMessage } from "@/lib/chat/types";
6
+ import { uuid } from "@/lib/uuid";
6
7
 
7
8
  interface PluginSetupState {
8
9
  isOpen: boolean;
@@ -74,7 +75,7 @@ export function usePluginSetupChat(
74
75
  const controller = new AbortController();
75
76
  abortRef.current = controller;
76
77
 
77
- const sessionId = crypto.randomUUID();
78
+ const sessionId = uuid();
78
79
  sessionIdRef.current = sessionId;
79
80
 
80
81
  const params = new URLSearchParams({ pluginId, botId, sessionId });
@@ -125,7 +126,7 @@ export function usePluginSetupChat(
125
126
  messages: [
126
127
  ...s.messages,
127
128
  {
128
- id: crypto.randomUUID(),
129
+ id: uuid(),
129
130
  role: "bot" as const,
130
131
  content: event.delta,
131
132
  timestamp: Date.now(),
@@ -144,7 +145,7 @@ export function usePluginSetupChat(
144
145
  messages: [
145
146
  ...s.messages,
146
147
  {
147
- id: crypto.randomUUID(),
148
+ id: uuid(),
148
149
  role: "event" as const,
149
150
  content: reason,
150
151
  timestamp: Date.now(),
@@ -186,7 +187,7 @@ export function usePluginSetupChat(
186
187
  messages: [
187
188
  ...s.messages,
188
189
  {
189
- id: crypto.randomUUID(),
190
+ id: uuid(),
190
191
  role: "user" as const,
191
192
  content: text,
192
193
  timestamp: Date.now(),
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { storageKey } from "../brand-config";
3
+ import { uuid } from "../uuid";
3
4
  import type { ChatMessage } from "./types";
4
5
 
5
6
  const HISTORY_KEY = storageKey("chat-history");
@@ -23,12 +24,12 @@ export function getSessionId(): string {
23
24
  try {
24
25
  const existing = localStorage.getItem(SESSION_KEY);
25
26
  if (existing) return existing;
26
- const id = crypto.randomUUID();
27
+ const id = uuid();
27
28
  localStorage.setItem(SESSION_KEY, id);
28
29
  return id;
29
30
  } catch {
30
31
  // localStorage blocked (private browsing) — use ephemeral session ID
31
- return crypto.randomUUID();
32
+ return uuid();
32
33
  }
33
34
  }
34
35
 
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { openChatStream, sendChatMessage } from "@/lib/api";
5
5
  import { eventName } from "@/lib/brand-config";
6
+ import { uuid } from "@/lib/uuid";
6
7
  import { clearChatHistory, getSessionId, loadChatHistory, saveChatHistory } from "./chat-store";
7
8
  import type { ChatEvent, ChatMessage, ChatMode } from "./types";
8
9
 
@@ -76,7 +77,7 @@ export function useChat(): UseChatReturn {
76
77
  if (data.type === "text") {
77
78
  setIsTyping(true);
78
79
  if (pendingBotMsgRef.current === null) {
79
- pendingBotMsgRef.current = crypto.randomUUID();
80
+ pendingBotMsgRef.current = uuid();
80
81
  }
81
82
  const msgId = pendingBotMsgRef.current;
82
83
  setMessages((prev) => {
@@ -105,7 +106,7 @@ export function useChat(): UseChatReturn {
105
106
  setIsTyping(false);
106
107
  pendingBotMsgRef.current = null;
107
108
  addMessage({
108
- id: crypto.randomUUID(),
109
+ id: uuid(),
109
110
  role: "bot",
110
111
  content: `Error: ${data.message}`,
111
112
  timestamp: Date.now(),
@@ -165,7 +166,7 @@ export function useChat(): UseChatReturn {
165
166
  if (!trimmed) return;
166
167
 
167
168
  const userMsg: ChatMessage = {
168
- id: crypto.randomUUID(),
169
+ id: uuid(),
169
170
  role: "user",
170
171
  content: trimmed,
171
172
  timestamp: Date.now(),
@@ -177,7 +178,7 @@ export function useChat(): UseChatReturn {
177
178
  sendChatMessage(sessionId.current, trimmed).catch(() => {
178
179
  setIsTyping(false);
179
180
  addMessage({
180
- id: crypto.randomUUID(),
181
+ id: uuid(),
181
182
  role: "bot",
182
183
  content: "Sorry, your message could not be sent. Please try again.",
183
184
  timestamp: Date.now(),
@@ -190,7 +191,7 @@ export function useChat(): UseChatReturn {
190
191
  const addEventMarker = useCallback(
191
192
  (text: string) => {
192
193
  addMessage({
193
- id: crypto.randomUUID(),
194
+ id: uuid(),
194
195
  role: "event",
195
196
  content: text,
196
197
  timestamp: Date.now(),
@@ -227,7 +228,7 @@ export function useChat(): UseChatReturn {
227
228
  const notify = useCallback(
228
229
  (text: string) => {
229
230
  addMessage({
230
- id: crypto.randomUUID(),
231
+ id: uuid(),
231
232
  role: "bot",
232
233
  content: text,
233
234
  timestamp: Date.now(),
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Generate a v4 UUID that works in all contexts.
3
+ *
4
+ * crypto.randomUUID() requires a secure context (HTTPS or localhost).
5
+ * On HTTP with a non-localhost domain (e.g. local dev via /etc/hosts),
6
+ * it throws. This utility falls back to crypto.getRandomValues() which
7
+ * works everywhere, including non-secure contexts.
8
+ */
9
+ export function uuid(): string {
10
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
11
+ return crypto.randomUUID();
12
+ }
13
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
14
+ const bytes = new Uint8Array(16);
15
+ crypto.getRandomValues(bytes);
16
+ // Set version (4) and variant (RFC 4122)
17
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
18
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
19
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
20
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21
+ }
22
+ // Last resort: Math.random (not cryptographically secure, but won't crash)
23
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
24
+ const r = (Math.random() * 16) | 0;
25
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
26
+ });
27
+ }
package/src/proxy.ts CHANGED
@@ -9,7 +9,11 @@ const apiOrigin = process.env.NEXT_PUBLIC_API_URL
9
9
  ? new URL(process.env.NEXT_PUBLIC_API_URL).origin
10
10
  : "";
11
11
 
12
- const isSecureOrigin = process.env.NODE_ENV === "production";
12
+ /**
13
+ * Only add upgrade-insecure-requests when actually serving over HTTPS.
14
+ * Checking NODE_ENV breaks local dev in Docker (NODE_ENV=production but no TLS).
15
+ * Computed per-request in buildCsp() from the request URL protocol.
16
+ */
13
17
 
14
18
  /**
15
19
  * Nonce-based style-src toggle.
@@ -21,7 +25,8 @@ const isSecureOrigin = process.env.NODE_ENV === "production";
21
25
  const NONCE_STYLES_ENABLED = true;
22
26
 
23
27
  /** Build the CSP header value with a per-request nonce. */
24
- function buildCsp(nonce: string): string {
28
+ function buildCsp(nonce: string, requestUrl?: string): string {
29
+ const isHttps = requestUrl ? requestUrl.startsWith("https://") : false;
25
30
  return [
26
31
  "default-src 'self'",
27
32
  `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://js.stripe.com`,
@@ -36,7 +41,7 @@ function buildCsp(nonce: string): string {
36
41
  "base-uri 'self'",
37
42
  "form-action 'self'",
38
43
  "object-src 'none'",
39
- ...(isSecureOrigin ? ["upgrade-insecure-requests"] : []),
44
+ ...(isHttps ? ["upgrade-insecure-requests"] : []),
40
45
  ].join("; ");
41
46
  }
42
47
 
@@ -160,7 +165,7 @@ export default async function middleware(request: NextRequest) {
160
165
 
161
166
  // Generate a per-request nonce for CSP
162
167
  const nonce = crypto.randomUUID();
163
- const cspHeaderValue = buildCsp(nonce);
168
+ const cspHeaderValue = buildCsp(nonce, request.url);
164
169
 
165
170
  /** Apply CSP and cache-busting headers to any response before returning it. */
166
171
  function withCsp(response: NextResponse): NextResponse {