@yak-io/javascript 0.10.0 → 0.11.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server/sources.ts", "../src/server/createYakHandler.ts"],
4
+ "sourcesContent": ["import type { RouteInfo } from \"../types/routes.js\";\nimport type { ToolDefinition, ToolExecutor, ToolManifest } from \"../types/tools.js\";\n\nexport type RouteSource = {\n id?: string;\n getRoutes: () => Promise<RouteInfo[]>;\n};\n\ntype RouteSourceFn = () => Promise<RouteInfo[]>;\n\ntype RouteSourceInputItem = RouteSource | RouteSourceFn | RouteInfo[];\n\nexport type RouteSourceInput = RouteSourceInputItem | RouteSourceInputItem[];\n\nexport type ToolSource = {\n id?: string;\n getTools: () => Promise<ToolDefinition[]>;\n executeTool?: ToolExecutor;\n};\n\ntype ToolSourceFn = () => Promise<ToolDefinition[] | ToolManifest>;\n\ntype ToolSourceInputItem =\n | ToolSource\n | ToolSourceFn\n | ToolDefinition[]\n | ToolManifest\n | {\n getTools: ToolSourceFn;\n executeTool?: ToolExecutor;\n };\n\nexport type ToolSourceInput = ToolSourceInputItem | ToolSourceInputItem[];\n\nfunction isRouteSource(value: unknown): value is RouteSource {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n \"getRoutes\" in value &&\n typeof (value as RouteSource).getRoutes === \"function\"\n );\n}\n\nfunction isRouteInfoArray(value: unknown): value is RouteInfo[] {\n return Array.isArray(value) && value.every((route) => route && typeof route.path === \"string\");\n}\n\nfunction normalizeRouteSourceItem(item: RouteSourceInputItem, index: number): RouteSource {\n if (isRouteSource(item)) {\n return {\n id: item.id ?? `route-source-${index}`,\n getRoutes: item.getRoutes,\n };\n }\n\n if (typeof item === \"function\") {\n return {\n id: `route-fn-${index}`,\n getRoutes: item,\n };\n }\n\n if (isRouteInfoArray(item)) {\n return {\n id: `route-static-${index}`,\n getRoutes: async () => item,\n };\n }\n\n throw new Error(\"Unsupported route source input\");\n}\n\nexport function normalizeRouteSources(input: RouteSourceInput): RouteSource[] {\n if (Array.isArray(input)) {\n if (input.length === 0) {\n return [];\n }\n if (isRouteInfoArray(input)) {\n return [normalizeRouteSourceItem(input, 0)];\n }\n return (input as RouteSourceInputItem[]).map((item, index) =>\n normalizeRouteSourceItem(item, index)\n );\n }\n\n return [normalizeRouteSourceItem(input, 0)];\n}\n\nfunction isToolSource(value: unknown): value is ToolSource {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n \"getTools\" in value &&\n typeof (value as ToolSource).getTools === \"function\"\n );\n}\n\nfunction isToolManifest(value: unknown): value is ToolManifest {\n return Boolean(value && typeof value === \"object\" && \"tools\" in value);\n}\n\nfunction isToolDefinitionArray(value: unknown): value is ToolDefinition[] {\n return Array.isArray(value) && value.every((tool) => tool && typeof tool.name === \"string\");\n}\n\nfunction toToolDefinitions(result: unknown): ToolDefinition[] {\n if (isToolDefinitionArray(result)) {\n return result;\n }\n if (isToolManifest(result)) {\n return result.tools;\n }\n throw new Error(\"Tool source must resolve to ToolDefinition[] or ToolManifest\");\n}\n\nfunction normalizeToolSourceItem(item: ToolSourceInputItem, index: number): ToolSource {\n if (isToolSource(item)) {\n return {\n id: item.id ?? `tool-source-${index}`,\n getTools: async () => {\n const result = await item.getTools();\n return toToolDefinitions(result);\n },\n executeTool: item.executeTool,\n };\n }\n\n if (typeof item === \"function\") {\n return {\n id: `tool-fn-${index}`,\n getTools: async () => {\n const result = await item();\n return toToolDefinitions(result);\n },\n };\n }\n\n if (isToolDefinitionArray(item)) {\n return {\n id: `tool-static-${index}`,\n getTools: async () => item,\n };\n }\n\n if (isToolManifest(item)) {\n return {\n id: `tool-manifest-${index}`,\n getTools: async () => item.tools,\n };\n }\n\n if (\n item &&\n typeof item === \"object\" &&\n \"getTools\" in item &&\n typeof item.getTools === \"function\"\n ) {\n return {\n id: (item as { id?: string }).id ?? `tool-wrapper-${index}`,\n getTools: async () => {\n const result = await item.getTools();\n return toToolDefinitions(result);\n },\n executeTool: item.executeTool,\n };\n }\n\n throw new Error(\"Unsupported tool source input\");\n}\n\nexport function normalizeToolSources(input?: ToolSourceInput): ToolSource[] {\n if (!input) {\n return [];\n }\n if (Array.isArray(input)) {\n if (input.length === 0) {\n return [];\n }\n if (isToolDefinitionArray(input)) {\n return [normalizeToolSourceItem(input, 0)];\n }\n return (input as ToolSourceInputItem[]).map((item, index) =>\n normalizeToolSourceItem(item, index)\n );\n }\n\n return [normalizeToolSourceItem(input, 0)];\n}\n", "import type { ChatConfig } from \"../types/config.js\";\nimport type { RouteInfo, RouteManifest } from \"../types/routes.js\";\nimport type { ToolCallResult, ToolDefinition, ToolManifest } from \"../types/tools.js\";\nimport type { RouteSource, RouteSourceInput, ToolSource, ToolSourceInput } from \"./sources.js\";\nimport { normalizeRouteSources, normalizeToolSources } from \"./sources.js\";\n\nexport type YakHandlerConfig = {\n routes: RouteSourceInput;\n tools?: ToolSourceInput;\n};\n\nexport function createYakHandler(config: YakHandlerConfig) {\n const routeSources = normalizeRouteSources(config.routes);\n const toolSources = normalizeToolSources(config.tools);\n\n const GET = async function handleConfig(_req: Request): Promise<Response> {\n const chatConfig: ChatConfig = {\n routes: await buildRouteManifest(routeSources),\n };\n\n if (toolSources.length > 0) {\n const { manifest } = await buildToolRegistry(toolSources);\n chatConfig.tools = manifest;\n }\n\n return jsonResponse(chatConfig, 200, {\n \"Cache-Control\": \"no-store\",\n });\n };\n\n const POST = async function handleToolCall(req: Request): Promise<Response> {\n if (toolSources.length === 0) {\n return errorResponse(\"Tool execution is not configured\", 501);\n }\n\n let payload: unknown;\n\n try {\n payload = await req.json();\n } catch {\n return errorResponse(\"Invalid JSON payload\", 400);\n }\n\n if (!isHostToolCallPayload(payload)) {\n return errorResponse(\"Invalid tool call payload\", 400);\n }\n\n try {\n const { lookup } = await buildToolRegistry(toolSources);\n const owner = lookup.get(payload.name);\n\n if (!owner) {\n return createToolErrorResponse(`Tool '${payload.name}' is not registered`);\n }\n\n if (!owner.executeTool) {\n return createToolErrorResponse(`Tool '${payload.name}' does not expose an executor`);\n }\n\n const result = await owner.executeTool(payload.name, payload.args, req);\n\n const successResult: ToolCallResult = {\n ok: true,\n result,\n };\n\n return jsonResponse(successResult);\n } catch (error) {\n return createToolErrorResponse(extractErrorMessage(error));\n }\n };\n\n return { GET, POST };\n}\n\nexport type YakConfigHandlerConfig = {\n routes: RouteSourceInput;\n tools?: ToolSourceInput;\n};\n\nexport function createYakConfigHandler(config: YakConfigHandlerConfig) {\n const routeSources = normalizeRouteSources(config.routes);\n const toolSources = normalizeToolSources(config.tools);\n\n return async function handleConfig(_req: Request): Promise<Response> {\n const chatConfig: ChatConfig = {\n routes: await buildRouteManifest(routeSources),\n };\n\n if (toolSources.length > 0) {\n const { manifest } = await buildToolRegistry(toolSources);\n chatConfig.tools = manifest;\n }\n\n return jsonResponse(chatConfig, 200, {\n \"Cache-Control\": \"no-store\",\n });\n };\n}\n\nexport type YakToolsHandlerConfig = {\n tools: ToolSourceInput;\n};\n\nexport function createYakToolsHandler(config: YakToolsHandlerConfig) {\n const toolSources = normalizeToolSources(config.tools);\n\n if (toolSources.length === 0) {\n throw new Error(\"createYakToolsHandler requires at least one tool source\");\n }\n\n return async function handleTools(req: Request): Promise<Response> {\n let payload: unknown;\n\n try {\n payload = await req.json();\n } catch {\n return errorResponse(\"Invalid JSON payload\", 400);\n }\n\n if (!isHostToolCallPayload(payload)) {\n return errorResponse(\"Invalid tool call payload\", 400);\n }\n\n try {\n const { lookup } = await buildToolRegistry(toolSources);\n const owner = lookup.get(payload.name);\n\n if (!owner) {\n return createToolErrorResponse(`Tool '${payload.name}' is not registered`);\n }\n\n if (!owner.executeTool) {\n return createToolErrorResponse(`Tool '${payload.name}' does not expose an executor`);\n }\n\n const result = await owner.executeTool(payload.name, payload.args, req);\n const successResult: ToolCallResult = {\n ok: true,\n result,\n };\n\n return jsonResponse(successResult);\n } catch (error) {\n return createToolErrorResponse(extractErrorMessage(error));\n }\n };\n}\n\nasync function buildRouteManifest(routeSources: RouteSource[]): Promise<RouteManifest> {\n const entries = await Promise.all(\n routeSources.map(async (source) => ({\n id: source.id ?? \"routes\",\n routes: await source.getRoutes(),\n }))\n );\n\n const merged: RouteInfo[] = [];\n const seen = new Set<string>();\n\n for (const entry of entries) {\n for (const route of entry.routes) {\n const key = route.path;\n if (!seen.has(key)) {\n seen.add(key);\n merged.push(route);\n }\n }\n }\n\n merged.sort((a, b) => a.path.localeCompare(b.path));\n\n return {\n routes: merged,\n generated_at: new Date().toISOString(),\n sources: entries.map((entry) => ({ id: entry.id, count: entry.routes.length })),\n };\n}\n\ntype ToolRegistry = {\n manifest: ToolManifest;\n lookup: Map<string, ToolSource>;\n};\n\nasync function buildToolRegistry(toolSources: ToolSource[]): Promise<ToolRegistry> {\n const entries = await Promise.all(\n toolSources.map(async (source) => ({\n id: source.id ?? \"tools\",\n source,\n tools: await source.getTools(),\n }))\n );\n\n const manifestTools: ToolDefinition[] = [];\n const lookup = new Map<string, ToolSource>();\n\n for (const entry of entries) {\n for (const tool of entry.tools) {\n manifestTools.push(tool);\n lookup.set(tool.name, entry.source);\n }\n }\n\n return {\n manifest: {\n tools: manifestTools,\n generated_at: new Date().toISOString(),\n sources: entries.map((entry) => ({ id: entry.id, count: entry.tools.length })),\n },\n lookup,\n };\n}\n\n/**\n * Payload received from the host (id is optional since YakClient handles correlation)\n */\ntype HostToolCallPayload = {\n name: string;\n args: unknown;\n};\n\nfunction isHostToolCallPayload(payload: unknown): payload is HostToolCallPayload {\n return (\n typeof payload === \"object\" &&\n payload !== null &&\n typeof (payload as HostToolCallPayload).name === \"string\" &&\n \"args\" in payload\n );\n}\n\nfunction extractErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n return \"An unknown error occurred\";\n}\n\nfunction jsonResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n });\n}\n\nfunction errorResponse(message: string, status: number): Response {\n return jsonResponse({ error: message }, status);\n}\n\nfunction createToolErrorResponse(error: string): Response {\n const errorResult: ToolCallResult = {\n ok: false,\n error,\n };\n\n return jsonResponse(errorResult);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,10 +1,21 @@
1
1
  /**
2
- * Generates a tool ID from a tool name using a 32-bit hash.
3
- * Format: `yt_<8-char-hex-hash>`.
2
+ * Convert a host tool name into a model-facing function name.
4
3
  *
5
- * The hash is deterministic so chat and voice always derive the same id for
6
- * the same tool name this is what lets the mint route, the iframe, and the
7
- * SDK all agree on which decorated id maps back to which host tool name.
4
+ * We sanitise to the allowed function-name charset (rather than hashing to an opaque
5
+ * `yt_<hex>`), so the model keeps the semantic signal of a readable name e.g.
6
+ * `orders.list` `orders_list`.
7
+ *
8
+ * This is a pure per-name transform; uniqueness across a manifest (and avoiding the
9
+ * reserved `redirect` name / `mcp__` namespace) is handled by the caller via
10
+ * {@link uniqueToolId}. Each runtime (the chat-ui iframe and this SDK's voice session)
11
+ * decorates and reverse-maps with its own manifest, so the ids never need to match
12
+ * across paths — only to be self-consistent within one.
8
13
  */
9
14
  export declare function generateToolId(originalName: string): string;
15
+ /**
16
+ * Resolve a collision-free, model-safe id for a host tool name, given the ids already
17
+ * taken in this manifest (seed `used` with reserved names like `redirect`). Host ids must
18
+ * never start with the `mcp__` namespace, which the dispatcher routes on.
19
+ */
20
+ export declare function uniqueToolId(originalName: string, used: Set<string>): string;
10
21
  //# sourceMappingURL=tool-name.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-name.d.ts","sourceRoot":"","sources":["../src/tool-name.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG3D"}
1
+ {"version":3,"file":"tool-name.d.ts","sourceRoot":"","sources":["../src/tool-name.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG3D;AAKD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAc5E"}
@@ -81,11 +81,11 @@ export declare class YakVoiceSession {
81
81
  private execMcpTool;
82
82
  private mintToken;
83
83
  /**
84
- * Decorate the host's tool manifest with hashed ids and populate
85
- * `this.toolNameById` for reverse lookup. Mirrors the decoration the chat-ui
86
- * iframe applies before sending tools to `/api/chat`. GraphQL/REST tools are
87
- * ordinary manifest entries here (contributed by their adapters), so no
88
- * special-casing is needed.
84
+ * Decorate the host's tool manifest with readable, collision-free model-facing ids
85
+ * and populate `this.toolNameById` for reverse lookup. Mirrors the decoration the
86
+ * chat-ui iframe applies before sending tools to `/api/chat`. GraphQL/REST tools are
87
+ * ordinary manifest entries here (contributed by their adapters), so no special-casing
88
+ * is needed.
89
89
  */
90
90
  private buildDecoratedManifest;
91
91
  private exchangeSdp;
@@ -1 +1 @@
1
- {"version":3,"file":"voice-session.d.ts","sourceRoot":"","sources":["../src/voice-session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC;CACF;AAiBD,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;AAEjE,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,eAAe,CAA6B;IACpD,6EAA6E;IAC7E,OAAO,CAAC,KAAK,CAAkC;IAC/C;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAA6B;gBAErC,MAAM,EAAE,qBAAqB;IAKzC;;;;OAIG;IACH,OAAO,KAAK,SAAS,GAEpB;IAED,0DAA0D;IACnD,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,IAAI;IAIzD,QAAQ,IAAI,YAAY;IAI/B;;;OAGG;IACI,YAAY,IAAI,MAAM;IAItB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAO9D;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwJnC,oDAAoD;IACvC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,2FAA2F;IACpF,OAAO,IAAI,IAAI;IAWtB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,mBAAmB;YAeb,aAAa;IA8B3B;;;;OAIG;YACW,WAAW;YAwBX,SAAS;IAsBvB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;YAchB,WAAW;IAmBzB,OAAO,CAAC,kBAAkB;YAWZ,gBAAgB;YA2BhB,QAAQ;YAuCR,QAAQ;IAMtB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,cAAc;CAevB"}
1
+ {"version":3,"file":"voice-session.d.ts","sourceRoot":"","sources":["../src/voice-session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC;CACF;AAiBD,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;AAEjE,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,iBAAiB,CAAqB;IAC9C,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,eAAe,CAA6B;IACpD,6EAA6E;IAC7E,OAAO,CAAC,KAAK,CAAkC;IAC/C;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAA6B;gBAErC,MAAM,EAAE,qBAAqB;IAKzC;;;;OAIG;IACH,OAAO,KAAK,SAAS,GAEpB;IAED,0DAA0D;IACnD,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,GAAG,IAAI;IAIzD,QAAQ,IAAI,YAAY;IAI/B;;;OAGG;IACI,YAAY,IAAI,MAAM;IAItB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAO9D;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwJnC,oDAAoD;IACvC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,2FAA2F;IACpF,OAAO,IAAI,IAAI;IAWtB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,mBAAmB;YAeb,aAAa;IA8B3B;;;;OAIG;YACW,WAAW;YA2BX,SAAS;IAsBvB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;YAgBhB,WAAW;IAmBzB,OAAO,CAAC,kBAAkB;YAWZ,gBAAgB;YA2BhB,QAAQ;YAuCR,QAAQ;IAMtB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,cAAc;CAevB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yak-io/javascript",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Core JavaScript SDK for embedding yak chatbot",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -30,18 +30,20 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "sideEffects": false,
33
- "main": "./dist/index.js",
33
+ "main": "./dist/index.cjs",
34
34
  "module": "./dist/index.js",
35
35
  "types": "./dist/index.d.ts",
36
36
  "exports": {
37
37
  ".": {
38
38
  "types": "./dist/index.d.ts",
39
39
  "import": "./dist/index.js",
40
+ "require": "./dist/index.cjs",
40
41
  "default": "./dist/index.js"
41
42
  },
42
43
  "./server": {
43
44
  "types": "./dist/index.server.d.ts",
44
45
  "import": "./dist/index.server.js",
46
+ "require": "./dist/index.server.cjs",
45
47
  "default": "./dist/index.server.js"
46
48
  },
47
49
  "./package.json": "./package.json"
@@ -65,7 +67,7 @@
65
67
  },
66
68
  "homepage": "https://docs.yak.io/docs/sdks/javascript",
67
69
  "scripts": {
68
- "build": "tsc",
70
+ "build": "node ../../scripts/build-package.mjs && tsc --emitDeclarationOnly",
69
71
  "check-types": "tsc --noEmit",
70
72
  "test": "vitest run",
71
73
  "lint": "biome lint ./src --fix",
package/dist/client.js DELETED
@@ -1,524 +0,0 @@
1
- import { isYakLoggingEnabled, logger } from "./logger.js";
2
- import { debounce, extractPageContext } from "./page-context.js";
3
- import { EMBED_PROTOCOL_VERSION } from "./version.js";
4
- /** localStorage key for the per-app signed session token. */
5
- const SESSION_STORAGE_KEY = (appId) => `yak:session:${appId}`;
6
- /** localStorage key for the per-app active-conversation pointer. */
7
- const CONVERSATION_POINTER_KEY = (appId) => `yak:conversation:${appId}`;
8
- /** Read a stored session token for this app, if any. SSR-safe. */
9
- function readStoredSessionToken(appId) {
10
- if (typeof window === "undefined" || typeof window.localStorage === "undefined")
11
- return undefined;
12
- try {
13
- return window.localStorage.getItem(SESSION_STORAGE_KEY(appId)) ?? undefined;
14
- }
15
- catch {
16
- return undefined;
17
- }
18
- }
19
- function writeStoredSessionToken(appId, token) {
20
- if (typeof window === "undefined" || typeof window.localStorage === "undefined")
21
- return;
22
- try {
23
- window.localStorage.setItem(SESSION_STORAGE_KEY(appId), token);
24
- }
25
- catch {
26
- // localStorage can throw in private-browsing modes; silently ignore.
27
- }
28
- }
29
- function readStoredConversationPointer(appId) {
30
- if (typeof window === "undefined" || typeof window.localStorage === "undefined")
31
- return undefined;
32
- try {
33
- return window.localStorage.getItem(CONVERSATION_POINTER_KEY(appId)) ?? undefined;
34
- }
35
- catch {
36
- return undefined;
37
- }
38
- }
39
- function writeStoredConversationPointer(appId, pointer) {
40
- if (typeof window === "undefined" || typeof window.localStorage === "undefined")
41
- return;
42
- try {
43
- window.localStorage.setItem(CONVERSATION_POINTER_KEY(appId), pointer);
44
- }
45
- catch {
46
- // localStorage can throw in private-browsing modes; silently ignore.
47
- }
48
- }
49
- function clearStoredConversationPointer(appId) {
50
- if (typeof window === "undefined" || typeof window.localStorage === "undefined")
51
- return;
52
- try {
53
- window.localStorage.removeItem(CONVERSATION_POINTER_KEY(appId));
54
- }
55
- catch {
56
- // localStorage can throw in private-browsing modes; silently ignore.
57
- }
58
- }
59
- /** Production chat origin — where the widget iframe loads from by default. */
60
- const DEFAULT_CHAT_ORIGIN = "https://chat.yak.io";
61
- /**
62
- * Resolves the iframe origin when no explicit `origin` is configured. Points at
63
- * a local chat UI during yak's own local development; production otherwise.
64
- * Integrators override the result with the `origin` config option.
65
- */
66
- function getDefaultIframeOrigin() {
67
- if (typeof window !== "undefined" &&
68
- (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") &&
69
- typeof window.__YAK_INTERNAL_DEV__ !== "undefined") {
70
- return "http://localhost:3001";
71
- }
72
- return DEFAULT_CHAT_ORIGIN;
73
- }
74
- export class YakClient {
75
- config;
76
- iframeWindow = null;
77
- isWidgetOpen = false;
78
- readyTarget = null;
79
- unexpectedOriginLogged = false;
80
- lastUrl = " ";
81
- debouncedSendContext;
82
- observer = null;
83
- constructor(config) {
84
- this.config = config;
85
- this.debouncedSendContext = debounce(() => {
86
- logger.debug("DOM mutation detected, sending page context");
87
- this.sendPageContext();
88
- }, 2000);
89
- }
90
- updateConfig(newConfig) {
91
- this.config = { ...this.config, ...newConfig };
92
- // Resend config when the iframe is ready and we have anything to deliver —
93
- // tool/route manifests, or a user identity that needs to land in the
94
- // widget so persistence/history light up.
95
- if (this.readyTarget && (this.config.chatConfig || this.config.user)) {
96
- this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);
97
- }
98
- }
99
- /**
100
- * Get the iframe origin URL (base URL for the chat widget). Recomputed on
101
- * each call so environment-dependent defaults resolve correctly.
102
- */
103
- getIframeOrigin() {
104
- return this.config.origin ?? getDefaultIframeOrigin();
105
- }
106
- /**
107
- * Get the full iframe embed URL for the chatbot
108
- *
109
- * @example
110
- * ```ts
111
- * const client = new YakClient({ appId: "my-app" });
112
- * const iframeSrc = client.getEmbedUrl();
113
- * // Returns: "https://chat.yak.io/embed/v1/my-app"
114
- * ```
115
- */
116
- getEmbedUrl() {
117
- const origin = this.getIframeOrigin();
118
- const baseUrl = `${origin}/embed/v${EMBED_PROTOCOL_VERSION}/${encodeURIComponent(this.config.appId)}`;
119
- const params = new URLSearchParams();
120
- const theme = this.config.theme;
121
- if (theme?.colorMode && theme.colorMode !== "system") {
122
- params.set("colorMode", theme.colorMode);
123
- }
124
- this.appendThemeColors(params, theme?.light, "light");
125
- this.appendThemeColors(params, theme?.dark, "dark");
126
- // Pass debug flag to iframe so it can enable logging immediately
127
- if (isYakLoggingEnabled()) {
128
- params.set("yakDebug", "1");
129
- }
130
- const queryString = params.toString();
131
- return queryString ? `${baseUrl}?${queryString}` : baseUrl;
132
- }
133
- /**
134
- * Append theme color parameters to a URLSearchParams object
135
- */
136
- appendThemeColors(params, colors, prefix) {
137
- if (!colors)
138
- return;
139
- const map = [
140
- [`${prefix}Bg`, colors.background],
141
- [`${prefix}Border`, colors.border],
142
- [`${prefix}MessageBg`, colors.messageBackground],
143
- [`${prefix}Placeholder`, colors.placeholderColor],
144
- [`${prefix}SubmitBtn`, colors.submitButtonColor],
145
- [`${prefix}SubmitBtnText`, colors.submitButtonTextColor],
146
- [`${prefix}HeaderIcon`, colors.headerIconColor],
147
- ];
148
- for (const [key, value] of map) {
149
- if (value)
150
- params.set(key, value);
151
- }
152
- }
153
- /**
154
- * Get the app ID
155
- */
156
- getAppId() {
157
- return this.config.appId;
158
- }
159
- /**
160
- * Get the current theme configuration
161
- */
162
- getTheme() {
163
- return this.config.theme;
164
- }
165
- /**
166
- * Send a prompt message to the chatbot iframe
167
- * Note: The iframe must be ready to receive messages (onReady callback must have fired)
168
- *
169
- * @example
170
- * ```ts
171
- * const client = new YakClient({ appId: "my-app", onReady: () => {
172
- * client.sendPrompt("Help me with my order");
173
- * }});
174
- * ```
175
- */
176
- sendPrompt(prompt) {
177
- if (!this.iframeWindow) {
178
- logger.warn("Cannot send prompt: iframe not ready");
179
- return;
180
- }
181
- const message = {
182
- type: "yak:prompt",
183
- payload: { prompt },
184
- };
185
- this.iframeWindow.postMessage(message, this.getIframeOrigin());
186
- }
187
- /**
188
- * Send a focus request to the chatbot iframe
189
- * This will focus the chat input field
190
- */
191
- sendFocus() {
192
- if (!this.iframeWindow) {
193
- logger.warn("Cannot send focus: iframe not ready");
194
- return;
195
- }
196
- const message = {
197
- type: "yak:focus",
198
- };
199
- this.iframeWindow.postMessage(message, this.getIframeOrigin());
200
- }
201
- /**
202
- * Check if the iframe is ready to receive messages
203
- */
204
- isReady() {
205
- return this.readyTarget !== null;
206
- }
207
- setIframeWindow(window) {
208
- this.iframeWindow = window;
209
- if (!window) {
210
- this.readyTarget = null;
211
- }
212
- }
213
- setWidgetOpen(isOpen) {
214
- this.isWidgetOpen = isOpen;
215
- if (isOpen && this.readyTarget && this.config.chatConfig) {
216
- this.sendConfigToIframe(this.readyTarget.window, this.readyTarget.origin);
217
- }
218
- if (isOpen) {
219
- this.startObserving();
220
- }
221
- else {
222
- this.stopObserving();
223
- }
224
- }
225
- mount() {
226
- if (typeof window !== "undefined") {
227
- window.addEventListener("message", this.handleMessage);
228
- window.addEventListener("popstate", this.handlePopState);
229
- }
230
- }
231
- unmount() {
232
- if (typeof window !== "undefined") {
233
- window.removeEventListener("message", this.handleMessage);
234
- window.removeEventListener("popstate", this.handlePopState);
235
- }
236
- this.stopObserving();
237
- }
238
- startObserving() {
239
- if (typeof window === "undefined" || !this.iframeWindow)
240
- return;
241
- // Initial check
242
- const currentUrl = window.location.href;
243
- if (currentUrl !== this.lastUrl) {
244
- this.lastUrl = currentUrl;
245
- logger.debug("URL changed, sending page context");
246
- this.sendPageContext();
247
- }
248
- if (this.observer)
249
- return;
250
- this.observer = new MutationObserver((mutations) => {
251
- const hasSubstantialChanges = mutations.some((mutation) => mutation.type === "childList" &&
252
- (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0));
253
- if (hasSubstantialChanges) {
254
- this.debouncedSendContext();
255
- }
256
- });
257
- this.observer.observe(document.body, {
258
- childList: true,
259
- subtree: true,
260
- });
261
- }
262
- stopObserving() {
263
- if (this.observer) {
264
- this.observer.disconnect();
265
- this.observer = null;
266
- }
267
- }
268
- handlePopState = () => {
269
- logger.debug("Navigation detected, sending page context");
270
- this.sendPageContext();
271
- };
272
- handleMessage = (event) => {
273
- if (typeof window === "undefined")
274
- return;
275
- if (!this.isWidgetOpen && event.data?.type !== "yak:ready") {
276
- return;
277
- }
278
- const hostOrigin = window.location.origin;
279
- const allowedOrigins = new Set();
280
- allowedOrigins.add(this.getIframeOrigin());
281
- if (hostOrigin) {
282
- allowedOrigins.add(hostOrigin);
283
- }
284
- if (!allowedOrigins.has(event.origin)) {
285
- if (!this.unexpectedOriginLogged) {
286
- logger.warn(`Ignoring message from unexpected origin: ${event.origin}, allowed: ${Array.from(allowedOrigins).join(", ")}`);
287
- this.unexpectedOriginLogged = true;
288
- }
289
- return;
290
- }
291
- // Validate message structure
292
- if (!event.data || typeof event.data !== "object" || !("type" in event.data)) {
293
- // Filter out known browser extension messages
294
- const data = event.data;
295
- const isReactDevTools = data?.source === "react-devtools-content-script" ||
296
- data?.source === "react-devtools-bridge" ||
297
- data?.source === "react-devtools-inject-backend";
298
- const isReduxDevTools = data?.source === "@devtools-page";
299
- if (isReactDevTools || isReduxDevTools) {
300
- return;
301
- }
302
- return;
303
- }
304
- const message = event.data;
305
- const targetWindow = (event.source && "postMessage" in event.source ? event.source : null) ??
306
- this.iframeWindow;
307
- const targetOrigin = this.getIframeOrigin();
308
- logger.debug("Message received from iframe:", message.type);
309
- switch (message.type) {
310
- case "yak:ready": {
311
- logger.debug("Iframe ready, sending config");
312
- if (targetWindow) {
313
- this.readyTarget = { window: targetWindow, origin: targetOrigin };
314
- this.sendConfigToIframe(targetWindow, targetOrigin);
315
- // Send initial page context after config
316
- setTimeout(() => this.sendPageContext(), 100);
317
- // Mark iframe as ready after config is sent
318
- setTimeout(() => this.config.onReady?.(), 200);
319
- }
320
- else {
321
- logger.warn("Unable to send config: iframe window not registered yet");
322
- }
323
- break;
324
- }
325
- case "yak:tool_call": {
326
- const { id, name, args } = message.payload;
327
- void this.handleToolCall(id, name, args);
328
- break;
329
- }
330
- case "yak:redirect": {
331
- const { path } = message.payload;
332
- logger.debug("Redirect request received:", path);
333
- if (!this.isAllowedRedirect(path)) {
334
- logger.warn("Blocked potentially unsafe redirect:", path);
335
- break;
336
- }
337
- if (this.config.onRedirect) {
338
- this.config.onRedirect(path);
339
- }
340
- else if (typeof window !== "undefined") {
341
- window.location.assign(path);
342
- }
343
- break;
344
- }
345
- case "yak:session": {
346
- const { sessionToken } = message.payload;
347
- logger.debug("Session token received from iframe; persisting");
348
- writeStoredSessionToken(this.config.appId, sessionToken);
349
- break;
350
- }
351
- case "yak:conversation": {
352
- const { pointer } = message.payload;
353
- if (pointer === null) {
354
- logger.debug("Conversation pointer cleared by iframe");
355
- clearStoredConversationPointer(this.config.appId);
356
- }
357
- else {
358
- logger.debug("Conversation pointer received from iframe; persisting");
359
- writeStoredConversationPointer(this.config.appId, pointer);
360
- }
361
- break;
362
- }
363
- case "yak:close": {
364
- logger.debug("Close message received from iframe");
365
- this.config.onClose?.();
366
- break;
367
- }
368
- default:
369
- logger.debug("Unknown message type:", message.type);
370
- break;
371
- }
372
- };
373
- sendConfigToIframe(targetWindow, targetOrigin) {
374
- // Get logging enabled state from window flag
375
- const loggingEnabled = typeof window !== "undefined" && typeof window.__YAK_LOGGING_ENABLED__ === "boolean"
376
- ? window.__YAK_LOGGING_ENABLED__
377
- : undefined;
378
- const storedSessionToken = readStoredSessionToken(this.config.appId);
379
- const storedConversationPointer = readStoredConversationPointer(this.config.appId);
380
- const configMessage = {
381
- type: "yak:config",
382
- payload: {
383
- version: EMBED_PROTOCOL_VERSION,
384
- appId: this.config.appId,
385
- theme: this.config.theme,
386
- toolManifest: this.config.chatConfig?.tools ?? undefined,
387
- routeManifest: this.config.chatConfig?.routes ?? undefined,
388
- options: this.config.options,
389
- loggingEnabled,
390
- user: this.config.user,
391
- sessionToken: storedSessionToken,
392
- conversationPointer: storedConversationPointer,
393
- },
394
- };
395
- logger.debug("Posting config to iframe origin:", {
396
- origin: targetOrigin,
397
- version: EMBED_PROTOCOL_VERSION,
398
- hasToolManifest: Boolean(this.config.chatConfig?.tools),
399
- toolCount: this.config.chatConfig?.tools?.tools.length ?? 0,
400
- hasRouteManifest: Boolean(this.config.chatConfig?.routes),
401
- });
402
- targetWindow.postMessage(configMessage, targetOrigin);
403
- }
404
- sendPageContext() {
405
- if (!this.iframeWindow)
406
- return;
407
- try {
408
- const pageContext = extractPageContext();
409
- const message = {
410
- type: "yak:page_context",
411
- payload: pageContext,
412
- };
413
- logger.debug("Sending page context to iframe:", {
414
- url: pageContext.url,
415
- title: pageContext.title,
416
- textLength: pageContext.text.length,
417
- });
418
- this.iframeWindow.postMessage(message, this.getIframeOrigin());
419
- }
420
- catch (error) {
421
- logger.error("Error extracting page context:", error);
422
- }
423
- }
424
- async handleToolCall(id, name, args) {
425
- logger.debug(`Tool call received: ${name}`, { id, args });
426
- if (!this.config.onToolCall) {
427
- logger.error("Tool call received but no onToolCall handler configured");
428
- this.sendToolResultToIframe(id, false, undefined, "No tool call handler configured");
429
- this.config.onToolCallComplete?.({
430
- name,
431
- args,
432
- ok: false,
433
- error: "No tool call handler configured",
434
- });
435
- return;
436
- }
437
- try {
438
- const result = await this.config.onToolCall(name, args);
439
- logger.debug(`Tool call succeeded: ${name}`, { id });
440
- this.sendToolResultToIframe(id, true, result);
441
- this.config.onToolCallComplete?.({ name, args, ok: true, result });
442
- }
443
- catch (error) {
444
- const errorMessage = this.extractErrorMessage(error);
445
- logger.error(`Tool call failed: ${name}`, { id, error });
446
- this.sendToolResultToIframe(id, false, undefined, errorMessage);
447
- this.config.onToolCallComplete?.({ name, args, ok: false, error: errorMessage });
448
- }
449
- }
450
- sendToolResultToIframe(id, ok, result, error) {
451
- if (!this.iframeWindow)
452
- return;
453
- if (ok) {
454
- // Serialize result to remove non-cloneable values (functions, etc.)
455
- // postMessage uses structured clone which cannot handle functions
456
- const safeResult = this.toSerializable(result);
457
- const message = {
458
- type: "yak:tool_result",
459
- payload: { id, ok: true, result: safeResult },
460
- };
461
- this.iframeWindow.postMessage(message, this.getIframeOrigin());
462
- }
463
- else {
464
- const message = {
465
- type: "yak:tool_result",
466
- payload: { id, ok: false, error: error ?? "Unknown error" },
467
- };
468
- this.iframeWindow.postMessage(message, this.getIframeOrigin());
469
- }
470
- }
471
- /**
472
- * Convert a value to a serializable form by stripping functions and other non-cloneable values.
473
- * Uses JSON.parse(JSON.stringify()) which handles most cases.
474
- */
475
- toSerializable(value) {
476
- if (value === undefined || value === null) {
477
- return value;
478
- }
479
- try {
480
- return JSON.parse(JSON.stringify(value));
481
- }
482
- catch {
483
- // If JSON serialization fails, return a string representation
484
- logger.warn("Failed to serialize tool result, returning string representation");
485
- return String(value);
486
- }
487
- }
488
- extractErrorMessage(error) {
489
- if (error instanceof Error) {
490
- return error.message;
491
- }
492
- if (typeof error === "string") {
493
- return error;
494
- }
495
- return "Unknown error";
496
- }
497
- /**
498
- * Validates that a redirect path is safe (relative path or same-origin).
499
- * Blocks absolute URLs to external domains to prevent open redirect attacks.
500
- */
501
- isAllowedRedirect(path) {
502
- // Allow relative paths that don't start with // (protocol-relative URLs)
503
- if (path.startsWith("/") && !path.startsWith("//")) {
504
- return true;
505
- }
506
- // Allow hash-only and query-only paths
507
- if (path.startsWith("#") || path.startsWith("?")) {
508
- return true;
509
- }
510
- // For absolute URLs, verify same origin
511
- if (typeof window !== "undefined") {
512
- try {
513
- const url = new URL(path, window.location.origin);
514
- return url.origin === window.location.origin;
515
- }
516
- catch {
517
- // Invalid URL - block it
518
- return false;
519
- }
520
- }
521
- // In non-browser environments, only allow relative paths
522
- return false;
523
- }
524
- }