@frontera-sdk/chat 1.52.43 → 1.52.44

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": "@frontera-sdk/chat",
3
- "version": "1.52.43",
3
+ "version": "1.52.44",
4
4
  "description": "Typed client and React hooks for chatting with a Frontera agent from your own application.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -50,6 +50,10 @@
50
50
  "types": "./src/token-session.ts",
51
51
  "import": "./src/token-session.ts"
52
52
  },
53
+ "./public-token": {
54
+ "types": "./src/public-token.ts",
55
+ "import": "./src/public-token.ts"
56
+ },
53
57
  "./callback-page": {
54
58
  "types": "./src/callback-page.ts",
55
59
  "import": "./src/callback-page.ts"
@@ -77,7 +81,7 @@
77
81
  "smoke": "bun run scripts/smoke.ts"
78
82
  },
79
83
  "dependencies": {
80
- "@frontera-sdk/core": "1.52.43"
84
+ "@frontera-sdk/core": "1.52.44"
81
85
  },
82
86
  "peerDependencies": {
83
87
  "@ai-sdk/react": "^3.0.0",
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Start a chat for an anonymous visitor of a public website, from the browser.
3
+ *
4
+ * A public website key (`fpk_…`) is made to sit in page JavaScript: it reaches
5
+ * one Agent, only from the origins listed on it, and only to mint a short-lived
6
+ * token that chats and does nothing else. So, unlike the server exchange in
7
+ * `auth.ts`, this runs where the visitor is and needs no backend of your own.
8
+ *
9
+ * The platform names the visitor, not the page. What comes back with the first
10
+ * token is a visitor pass; this keeps it in `localStorage` and shows it at the
11
+ * next mint, so a returning visitor gets their conversations back. Storage can
12
+ * be unavailable (a private window, blocked site data) — the chat then still
13
+ * works, and each page load is simply a new visitor.
14
+ */
15
+
16
+ import { errorFromResponse } from '@frontera-sdk/core/errors'
17
+
18
+ import type { ChatToken } from './types'
19
+
20
+ export interface PublicChatTokenOptions {
21
+ /** Your deployment's API origin, for example `https://api.acme.example`. */
22
+ apiBaseUrl: string
23
+ /** The Agent's public website key, `fpk_…`. */
24
+ publishableKey: string
25
+ /** Test seam. Defaults to the global. */
26
+ fetchImpl?: typeof fetch
27
+ /** Test seam. Defaults to `localStorage`, when there is one. */
28
+ storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | null
29
+ }
30
+
31
+ /** One pass per key, so two Agents on one site never share a visitor. */
32
+ export function visitorPassStorageKey(publishableKey: string): string {
33
+ return `frontera:visitor:${publishableKey.slice(0, 12)}`
34
+ }
35
+
36
+ function defaultStorage(): PublicChatTokenOptions['storage'] {
37
+ try {
38
+ return typeof localStorage === 'undefined' ? null : localStorage
39
+ } catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ function read(storage: PublicChatTokenOptions['storage'], key: string): string | null {
45
+ try {
46
+ return storage?.getItem(key) ?? null
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ function write(storage: PublicChatTokenOptions['storage'], key: string, value: string): void {
53
+ try {
54
+ storage?.setItem(key, value)
55
+ } catch {
56
+ // Full or blocked: the next visit is a new visitor, nothing more.
57
+ }
58
+ }
59
+
60
+ export async function mintPublicChatToken(options: PublicChatTokenOptions): Promise<ChatToken> {
61
+ const storage = options.storage === undefined ? defaultStorage() : options.storage
62
+ const passKey = visitorPassStorageKey(options.publishableKey)
63
+ const visitorPass = read(storage, passKey)
64
+
65
+ const response = await (options.fetchImpl ?? fetch)(`${options.apiBaseUrl}/v1/chat-tokens/public`, {
66
+ method: 'POST',
67
+ headers: { 'content-type': 'application/json' },
68
+ // Never `include`: this call carries the key, not anyone's cookies.
69
+ credentials: 'omit',
70
+ body: JSON.stringify({ publishableKey: options.publishableKey, ...(visitorPass ? { visitorPass } : {}) }),
71
+ })
72
+ const body = (await response.json().catch(() => null)) as
73
+ | { data?: { token: string; expiresAt: string; visitorPass?: string } }
74
+ | null
75
+ if (!response.ok || !body?.data) throw errorFromResponse(response.status, body)
76
+
77
+ if (body.data.visitorPass) write(storage, passKey, body.data.visitorPass)
78
+ return { token: body.data.token, expiresAt: body.data.expiresAt }
79
+ }