@yak-io/javascript 0.1.0
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/LICENSE +36 -0
- package/dist/client.d.ts +175 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +504 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.server.d.ts +2 -0
- package/dist/index.server.d.ts.map +1 -0
- package/dist/index.server.js +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +44 -0
- package/dist/page-context.d.ts +10 -0
- package/dist/page-context.d.ts.map +1 -0
- package/dist/page-context.js +69 -0
- package/dist/schema-parser.d.ts +21 -0
- package/dist/schema-parser.d.ts.map +1 -0
- package/dist/schema-parser.js +341 -0
- package/dist/server/createYakHandler.d.ts +19 -0
- package/dist/server/createYakHandler.d.ts.map +1 -0
- package/dist/server/createYakHandler.js +185 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/sources.d.ts +24 -0
- package/dist/server/sources.d.ts.map +1 -0
- package/dist/server/sources.js +116 -0
- package/dist/types/config.d.ts +19 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +1 -0
- package/dist/types/messaging.d.ts +153 -0
- package/dist/types/messaging.d.ts.map +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/routes.d.ts +26 -0
- package/dist/types/routes.d.ts.map +1 -0
- package/dist/types/routes.js +1 -0
- package/dist/types/tools.d.ts +137 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +1 -0
- package/dist/version.d.ts +23 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/package.json +53 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { extractPageContext, debounce } from "./page-context.js";
|
|
2
|
+
import { EMBED_PROTOCOL_VERSION } from "./version.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Determines the iframe origin based on the current environment.
|
|
6
|
+
* - Internal dev (localhost on *.yak.* domain) -> http://localhost:3001
|
|
7
|
+
* - Dev stage (*.yak.supply) -> https://chat.yak.supply
|
|
8
|
+
* - Everything else (including external developers on localhost) -> https://chat.yak.io
|
|
9
|
+
*/
|
|
10
|
+
function getIframeOriginForEnvironment() {
|
|
11
|
+
if (typeof window !== "undefined") {
|
|
12
|
+
const hostname = window.location.hostname;
|
|
13
|
+
// Check for dev stage via hostname pattern (e.g., *.yak.supply)
|
|
14
|
+
if (hostname.endsWith(".yak.supply") || hostname === "yak.supply") {
|
|
15
|
+
return "https://chat.yak.supply";
|
|
16
|
+
}
|
|
17
|
+
// Internal development only: localhost when running on a yak domain locally
|
|
18
|
+
// This is detected by checking if we're on localhost AND the referrer/opener is a yak domain
|
|
19
|
+
// For simplicity, we check if running on localhost with a specific dev flag
|
|
20
|
+
if ((hostname === "localhost" || hostname === "127.0.0.1") &&
|
|
21
|
+
typeof window.__YAK_INTERNAL_DEV__ !== "undefined") {
|
|
22
|
+
return "http://localhost:3001";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Default to production - external developers on localhost will connect to prod
|
|
26
|
+
return "https://chat.yak.io";
|
|
27
|
+
}
|
|
28
|
+
export class YakClient {
|
|
29
|
+
config;
|
|
30
|
+
iframeWindow = null;
|
|
31
|
+
isWidgetOpen = false;
|
|
32
|
+
readyTarget = null;
|
|
33
|
+
unexpectedOriginLogged = false;
|
|
34
|
+
lastUrl = " ";
|
|
35
|
+
debouncedSendContext;
|
|
36
|
+
observer = null;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.debouncedSendContext = debounce(() => {
|
|
40
|
+
logger.debug("DOM mutation detected, sending page context");
|
|
41
|
+
this.sendPageContext();
|
|
42
|
+
}, 2000);
|
|
43
|
+
}
|
|
44
|
+
updateConfig(newConfig) {
|
|
45
|
+
this.config = { ...this.config, ...newConfig };
|
|
46
|
+
if (this.readyTarget && this.config.chatConfig) {
|
|
47
|
+
this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the iframe origin URL (base URL for the chat widget)
|
|
52
|
+
* This is computed each time to support setting __YAK_INTERNAL_DEV__ after page load.
|
|
53
|
+
*/
|
|
54
|
+
getIframeOrigin() {
|
|
55
|
+
return getIframeOriginForEnvironment();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the full iframe embed URL for the chatbot
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const client = new YakClient({ appId: "my-app" });
|
|
63
|
+
* const iframeSrc = client.getEmbedUrl();
|
|
64
|
+
* // Returns: "https://chat.yak.io/embed/v1/my-app"
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
getEmbedUrl() {
|
|
68
|
+
const origin = this.getIframeOrigin();
|
|
69
|
+
const baseUrl = `${origin}/embed/v${EMBED_PROTOCOL_VERSION}/${encodeURIComponent(this.config.appId)}`;
|
|
70
|
+
// Build URL params for theme colors to avoid flash of default colors
|
|
71
|
+
const params = new URLSearchParams();
|
|
72
|
+
const theme = this.config.theme;
|
|
73
|
+
if (theme?.colorMode && theme.colorMode !== "system") {
|
|
74
|
+
params.set("colorMode", theme.colorMode);
|
|
75
|
+
}
|
|
76
|
+
// Light mode colors
|
|
77
|
+
if (theme?.light?.background) {
|
|
78
|
+
params.set("lightBg", theme.light.background);
|
|
79
|
+
}
|
|
80
|
+
if (theme?.light?.border) {
|
|
81
|
+
params.set("lightBorder", theme.light.border);
|
|
82
|
+
}
|
|
83
|
+
if (theme?.light?.messageBackground) {
|
|
84
|
+
params.set("lightMessageBg", theme.light.messageBackground);
|
|
85
|
+
}
|
|
86
|
+
if (theme?.light?.placeholderColor) {
|
|
87
|
+
params.set("lightPlaceholder", theme.light.placeholderColor);
|
|
88
|
+
}
|
|
89
|
+
if (theme?.light?.submitButtonColor) {
|
|
90
|
+
params.set("lightSubmitBtn", theme.light.submitButtonColor);
|
|
91
|
+
}
|
|
92
|
+
if (theme?.light?.submitButtonTextColor) {
|
|
93
|
+
params.set("lightSubmitBtnText", theme.light.submitButtonTextColor);
|
|
94
|
+
}
|
|
95
|
+
if (theme?.light?.headerIconColor) {
|
|
96
|
+
params.set("lightHeaderIcon", theme.light.headerIconColor);
|
|
97
|
+
}
|
|
98
|
+
// Dark mode colors
|
|
99
|
+
if (theme?.dark?.background) {
|
|
100
|
+
params.set("darkBg", theme.dark.background);
|
|
101
|
+
}
|
|
102
|
+
if (theme?.dark?.border) {
|
|
103
|
+
params.set("darkBorder", theme.dark.border);
|
|
104
|
+
}
|
|
105
|
+
if (theme?.dark?.messageBackground) {
|
|
106
|
+
params.set("darkMessageBg", theme.dark.messageBackground);
|
|
107
|
+
}
|
|
108
|
+
if (theme?.dark?.placeholderColor) {
|
|
109
|
+
params.set("darkPlaceholder", theme.dark.placeholderColor);
|
|
110
|
+
}
|
|
111
|
+
if (theme?.dark?.submitButtonColor) {
|
|
112
|
+
params.set("darkSubmitBtn", theme.dark.submitButtonColor);
|
|
113
|
+
}
|
|
114
|
+
if (theme?.dark?.submitButtonTextColor) {
|
|
115
|
+
params.set("darkSubmitBtnText", theme.dark.submitButtonTextColor);
|
|
116
|
+
}
|
|
117
|
+
if (theme?.dark?.headerIconColor) {
|
|
118
|
+
params.set("darkHeaderIcon", theme.dark.headerIconColor);
|
|
119
|
+
}
|
|
120
|
+
const queryString = params.toString();
|
|
121
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the app ID
|
|
125
|
+
*/
|
|
126
|
+
getAppId() {
|
|
127
|
+
return this.config.appId;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the current theme configuration
|
|
131
|
+
*/
|
|
132
|
+
getTheme() {
|
|
133
|
+
return this.config.theme;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send a prompt message to the chatbot iframe
|
|
137
|
+
* Note: The iframe must be ready to receive messages (onReady callback must have fired)
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* const client = new YakClient({ appId: "my-app", onReady: () => {
|
|
142
|
+
* client.sendPrompt("Help me with my order");
|
|
143
|
+
* }});
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
sendPrompt(prompt) {
|
|
147
|
+
if (!this.iframeWindow) {
|
|
148
|
+
logger.warn("Cannot send prompt: iframe not ready");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const message = {
|
|
152
|
+
type: "yak:prompt",
|
|
153
|
+
payload: { prompt },
|
|
154
|
+
};
|
|
155
|
+
this.iframeWindow.postMessage(message, this.getIframeOrigin());
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Send a focus request to the chatbot iframe
|
|
159
|
+
* This will focus the chat input field
|
|
160
|
+
*/
|
|
161
|
+
sendFocus() {
|
|
162
|
+
if (!this.iframeWindow) {
|
|
163
|
+
logger.warn("Cannot send focus: iframe not ready");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const message = {
|
|
167
|
+
type: "yak:focus",
|
|
168
|
+
};
|
|
169
|
+
this.iframeWindow.postMessage(message, this.getIframeOrigin());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if the iframe is ready to receive messages
|
|
173
|
+
*/
|
|
174
|
+
isReady() {
|
|
175
|
+
return this.readyTarget !== null;
|
|
176
|
+
}
|
|
177
|
+
setIframeWindow(window) {
|
|
178
|
+
this.iframeWindow = window;
|
|
179
|
+
if (!window) {
|
|
180
|
+
this.readyTarget = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
setWidgetOpen(isOpen) {
|
|
184
|
+
this.isWidgetOpen = isOpen;
|
|
185
|
+
if (isOpen && this.readyTarget && this.config.chatConfig) {
|
|
186
|
+
this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);
|
|
187
|
+
}
|
|
188
|
+
if (isOpen) {
|
|
189
|
+
this.startObserving();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
this.stopObserving();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
mount() {
|
|
196
|
+
if (typeof window !== "undefined") {
|
|
197
|
+
window.addEventListener("message", this.handleMessage);
|
|
198
|
+
window.addEventListener("popstate", this.handlePopState);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
unmount() {
|
|
202
|
+
if (typeof window !== "undefined") {
|
|
203
|
+
window.removeEventListener("message", this.handleMessage);
|
|
204
|
+
window.removeEventListener("popstate", this.handlePopState);
|
|
205
|
+
}
|
|
206
|
+
this.stopObserving();
|
|
207
|
+
}
|
|
208
|
+
startObserving() {
|
|
209
|
+
if (typeof window === "undefined" || !this.iframeWindow)
|
|
210
|
+
return;
|
|
211
|
+
// Initial check
|
|
212
|
+
const currentUrl = window.location.href;
|
|
213
|
+
if (currentUrl !== this.lastUrl) {
|
|
214
|
+
this.lastUrl = currentUrl;
|
|
215
|
+
logger.debug("URL changed, sending page context");
|
|
216
|
+
this.sendPageContext();
|
|
217
|
+
}
|
|
218
|
+
if (this.observer)
|
|
219
|
+
return;
|
|
220
|
+
this.observer = new MutationObserver((mutations) => {
|
|
221
|
+
const hasSubstantialChanges = mutations.some((mutation) => mutation.type === "childList" &&
|
|
222
|
+
(mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0));
|
|
223
|
+
if (hasSubstantialChanges) {
|
|
224
|
+
this.debouncedSendContext();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
this.observer.observe(document.body, {
|
|
228
|
+
childList: true,
|
|
229
|
+
subtree: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
stopObserving() {
|
|
233
|
+
if (this.observer) {
|
|
234
|
+
this.observer.disconnect();
|
|
235
|
+
this.observer = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
handlePopState = () => {
|
|
239
|
+
logger.debug("Navigation detected, sending page context");
|
|
240
|
+
this.sendPageContext();
|
|
241
|
+
};
|
|
242
|
+
handleMessage = (event) => {
|
|
243
|
+
if (typeof window === "undefined")
|
|
244
|
+
return;
|
|
245
|
+
if (!this.isWidgetOpen && event.data?.type !== "yak:ready") {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const hostOrigin = window.location.origin;
|
|
249
|
+
const allowedOrigins = new Set();
|
|
250
|
+
allowedOrigins.add(this.getIframeOrigin());
|
|
251
|
+
if (hostOrigin) {
|
|
252
|
+
allowedOrigins.add(hostOrigin);
|
|
253
|
+
}
|
|
254
|
+
if (!allowedOrigins.has(event.origin)) {
|
|
255
|
+
if (!this.unexpectedOriginLogged) {
|
|
256
|
+
logger.warn(`Ignoring message from unexpected origin: ${event.origin}, allowed: ${Array.from(allowedOrigins).join(", ")}`);
|
|
257
|
+
this.unexpectedOriginLogged = true;
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Validate message structure
|
|
262
|
+
if (!event.data || typeof event.data !== "object" || !("type" in event.data)) {
|
|
263
|
+
// Filter out known browser extension messages
|
|
264
|
+
const data = event.data;
|
|
265
|
+
const isReactDevTools = data?.source === "react-devtools-content-script" ||
|
|
266
|
+
data?.source === "react-devtools-bridge" ||
|
|
267
|
+
data?.source === "react-devtools-inject-backend";
|
|
268
|
+
const isReduxDevTools = data?.source === "@devtools-page";
|
|
269
|
+
if (isReactDevTools || isReduxDevTools) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const message = event.data;
|
|
275
|
+
const targetWindow = (event.source && "postMessage" in event.source ? event.source : null) ??
|
|
276
|
+
this.iframeWindow;
|
|
277
|
+
const targetOrigin = this.getIframeOrigin();
|
|
278
|
+
logger.debug("Message received from iframe:", message.type);
|
|
279
|
+
switch (message.type) {
|
|
280
|
+
case "yak:ready": {
|
|
281
|
+
logger.debug("Iframe ready, sending config");
|
|
282
|
+
if (targetWindow) {
|
|
283
|
+
this.readyTarget = { window: targetWindow, origin: targetOrigin };
|
|
284
|
+
this.sendConfigToIframe(targetWindow, targetOrigin);
|
|
285
|
+
// Send initial page context after config
|
|
286
|
+
setTimeout(() => this.sendPageContext(), 100);
|
|
287
|
+
// Mark iframe as ready after config is sent
|
|
288
|
+
setTimeout(() => this.config.onReady?.(), 200);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
logger.warn("Unable to send config: iframe window not registered yet");
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "yak:tool_call": {
|
|
296
|
+
const { id, name, args } = message.payload;
|
|
297
|
+
void this.handleToolCall(id, name, args);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "yak:graphql_schema_call": {
|
|
301
|
+
const { id, schemaName, request } = message.payload;
|
|
302
|
+
void this.handleGraphQLSchemaCall(id, schemaName, request);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "yak:rest_schema_call": {
|
|
306
|
+
const { id, schemaName, request } = message.payload;
|
|
307
|
+
void this.handleRESTSchemaCall(id, schemaName, request);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "yak:redirect": {
|
|
311
|
+
const { path } = message.payload;
|
|
312
|
+
logger.debug("Redirect request received:", path);
|
|
313
|
+
if (!this.isAllowedRedirect(path)) {
|
|
314
|
+
logger.warn("Blocked potentially unsafe redirect:", path);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
if (this.config.onRedirect) {
|
|
318
|
+
this.config.onRedirect(path);
|
|
319
|
+
}
|
|
320
|
+
else if (typeof window !== "undefined") {
|
|
321
|
+
window.location.assign(path);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case "yak:close": {
|
|
326
|
+
logger.debug("Close message received from iframe");
|
|
327
|
+
this.config.onClose?.();
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
logger.debug("Unknown message type:", message.type);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
sendConfigToIframe(targetWindow, targetOrigin) {
|
|
336
|
+
const configMessage = {
|
|
337
|
+
type: "yak:config",
|
|
338
|
+
payload: {
|
|
339
|
+
version: EMBED_PROTOCOL_VERSION,
|
|
340
|
+
appId: this.config.appId,
|
|
341
|
+
theme: this.config.theme,
|
|
342
|
+
toolManifest: this.config.chatConfig?.tools ?? undefined,
|
|
343
|
+
routeManifest: this.config.chatConfig?.routes ?? undefined,
|
|
344
|
+
schemaSources: this.config.chatConfig?.schemaSources ?? undefined,
|
|
345
|
+
options: this.config.options,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
logger.debug("Posting config to iframe origin:", {
|
|
349
|
+
origin: targetOrigin,
|
|
350
|
+
version: EMBED_PROTOCOL_VERSION,
|
|
351
|
+
hasToolManifest: Boolean(this.config.chatConfig?.tools),
|
|
352
|
+
toolCount: this.config.chatConfig?.tools?.tools.length ?? 0,
|
|
353
|
+
hasRouteManifest: Boolean(this.config.chatConfig?.routes),
|
|
354
|
+
hasSchemaSources: Boolean(this.config.chatConfig?.schemaSources),
|
|
355
|
+
schemaCount: this.config.chatConfig?.schemaSources?.length ?? 0,
|
|
356
|
+
});
|
|
357
|
+
targetWindow.postMessage(configMessage, targetOrigin);
|
|
358
|
+
}
|
|
359
|
+
sendPageContext() {
|
|
360
|
+
if (!this.iframeWindow)
|
|
361
|
+
return;
|
|
362
|
+
try {
|
|
363
|
+
const pageContext = extractPageContext();
|
|
364
|
+
const message = {
|
|
365
|
+
type: "yak:page_context",
|
|
366
|
+
payload: pageContext,
|
|
367
|
+
};
|
|
368
|
+
logger.debug("Sending page context to iframe:", {
|
|
369
|
+
url: pageContext.url,
|
|
370
|
+
title: pageContext.title,
|
|
371
|
+
textLength: pageContext.text.length,
|
|
372
|
+
});
|
|
373
|
+
this.iframeWindow.postMessage(message, this.getIframeOrigin());
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
logger.error("Error extracting page context:", error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async handleToolCall(id, name, args) {
|
|
380
|
+
logger.debug(`Tool call received: ${name}`, { id, args });
|
|
381
|
+
if (!this.config.onToolCall) {
|
|
382
|
+
logger.error("Tool call received but no onToolCall handler configured");
|
|
383
|
+
this.sendToolResultToIframe(id, false, undefined, "No tool call handler configured");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const result = await this.config.onToolCall(name, args);
|
|
388
|
+
logger.debug(`Tool call succeeded: ${name}`, { id });
|
|
389
|
+
this.sendToolResultToIframe(id, true, result);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
logger.error(`Tool call failed: ${name}`, { id, error });
|
|
393
|
+
this.sendToolResultToIframe(id, false, undefined, this.extractErrorMessage(error));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async handleGraphQLSchemaCall(id, schemaName, request) {
|
|
397
|
+
logger.debug(`GraphQL schema call received: ${schemaName}`, { id, request });
|
|
398
|
+
if (!this.config.onGraphQLSchemaCall) {
|
|
399
|
+
logger.error("GraphQL schema call received but no onGraphQLSchemaCall handler configured");
|
|
400
|
+
this.sendToolResultToIframe(id, false, undefined, "No GraphQL schema handler configured");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const result = await this.config.onGraphQLSchemaCall(schemaName, request);
|
|
405
|
+
logger.debug(`GraphQL schema call succeeded: ${schemaName}`, { id });
|
|
406
|
+
this.sendToolResultToIframe(id, true, result);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
logger.error(`GraphQL schema call failed: ${schemaName}`, { id, error });
|
|
410
|
+
this.sendToolResultToIframe(id, false, undefined, this.extractErrorMessage(error));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async handleRESTSchemaCall(id, schemaName, request) {
|
|
414
|
+
logger.debug(`REST schema call received: ${schemaName}`, { id, request });
|
|
415
|
+
if (!this.config.onRESTSchemaCall) {
|
|
416
|
+
logger.error("REST schema call received but no onRESTSchemaCall handler configured");
|
|
417
|
+
this.sendToolResultToIframe(id, false, undefined, "No REST schema handler configured");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const result = await this.config.onRESTSchemaCall(schemaName, request);
|
|
422
|
+
logger.debug(`REST schema call succeeded: ${schemaName}`, { id });
|
|
423
|
+
this.sendToolResultToIframe(id, true, result);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
logger.error(`REST schema call failed: ${schemaName}`, { id, error });
|
|
427
|
+
this.sendToolResultToIframe(id, false, undefined, this.extractErrorMessage(error));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
sendToolResultToIframe(id, ok, result, error) {
|
|
431
|
+
if (!this.iframeWindow)
|
|
432
|
+
return;
|
|
433
|
+
if (ok) {
|
|
434
|
+
// Serialize result to remove non-cloneable values (functions, etc.)
|
|
435
|
+
// postMessage uses structured clone which cannot handle functions
|
|
436
|
+
const safeResult = this.toSerializable(result);
|
|
437
|
+
const message = {
|
|
438
|
+
type: "yak:tool_result",
|
|
439
|
+
payload: { id, ok: true, result: safeResult },
|
|
440
|
+
};
|
|
441
|
+
this.iframeWindow.postMessage(message, this.getIframeOrigin());
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const message = {
|
|
445
|
+
type: "yak:tool_result",
|
|
446
|
+
payload: { id, ok: false, error: error ?? "Unknown error" },
|
|
447
|
+
};
|
|
448
|
+
this.iframeWindow.postMessage(message, this.getIframeOrigin());
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Convert a value to a serializable form by stripping functions and other non-cloneable values.
|
|
453
|
+
* Uses JSON.parse(JSON.stringify()) which handles most cases.
|
|
454
|
+
*/
|
|
455
|
+
toSerializable(value) {
|
|
456
|
+
if (value === undefined || value === null) {
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return JSON.parse(JSON.stringify(value));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// If JSON serialization fails, return a string representation
|
|
464
|
+
logger.warn("Failed to serialize tool result, returning string representation");
|
|
465
|
+
return String(value);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
extractErrorMessage(error) {
|
|
469
|
+
if (error instanceof Error) {
|
|
470
|
+
return error.message;
|
|
471
|
+
}
|
|
472
|
+
if (typeof error === "string") {
|
|
473
|
+
return error;
|
|
474
|
+
}
|
|
475
|
+
return "Unknown error";
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Validates that a redirect path is safe (relative path or same-origin).
|
|
479
|
+
* Blocks absolute URLs to external domains to prevent open redirect attacks.
|
|
480
|
+
*/
|
|
481
|
+
isAllowedRedirect(path) {
|
|
482
|
+
// Allow relative paths that don't start with // (protocol-relative URLs)
|
|
483
|
+
if (path.startsWith('/') && !path.startsWith('//')) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
// Allow hash-only and query-only paths
|
|
487
|
+
if (path.startsWith('#') || path.startsWith('?')) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
// For absolute URLs, verify same origin
|
|
491
|
+
if (typeof window !== "undefined") {
|
|
492
|
+
try {
|
|
493
|
+
const url = new URL(path, window.location.origin);
|
|
494
|
+
return url.origin === window.location.origin;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Invalid URL - block it
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// In non-browser environments, only allow relative paths
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./types/config.js";
|
|
2
|
+
export * from "./types/messaging.js";
|
|
3
|
+
export * from "./types/routes.js";
|
|
4
|
+
export * from "./types/tools.js";
|
|
5
|
+
export { EMBED_PROTOCOL_VERSION } from "./version.js";
|
|
6
|
+
export type { EmbedProtocolVersion } from "./version.js";
|
|
7
|
+
export { YakClient } from "./client.js";
|
|
8
|
+
export type { YakClientConfig } from "./client.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Public types
|
|
2
|
+
export * from "./types/config.js";
|
|
3
|
+
export * from "./types/messaging.js";
|
|
4
|
+
export * from "./types/routes.js";
|
|
5
|
+
export * from "./types/tools.js";
|
|
6
|
+
// Version
|
|
7
|
+
export { EMBED_PROTOCOL_VERSION } from "./version.js";
|
|
8
|
+
// Public client API
|
|
9
|
+
export { YakClient } from "./client.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.server.d.ts","sourceRoot":"","sources":["../src/index.server.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./server/index.js";
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger utility for the yak client SDK.
|
|
3
|
+
* Debug/info logs are only emitted in development mode.
|
|
4
|
+
* Warnings and errors are always logged.
|
|
5
|
+
*/
|
|
6
|
+
export declare const logger: {
|
|
7
|
+
debug: (message: string, data?: unknown) => void;
|
|
8
|
+
info: (message: string, data?: unknown) => void;
|
|
9
|
+
warn: (message: string, data?: unknown) => void;
|
|
10
|
+
error: (message: string, data?: unknown) => void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,MAAM;qBACA,MAAM,SAAS,OAAO,KAAG,IAAI;oBAU9B,MAAM,SAAS,OAAO,KAAG,IAAI;oBAU7B,MAAM,SAAS,OAAO,KAAG,IAAI;qBAQ5B,MAAM,SAAS,OAAO,KAAG,IAAI;CAO/C,CAAC"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger utility for the yak client SDK.
|
|
3
|
+
* Debug/info logs are only emitted in development mode.
|
|
4
|
+
* Warnings and errors are always logged.
|
|
5
|
+
*/
|
|
6
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
|
|
7
|
+
export const logger = {
|
|
8
|
+
debug: (message, data) => {
|
|
9
|
+
if (isDev) {
|
|
10
|
+
if (data !== undefined) {
|
|
11
|
+
console.log(`[yak-chat-host] ${message}`, data);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(`[yak-chat-host] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
info: (message, data) => {
|
|
19
|
+
if (isDev) {
|
|
20
|
+
if (data !== undefined) {
|
|
21
|
+
console.info(`[yak-chat-host] ${message}`, data);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.info(`[yak-chat-host] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
warn: (message, data) => {
|
|
29
|
+
if (data !== undefined) {
|
|
30
|
+
console.warn(`[yak-chat-host] ${message}`, data);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.warn(`[yak-chat-host] ${message}`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
error: (message, data) => {
|
|
37
|
+
if (data !== undefined) {
|
|
38
|
+
console.error(`[yak-chat-host] ${message}`, data);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(`[yak-chat-host] ${message}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PageContext } from "./types/messaging.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract page context including URL, title, and visible text content
|
|
4
|
+
*/
|
|
5
|
+
export declare function extractPageContext(): PageContext;
|
|
6
|
+
/**
|
|
7
|
+
* Debounce function to limit how often a function is called
|
|
8
|
+
*/
|
|
9
|
+
export declare function debounce<T extends (...args: unknown[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void;
|
|
10
|
+
//# sourceMappingURL=page-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-context.d.ts","sourceRoot":"","sources":["../src/page-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AA2CxD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,WAAW,CAehD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,EAC7D,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAclC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract visible text content from the DOM, filtering out non-content elements
|
|
3
|
+
*/
|
|
4
|
+
function extractPageText() {
|
|
5
|
+
if (typeof document === "undefined")
|
|
6
|
+
return "";
|
|
7
|
+
// Clone the body to avoid modifying the actual DOM
|
|
8
|
+
const bodyClone = document.body.cloneNode(true);
|
|
9
|
+
// Remove script, style, noscript tags and hidden elements
|
|
10
|
+
const unwantedSelectors = [
|
|
11
|
+
"script",
|
|
12
|
+
"style",
|
|
13
|
+
"noscript",
|
|
14
|
+
"iframe",
|
|
15
|
+
"[style*='display: none']",
|
|
16
|
+
"[style*='display:none']",
|
|
17
|
+
"[hidden]",
|
|
18
|
+
".yak-chat-widget",
|
|
19
|
+
];
|
|
20
|
+
unwantedSelectors.forEach((selector) => {
|
|
21
|
+
const elements = bodyClone.querySelectorAll(selector);
|
|
22
|
+
elements.forEach((el) => el.remove());
|
|
23
|
+
});
|
|
24
|
+
// Get text content and clean it up
|
|
25
|
+
let text = bodyClone.textContent || bodyClone.innerText || "";
|
|
26
|
+
// Normalize whitespace: replace multiple spaces/newlines with single space
|
|
27
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
28
|
+
// Limit to reasonable size (100KB max)
|
|
29
|
+
const maxLength = 100000;
|
|
30
|
+
if (text.length > maxLength) {
|
|
31
|
+
text = text.substring(0, maxLength) + "... [truncated]";
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extract page context including URL, title, and visible text content
|
|
37
|
+
*/
|
|
38
|
+
export function extractPageContext() {
|
|
39
|
+
if (typeof window === "undefined") {
|
|
40
|
+
return {
|
|
41
|
+
url: "",
|
|
42
|
+
title: "",
|
|
43
|
+
text: "",
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
url: window.location.href,
|
|
49
|
+
title: document.title,
|
|
50
|
+
text: extractPageText(),
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Debounce function to limit how often a function is called
|
|
56
|
+
*/
|
|
57
|
+
export function debounce(func, wait) {
|
|
58
|
+
let timeout = null;
|
|
59
|
+
return function executedFunction(...args) {
|
|
60
|
+
const later = () => {
|
|
61
|
+
timeout = null;
|
|
62
|
+
func(...args);
|
|
63
|
+
};
|
|
64
|
+
if (timeout) {
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
}
|
|
67
|
+
timeout = setTimeout(later, wait);
|
|
68
|
+
};
|
|
69
|
+
}
|