dewee-webchat 0.3.0 → 0.4.1-beta.2
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/README.md +237 -22
- package/dist/{chunk-ZTVGWFXL.js → chunk-2NRPUV6T.js} +580 -42
- package/dist/{chunk-W6A4HVFB.js → chunk-PUIN5ZCQ.js} +52 -7
- package/dist/dewee-webchat.js +36 -29
- package/dist/handler-DunrbnSD.d.ts +307 -0
- package/dist/index.d.ts +91 -7
- package/dist/index.js +2 -2
- package/dist/protocol-CLbGE9Ns.d.ts +170 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/server-identity.d.ts +6 -0
- package/dist/server-identity.js +6 -0
- package/dist/server.d.ts +33 -75
- package/dist/server.js +1507 -41
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +2 -2
- package/package.json +13 -1
- package/dist/protocol-B72Ry-fY.d.ts +0 -92
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# dewee-webchat
|
|
2
2
|
|
|
3
3
|
Put your [Dewee](https://dewee.sh) agent on any website. A dependency-free chat widget that streams the
|
|
4
|
-
agent's **reasoning** and **tool calls
|
|
5
|
-
off the browser.
|
|
4
|
+
agent's answers (and, when you allow it, its **reasoning** and **tool calls**), plus a small backend
|
|
5
|
+
handler that keeps your integration secret off the browser.
|
|
6
6
|
|
|
7
7
|
**Playground:** [webchat.dewee.sh](https://webchat.dewee.sh) · **CDN:** [webchat-cdn.dewee.sh](https://webchat-cdn.dewee.sh) ·
|
|
8
8
|
**Source & examples:** [github.com/nextlevelbuilder/dewee-webchat](https://github.com/nextlevelbuilder/dewee-webchat)
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
browser <dewee-chat> ──HTTPS + SSE──▶ your backend /api/dewee/* ──
|
|
12
|
-
(visitor token only) (holds the
|
|
11
|
+
browser <dewee-chat> ──HTTPS + SSE──▶ your backend /api/dewee/* ──HTTPS + SSE──▶ Dewee CCP (Website chat)
|
|
12
|
+
(visitor token only) (holds the integration secret) (agents, sign-in, limits)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## 1. Add the backend
|
|
@@ -47,17 +47,43 @@ PHP? Use the single-file backend in [`php/`](https://github.com/nextlevelbuilder
|
|
|
47
47
|
|
|
48
48
|
### Environment
|
|
49
49
|
|
|
50
|
+
Create a **Website chat** integration in CCP (the Dewee control plane): choose its agents, allowed
|
|
51
|
+
sites, sign-in mode, limits and whether reasoning and tool activity are shown. Its page prints the
|
|
52
|
+
first three values below.
|
|
53
|
+
|
|
54
|
+
| Variable | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `DEWEE_CCP_CHAT_URL` | The ingress URL, `https://<ccp>/api/public/webchat/v1/wci_…`. https only (plain http to `localhost` for development). |
|
|
57
|
+
| `DEWEE_WEBCHAT_INTEGRATION_ID` | `wci_…`; must match the end of `DEWEE_CCP_CHAT_URL` |
|
|
58
|
+
| `DEWEE_WEBCHAT_INTEGRATION_SECRET` | `dwc_…`. **Server only.** Sent only to the `DEWEE_CCP_CHAT_URL` origin, never across a redirect. |
|
|
59
|
+
| `DEWEE_WEBCHAT_SECRET` | 32+ random chars to sign visitor sessions (`openssl rand -base64 32`) |
|
|
60
|
+
| `DEWEE_ALLOWED_ORIGINS` | Extra origins for cross-origin embedding (same origin always works) |
|
|
61
|
+
| `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` | Optional Cloudflare Turnstile check for new visitors (with `ccp`, a returning visitor's hourly refresh is not challenged) |
|
|
62
|
+
| `DEWEE_WEBCHAT_TRANSPORT` | Optional: `ccp` or `legacy-gateway`. Without it, the variables that are set decide (gateway variables alone select `legacy-gateway`, with a deprecation warning); set it to pin the transport explicitly. |
|
|
63
|
+
|
|
64
|
+
The same values as options: `createDeweeHandler({ transport: "ccp", ccp: { chatUrl, integrationId, integrationSecret }, secret })`.
|
|
65
|
+
With `ccp`, the agents come from the integration: `/config` lists them, the widget's `agent` is
|
|
66
|
+
forwarded and CCP decides whether it is allowed. Errors from CCP reach the browser as the usual
|
|
67
|
+
`{ error: { code, message } }` with this package's own messages; CCP's text, URLs and secrets never do.
|
|
68
|
+
|
|
69
|
+
#### Legacy: direct Gateway (deprecated)
|
|
70
|
+
|
|
71
|
+
`transport: "legacy-gateway"` connects straight to a Dewee Gateway with a URL and token. It still
|
|
72
|
+
works and logs a deprecation warning once; see
|
|
73
|
+
[docs/migrating-to-ccp-transport.md](https://github.com/nextlevelbuilder/dewee-webchat/blob/main/docs/migrating-to-ccp-transport.md).
|
|
74
|
+
|
|
50
75
|
| Variable | |
|
|
51
76
|
|---|---|
|
|
52
77
|
| `DEWEE_GATEWAY_URL` | Gateway runtime origin, e.g. `https://gateway.example.com` |
|
|
53
78
|
| `DEWEE_GATEWAY_TOKEN` | Operator API key (`operator.read,operator.write`) or gateway token. **Server only.** |
|
|
54
|
-
| `
|
|
79
|
+
| `DEWEE_TENANT_ID` | Optional tenant UUID |
|
|
55
80
|
| `DEWEE_AGENTS` | Allowlist, comma separated (`support,sales`) or `*` |
|
|
56
81
|
| `DEWEE_DEFAULT_AGENT` | Optional; defaults to the first allowed agent |
|
|
57
|
-
| `
|
|
58
|
-
| `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` | Optional Cloudflare Turnstile check for new visitors |
|
|
82
|
+
| `DEWEE_ALLOW_CLIENT_CREDENTIALS`, `DEWEE_CLIENT_CREDENTIAL_HOSTS` | "Bring your own gateway" (playgrounds only) |
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
The two sets cannot be mixed: with CCP and gateway variables both set, or `transport` naming one and
|
|
85
|
+
the other's variables set, the handler refuses to start rather than send a credential to an origin
|
|
86
|
+
you did not pick. `agents`, `defaultAgent` and `allowClientCredentials` are refused with `ccp`.
|
|
61
87
|
|
|
62
88
|
## 2. Add the widget
|
|
63
89
|
|
|
@@ -119,8 +145,8 @@ Attributes use kebab-case (`show-reasoning="false"`), `data-*` on the script tag
|
|
|
119
145
|
| `theme` | `light` | `light`, `dark`, `auto` |
|
|
120
146
|
| `accentColor` | `#5446e8` | Any CSS color |
|
|
121
147
|
| `locale` | page language | `en`, `vi` |
|
|
122
|
-
| `showReasoning` | `true` | Stream the model's thinking |
|
|
123
|
-
| `showToolCalls` | `true` | Show tool calls with arguments and results |
|
|
148
|
+
| `showReasoning` | `true` | Stream the model's thinking (with `ccp`, only when the integration shows it) |
|
|
149
|
+
| `showToolCalls` | `true` | Show tool calls with arguments and results (with `ccp`: names only, when the integration shows them) |
|
|
124
150
|
| `allowAgentSwitch` | `true` | Agent picker when the backend allows several |
|
|
125
151
|
| `persist` | `true` | Keep the conversation across page loads |
|
|
126
152
|
| `open`, `hideBranding`, `width`, `height`, `zIndex` | | |
|
|
@@ -131,8 +157,9 @@ Attributes use kebab-case (`show-reasoning="false"`), `data-*` on the script tag
|
|
|
131
157
|
| `fetch` | `globalThis.fetch` | Custom fetch for every widget request (JS/props only) |
|
|
132
158
|
|
|
133
159
|
Methods: `open()`, `close()`, `toggle()`, `send(text)`, `stop()`, `newConversation()`, `configure(partial)`,
|
|
134
|
-
`registerActions(actions)`, `unregisterAction(name)
|
|
135
|
-
|
|
160
|
+
`registerActions(actions)`, `unregisterAction(name)`, and for [signed-in users](#signed-in-users)
|
|
161
|
+
`renewSession()` and `startLink()`.
|
|
162
|
+
Events: `dewee:ready`, `dewee:open`, `dewee:close`, `dewee:send`, `dewee:reply`, `dewee:error`, `dewee:action`, `dewee:link`.
|
|
136
163
|
CSS variables: `--dw-accent`, `--dw-font`, `--dw-radius`, `--dw-width`, `--dw-height`, `--dw-z`.
|
|
137
164
|
|
|
138
165
|
## Custom headers and fetch
|
|
@@ -148,13 +175,21 @@ Behind a same-origin BFF that wants a CSRF header, cookies or tracing:
|
|
|
148
175
|
```
|
|
149
176
|
|
|
150
177
|
Vanilla: `chat.configure({ headers, fetch })` or the `chat.headers` / `chat.fetch` properties. `headers` applies to
|
|
151
|
-
every request to `endpoint` (config, session, history, chat). It cannot override `content-type`,
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
every request to `endpoint` (config, session, history, link, chat). It cannot override `content-type`,
|
|
179
|
+
`authorization` (which carries the anonymous or signed-in session token), `x-dewee-gateway-url` or
|
|
180
|
+
`x-dewee-gateway-token` (case-insensitive; such keys are dropped). Header names are sent lower-cased. For a
|
|
181
|
+
cross-origin `endpoint`, the backend's CORS preflight must allow the extra headers; the built-in handlers only
|
|
182
|
+
allow their own. A custom `fetch` receives every request as sent, including the session bearer token of a
|
|
183
|
+
signed-in user, so only the trusted host page should supply it.
|
|
184
|
+
|
|
185
|
+
Anonymous visitor sessions are reused until less than 10% of the token lifetime remains (at most 24 h, at least
|
|
186
|
+
30 s), so backends with short session TTLs do not mint a new session on every request. With `transport: "ccp"`,
|
|
187
|
+
`/session` reports the end of the CCP edge session sealed in the token as `expiresAt` (and `expiresIn`), so the
|
|
188
|
+
widget asks `/session` again before it lapses, about once an hour, although the token itself lives 30 days. That
|
|
189
|
+
refresh keeps the visitor id and re-seals a refreshed edge session, so a host that keeps no memory between
|
|
190
|
+
requests (serverless, PHP) never refreshes or opens one while answering a chat. Signed-in sessions are used from
|
|
191
|
+
memory and refreshed `min(60 s, lifetime / 2)` before they expire (see [signed-in users](#signed-in-users) for the
|
|
192
|
+
tab's reuse hint).
|
|
158
193
|
|
|
159
194
|
## Agent page actions
|
|
160
195
|
|
|
@@ -218,13 +253,193 @@ or inserts it as HTML, caps calls per answer and automatic turns per message, an
|
|
|
218
253
|
`mutate`/`destructive` handler without approval. Page context and results are labelled as data for the
|
|
219
254
|
agent, but prompt injection through page content is still possible — do not put secrets in either.
|
|
220
255
|
|
|
256
|
+
## Signed-in users
|
|
257
|
+
|
|
258
|
+
When your site knows who the visitor is, the agent can know it too, with private tools that act for
|
|
259
|
+
that user only. You pass one server-side callback. You do not mint tokens, store per-visitor
|
|
260
|
+
credentials or put anything secret in the page: the handler exchanges your user for a Dewee identity
|
|
261
|
+
session, and Dewee derives the user from it.
|
|
262
|
+
|
|
263
|
+
With `transport: "ccp"` the same `identity` option works through CCP: the handler signs the partner
|
|
264
|
+
assertion with `@dewee/identity` and CCP exchanges it for the identity session (`identity/exchange`),
|
|
265
|
+
so the website needs no gateway key. `identity.agent` is optional there (the integration default is
|
|
266
|
+
used), `allowInsecureHttp` does not apply, a user switch under `rebindEachRequest` revokes the
|
|
267
|
+
previous user's session at CCP, and logout revokes it through [`handler.revoke`](#signed-in-users).
|
|
268
|
+
The integration's sign-in mode must allow signed-in users.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createDeweeHandler, optionsFromEnv } from "dewee-webchat/server";
|
|
272
|
+
import { deweeIdentitySdk } from "dewee-webchat/server/identity";
|
|
273
|
+
|
|
274
|
+
const handler = createDeweeHandler({
|
|
275
|
+
...optionsFromEnv(process.env),
|
|
276
|
+
identity: {
|
|
277
|
+
sdk: deweeIdentitySdk,
|
|
278
|
+
// Your own login. Return null for a visitor who is not signed in.
|
|
279
|
+
authenticate: async (req) => {
|
|
280
|
+
const user = await getUserFromSessionCookie(req.headers.get("cookie"));
|
|
281
|
+
return user ? { subject: user.id } : null; // your stable id, never an email address
|
|
282
|
+
},
|
|
283
|
+
rebindEachRequest: true, // recommended with a cookie login, see below
|
|
284
|
+
appId: process.env.DEWEE_IDENTITY_APP_ID!,
|
|
285
|
+
issuer: process.env.DEWEE_IDENTITY_ISSUER!,
|
|
286
|
+
assertionSecret: process.env.DEWEE_IDENTITY_ASSERTION_SECRET!, // or privateKeyJwk for a jwks application
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Setup: create the application once with `dewee identity setup --name <site> --issuer <issuer>` (it
|
|
292
|
+
prints the application id and the assertion secret), and install the server SDK next to this package:
|
|
293
|
+
`npm install @dewee/identity`. It is an optional peer dependency used only by the
|
|
294
|
+
`dewee-webchat/server/identity` entry; `dewee-webchat/server` itself never imports it, so a site that
|
|
295
|
+
serves only anonymous visitors bundles fine (webpack, esbuild…) without it.
|
|
296
|
+
`@dewee/identity` is not on npm yet: until it is, build its tarball from the
|
|
297
|
+
[dewee repository](https://github.com/nextlevelbuilder/dewee/blob/dev/packages/dewee-identity/README.md)
|
|
298
|
+
(`pnpm --filter @dewee/identity pack`) and install that file.
|
|
299
|
+
|
|
300
|
+
| `identity` option | |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `sdk` | Required. `deweeIdentitySdk` from `dewee-webchat/server/identity`. |
|
|
303
|
+
| `authenticate(req)` | Required. Resolves `{ subject }` for a signed-in user, `null` otherwise. Throwing means "could not check". |
|
|
304
|
+
| `appId`, `issuer` | Required. The Dewee application id and its issuer, exactly. |
|
|
305
|
+
| `assertionSecret` \| `privateKeyJwk` (+ `algorithm`) | Exactly one: `hmac` or `jwks` application. Server only. |
|
|
306
|
+
| `audience` | Optional check; must be `dewee:<appId>`. |
|
|
307
|
+
| `agent` | The agent sessions are bound to. Default: `defaultAgent`, else the first of `agents` (with `ccp`: the integration default). |
|
|
308
|
+
| `required` | Default `false`. When `true`, visitors who are not signed in get no session at all. |
|
|
309
|
+
| `rebindEachRequest` | Default `false`. Check the signed-in user again on every `/chat`, `/history` and `/link`. |
|
|
310
|
+
| `sessionTtlSeconds` | 60 to 43200. Default: the gateway default (3600). |
|
|
311
|
+
| `allowInsecureHttp` | Legacy gateway only: allow plain http to a gateway that is not loopback (private networks only). |
|
|
312
|
+
|
|
313
|
+
`AuthenticatedUser` has no scopes: the session exchange does not carry any. When a user's permissions
|
|
314
|
+
change, report it from your server with `lifecycle({ event: "scopes_changed", … })` from
|
|
315
|
+
`@dewee/identity`.
|
|
316
|
+
|
|
317
|
+
How it behaves:
|
|
318
|
+
|
|
319
|
+
- `POST /session` calls `authenticate`. For a user it returns a visitor token that carries a Dewee
|
|
320
|
+
identity session, encrypted (AES-256-GCM, key derived from `secret`) so the browser only holds an
|
|
321
|
+
opaque value. The gateway token, the assertion secret, the private key, the raw identity session and
|
|
322
|
+
your subject never reach the browser.
|
|
323
|
+
- One identity session per user is kept in memory and reused until it nears expiry, so a page load
|
|
324
|
+
does not cost an exchange; a session the gateway reports expired or revoked is dropped.
|
|
325
|
+
- With CCP, a host that keeps no memory between requests reuses it too. `/session` also returns a
|
|
326
|
+
`reuseHint`: the same sealed session in a token that is no bearer (`/chat`, `/history` and `/link`
|
|
327
|
+
refuse it). The widget keeps only that hint in the tab's `sessionStorage`, never the bearer token,
|
|
328
|
+
and sends it back as `reuseHint`. The handler reuses its identity and edge sessions only when it was
|
|
329
|
+
issued by this backend for the user `authenticate` returns now and the same agent, with more than 2
|
|
330
|
+
minutes left. It is never extended past its own expiry; anything else is exchanged anew, and it
|
|
331
|
+
never makes a signed-out visitor signed in. The legacy gateway and PHP send no hint, and the widget
|
|
332
|
+
then stores nothing.
|
|
333
|
+
- The hint lives in one tab, so on a host that keeps no memory between requests each new tab of a
|
|
334
|
+
signed-in user costs one identity exchange, and CCP allows 10 exchanges per visitor per 10 minutes.
|
|
335
|
+
Sites where users open many tabs should run the handler where its in-memory session cache survives
|
|
336
|
+
between requests (a long-lived process or reused instance), which serves every tab of the user.
|
|
337
|
+
- `/chat` and `/history` connect to the gateway with that `identity_session` and no `user_id`, and only
|
|
338
|
+
go on when the gateway confirms it derived a Dewee user (`tuc:…`); a gateway that ignored the session
|
|
339
|
+
(identity switched off) gets `503 SOURCE_UNAVAILABLE` and nothing runs. The same user gets the same
|
|
340
|
+
conversations on every device. The session's agent must still be in `agents` on every request.
|
|
341
|
+
- By default the visitor token is the credential: a token for user A keeps chatting as A even if a
|
|
342
|
+
later request carries B's cookie, and an altered token is refused (401). With
|
|
343
|
+
`rebindEachRequest: true`, `/chat`, `/history` and `/link` call `authenticate(req)` again and compare
|
|
344
|
+
it with a keyed digest of the subject sealed in the token: another user or no user is
|
|
345
|
+
`401 IDENTITY_REQUIRED` (the widget then asks `/session` again), and a throwing `authenticate` is
|
|
346
|
+
`503 SOURCE_UNAVAILABLE`. Turn it on when your login is a cookie: a widget token replayed under
|
|
347
|
+
another user's cookie is then useless. With CCP, another user also revokes the previous user's
|
|
348
|
+
session at CCP. That is the old user's one shared session, so it ends on their other devices too;
|
|
349
|
+
their widget gets a `401` there and exchanges a new one at `/session`. A request with no user
|
|
350
|
+
(signed out, or sent without the cookie) is only refused, because the session may still be in use
|
|
351
|
+
on the user's other devices.
|
|
352
|
+
- Logout: call `revoke(subject, request, hint?)` on the handler from your logout route. It forgets
|
|
353
|
+
the identity sessions the handler holds for that user and, with CCP, revokes them at CCP
|
|
354
|
+
(`identity/revoke`), so the user's tokens stop working on every device. On a host without shared
|
|
355
|
+
memory, have the logout page send the widget's hint (`chat.reuseHint()`) along: the session in it
|
|
356
|
+
is revoked when it belongs to that subject. Pass the logout request so CCP sees the visitor's
|
|
357
|
+
origin (an integration with an origin allowlist needs it; without a request, the origin the
|
|
358
|
+
session was issued for is used). Sessions held only by other instances end when they expire. The
|
|
359
|
+
legacy gateway has no revoke call, so there it only forgets them.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// `handler` from the example above; the logout page posts { hint: chat.reuseHint() }
|
|
363
|
+
export async function logout(req: Request) {
|
|
364
|
+
const user = await getUserFromSessionCookie(req.headers.get("cookie"));
|
|
365
|
+
const { hint } = await req.clone().json().catch(() => ({}));
|
|
366
|
+
if (user) await handler.revoke(user.id, req, typeof hint === "string" ? hint : undefined); // the subject authenticate returns
|
|
367
|
+
// …then clear your own session cookie
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
- Fail closed. If `authenticate` returns `null`, the visitor chats anonymously, exactly as without
|
|
371
|
+
`identity`, unless `required` is set, in which case no session is issued. If it throws, the answer
|
|
372
|
+
is `503 SOURCE_UNAVAILABLE` and no session is issued. If the exchange for a signed-in user fails, no
|
|
373
|
+
session is issued: a user is never silently downgraded. An exchange refusal meant for the operator
|
|
374
|
+
(an application or agent set up wrong) is `500 identity_misconfigured`, with the reason in your logs.
|
|
375
|
+
- What the identity session allows, until it expires or is revoked: chatting with the bound agent and
|
|
376
|
+
reading that chat's history as the user, starting web-first links, and listing and removing the
|
|
377
|
+
user's own linked chat accounts (`/v1/identity/me/bindings`). That is why it never reaches the
|
|
378
|
+
browser in the clear.
|
|
379
|
+
- `authenticate` sees the request the widget sends. A widget on another origin (`allowedOrigins`)
|
|
380
|
+
sends no cookies of your site, so a cookie-based `authenticate` only works same-origin.
|
|
381
|
+
- Legacy gateway only: `DEWEE_GATEWAY_TOKEN` must be a tenant API key without an owner: the gateway refuses an identity
|
|
382
|
+
session next to a key bound to a user (`IDENTITY_CONFLICT`).
|
|
383
|
+
- `identity` cannot be combined with `allowClientCredentials` (the handler refuses to start). Sessions
|
|
384
|
+
are bound to one agent, so `/config` turns the agent picker off.
|
|
385
|
+
- Gateway identity errors (`IDENTITY_REQUIRED`, `IDENTITY_LINK_REQUIRED`, `LINK_EXPIRED`, `LINK_REVOKED`,
|
|
386
|
+
`PERMISSION_DENIED`, `SOURCE_UNAVAILABLE`, `IDENTITY_CONFLICT`) reach the widget as
|
|
387
|
+
`{ error: { code, remediation } }`. The widget shows its own localized message and, on `LINK_EXPIRED`
|
|
388
|
+
or `LINK_REVOKED`, asks `/session` again.
|
|
389
|
+
- The widget uses a signed-in session from memory, so each page load asks your backend who the
|
|
390
|
+
visitor is. When `/session` returns a `reuseHint` (CCP only), it keeps that hint, and nothing
|
|
391
|
+
else, in `sessionStorage` (never `localStorage`), and drops it when `/session` answers anonymously
|
|
392
|
+
or `401`, or a signed-in request is refused. It refreshes the session `min(60 s, lifetime/2)` before it ends, counting the lifetime
|
|
393
|
+
(`expiresIn`) from when it received it. Call `chat.renewSession()` after a sign-in or sign-out
|
|
394
|
+
without a page load.
|
|
395
|
+
- Rate limits (default, in memory): 120 `authenticate` calls per IP per minute (checked before
|
|
396
|
+
`authenticate` runs), 30 signed-in `/session` calls per user per 10 minutes, 5 link starts per user per
|
|
397
|
+
10 minutes. A visitor who is not signed in and still has a valid anonymous token gets it back as is;
|
|
398
|
+
only minting a new one counts against the 10 per IP per 10 minutes. With CCP, refreshing a
|
|
399
|
+
returning visitor's token counts per visitor instead, so visitors behind one address do not use up
|
|
400
|
+
the budget of new ones. CCP's own per-IP budget still applies to edge session creates: a visitor
|
|
401
|
+
back after more than a day (past CCP's refresh window) gets a new edge session, which counts there. A custom `rateLimit` receives the
|
|
402
|
+
buckets `preauth`, `verified` and `link` besides `session` and `chat`.
|
|
403
|
+
- Keep `authenticate` cheap or cached (a signed-cookie check, not a database round trip per call): it
|
|
404
|
+
runs on every `/session`, so on every page load, and with `rebindEachRequest` on every `/chat`,
|
|
405
|
+
`/history` and `/link`, with only the generous per-IP cap in front of it.
|
|
406
|
+
|
|
407
|
+
**Linking a chat app.** A signed-in user can link their Telegram, Discord or other chat account
|
|
408
|
+
without typing any id: call `chat.startLink()`. The widget asks `POST {endpoint}/link`, which starts
|
|
409
|
+
a Dewee web-first link, and shows the proof code with "Continue on Telegram" for each chat account
|
|
410
|
+
the code can be sent to. The user sends the code in a direct message; Dewee checks both sides and
|
|
411
|
+
links on its own. `dewee:link` fires with `{ expiresAt, deepLinks }`. A link that is already in
|
|
412
|
+
progress answers `409 link_in_progress`, a reused idempotency key `409 idempotency_key_reused`, and a
|
|
413
|
+
gateway with linking switched off `503 linking_unavailable`; the widget shows its own text for each.
|
|
414
|
+
|
|
415
|
+
The PHP backend does not support signed-in users (it has no JWS signing), with either transport; use
|
|
416
|
+
the Node/Workers handler. It refuses tokens that carry an identity session.
|
|
417
|
+
|
|
221
418
|
## Security model
|
|
222
419
|
|
|
223
|
-
- The gateway token never leaves your server. Browsers get an
|
|
224
|
-
-
|
|
225
|
-
-
|
|
420
|
+
- The integration secret (or, legacy, the gateway token) never leaves your server. Browsers get an
|
|
421
|
+
HMAC-signed visitor id; the CCP edge session travels inside it encrypted (AES-256-GCM).
|
|
422
|
+
- The integration secret is sent only to the `DEWEE_CCP_CHAT_URL` origin: redirects are refused, not
|
|
423
|
+
followed, and the URL must be https (plain http only to `localhost`).
|
|
424
|
+
- Each visitor gets its own visitor identity at Dewee (legacy: its own gateway user `webchat-<id>` and
|
|
425
|
+
session keys), so history is private to them. A [signed-in user](#signed-in-users) is the user Dewee
|
|
426
|
+
derives from their identity session.
|
|
427
|
+
- With `ccp`, reasoning and tool activity are hidden unless the integration shows them, and tool
|
|
428
|
+
arguments and results never reach the browser.
|
|
429
|
+
- Only allowed agents are reachable (the integration's, legacy: `agents`); origin allowlist, rate limits (20 messages/min per visitor, 10
|
|
226
430
|
new sessions/10 min per IP by default — pass `rateLimit` for a shared store), 4000-char messages,
|
|
227
431
|
optional Turnstile.
|
|
432
|
+
- The Dewee gateway enforces the shared admission limits on every message. A refusal reaches the widget
|
|
433
|
+
as a typed SSE error (`POLICY_DENIED`, or `RATE_LIMITED` with `retryAfterMs`) with a fixed message, never
|
|
434
|
+
the gateway's own text. The widget shows a localized error and disables sending until `retryAfterMs`
|
|
435
|
+
has passed, announced to screen readers. With `ccp`, CCP's refusals of a message reach the widget the
|
|
436
|
+
same way. The backend's in-memory limiter is defense in depth; the
|
|
437
|
+
gateway owns the shared limit. See [admission errors](../../docs/http-contract.md#admission-errors).
|
|
438
|
+
- The client IP for per-IP limits comes from `cf-connecting-ip`, `x-real-ip` or the first
|
|
439
|
+
`x-forwarded-for` entry. These headers are only trustworthy behind a proxy that overwrites them
|
|
440
|
+
(as before signed-in users existed); elsewhere a client can choose its own IP, both for this
|
|
441
|
+
backend's limits and for the address CCP is told (`X-Dewee-Visitor-IP`, sent only when it is a
|
|
442
|
+
well-formed IP address).
|
|
228
443
|
- "Bring your own gateway" (`allowClientCredentials`) is off by default; when on, internal hosts are refused.
|
|
229
444
|
|
|
230
445
|
The wire format is documented in [docs/http-contract.md](https://github.com/nextlevelbuilder/dewee-webchat/blob/main/docs/http-contract.md).
|