Embedded Host Page
An intranet page that embeds the chat in an iframe, signs the person in with a token its own server signs, handles one host tool and listens to events. The chat page itself is EmbedChat; a Cognipeer Console portal serves it for you at /api-embed/portal/{key}.
1. The host server mints a token
The token is whatever your portal expects to verify (a signed identity claim for the signed-in person). The host only has to produce it on request and keep it short-lived. A sketch:
ts
// GET /api/cognipeer-token (your server, behind your own login)
app.get("/api/cognipeer-token", requireLogin, async (req, res) => {
const token = await signIdentityToken({ sub: req.user.id, email: req.user.email, exp: nowPlusMinutes(5) });
res.type("text/plain").send(token);
});1
2
3
4
5
2
3
4
5
How the portal verifies it (key, claims, lifetime) is configured in the portal; see its own embedding guide.
2. The host page
html
<iframe
id="cp"
title="Support assistant"
src="https://console.example.com/api-embed/portal/support?tenant=acme&origin=https%3A%2F%2Fapp.example.com"
allow="clipboard-write"
style="width: 420px; height: 640px; border: 0"
></iframe>
<script>
const CONSOLE = "https://console.example.com";
const frame = document.getElementById("cp");
async function getToken() {
const res = await fetch("/api/cognipeer-token", { credentials: "include" });
return res.text();
}
// The tools the portal declared for this page
const tools = {
open_ticket: async ({ id }) => {
location.hash = `#/tickets/${encodeURIComponent(id)}`;
return { opened: id };
},
};
const send = (message) => frame.contentWindow.postMessage({ v: 1, ...message }, CONSOLE);
window.addEventListener("message", async (event) => {
// Only the iframe, only the console's origin
if (event.origin !== CONSOLE || event.source !== frame.contentWindow) return;
const m = event.data;
if (!m || m.v !== 1) return;
switch (m.type) {
case "cp:ready":
case "cp:token_request": // reason: expiring | expired | rejected
send({ type: "cp:auth", token: await getToken() });
break;
case "cp:tool_call": {
const run = tools[m.name];
if (!run) return send({ type: "cp:tool_result", callId: m.callId, ok: false, error: "Unknown tool" });
try {
send({ type: "cp:tool_result", callId: m.callId, ok: true, output: await run(m.args) });
} catch (e) {
send({ type: "cp:tool_result", callId: m.callId, ok: false, error: String(e.message || e) });
}
break;
}
case "cp:event":
if (m.event === "message:completed") badge.textContent = "1";
if (m.event === "auth:error") console.warn("chat sign-in problem", m.data.code, m.data.reference);
if (m.event === "limit:reached") showToast("Usage limit reached");
break;
}
});
// Page context (unsigned: never use it for authorization) and a prefilled question
send({ type: "cp:context", context: { page: "tickets", selectedTicket: 1042 } });
document.getElementById("ask").onclick = () => send({ type: "cp:prefill", text: "Summarise this ticket", send: true });
// When the person logs out of your app
document.getElementById("logout").onclick = () => send({ type: "cp:logout" });
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

3. The chat page (if you serve it yourself)
tsx
// the page served in the iframe
import { EmbedChat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";
export default function EmbeddedSupport() {
return (
<EmbedChat
baseUrl="/api/portal/support"
allowedOrigins={["https://app.example.com"]}
clientTools={[
{ name: "open_ticket", description: "Open a ticket in the host app", timeoutSeconds: 30,
inputSchema: { type: "object", required: ["id"], properties: { id: { type: "number" } } } },
]}
eventContent="none"
showHistory={false}
theme="light"
/>
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Checks before going live
allowedOriginslists exactly the host origin (scheme and port included); the iframe'soriginquery parameter says the same.- The server sends
frame-ancestorsfor the allowed origins only. - The host answers
cp:readyand everycp:token_request, and mints tokens that live long enough to be exchanged. - Your tool handlers treat
argsas untrusted input. - You listen for
auth:errorand show thereferenceto people who need support.
Related
- Embedding the Chat
- EmbedChat component
- Embedding & limits (the server side)

