dewee-webchat 0.4.2 → 0.5.0-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 CHANGED
@@ -56,7 +56,7 @@ first three values below.
56
56
  | `DEWEE_CCP_CHAT_URL` | The ingress URL, `https://<ccp>/api/public/webchat/v1/wci_…`. https only (plain http to `localhost` for development). |
57
57
  | `DEWEE_WEBCHAT_INTEGRATION_ID` | `wci_…`; must match the end of `DEWEE_CCP_CHAT_URL` |
58
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`) |
59
+ | `DEWEE_WEBCHAT_SECRET` | At least 32 random UTF-8 bytes to sign visitor sessions (`openssl rand -base64 32`) |
60
60
  | `DEWEE_ALLOWED_ORIGINS` | Extra origins for cross-origin embedding (same origin always works) |
61
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
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. |
@@ -85,8 +85,26 @@ The two sets cannot be mixed: with CCP and gateway variables both set, or `trans
85
85
  the other's variables set, the handler refuses to start rather than send a credential to an origin
86
86
  you did not pick. `agents`, `defaultAgent` and `allowClientCredentials` are refused with `ccp`.
87
87
 
88
+ Both transports need a stable `secret` shared by every backend instance. For local development only,
89
+ the legacy transport can opt in to a generated process-local secret with `allowEphemeralSecret: true`.
90
+ Sessions and identity bindings then stop working after a restart or when a request reaches another
91
+ instance. CCP always requires a configured secret of at least 32 bytes.
92
+
88
93
  ## 2. Add the widget
89
94
 
95
+ ### Optional current-page context
96
+
97
+ Page context is off by default. Set `page-context="metadata"` or `page-context="content"` on the
98
+ widget (or `pageContext` in framework configuration) to offer it. The visitor previews the page
99
+ and explicitly chooses whether to share it for each message. Only a supported Gateway/CCP version
100
+ receives the attachment; ordinary chat continues when support is absent. For content, the default
101
+ source is `main`, `article` or `[role=main]`; `contextSelector` and `excludeSelectors` narrow it.
102
+ Form controls, editable areas and hidden content are excluded. Configure a client-side
103
+ `contextProvider` for host-filtered data when needed; the host is responsible for excluding private
104
+ data from its provider. In Astro, set that function with `element.configure()` in a client script,
105
+ not as a serialized component prop. The preview contains the data to be sent: review it before
106
+ sharing. See the [HTTP contract](https://github.com/nextlevelbuilder/dewee-webchat/blob/main/docs/http-contract.md#page-context-attachment).
107
+
90
108
  **Script tag** (no build step):
91
109
 
92
110
  ```html
@@ -140,6 +158,7 @@ Attributes use kebab-case (`show-reasoning="false"`), `data-*` on the script tag
140
158
  | `title`, `subtitle`, `greeting`, `intro`, `placeholder` | | Texts |
141
159
  | `suggestions` | | Quick replies (`a|b` or JSON array as attribute) |
142
160
  | `avatar`, `launcherAvatar` | | Image URL; also use it on the launcher |
161
+ | `imageOrigins` | `[]` | Additional trusted HTTPS origins for Markdown images |
143
162
  | `mode` | `floating` | `floating` (bubble + panel) or `inline` (fills its container) |
144
163
  | `position` | `bottom-right` | or `bottom-left` |
145
164
  | `theme` | `light` | `light`, `dark`, `auto` |
@@ -164,6 +183,49 @@ Methods: `open()`, `close()`, `toggle()`, `send(text)`, `stop()`, `newConversati
164
183
  Events: `dewee:ready`, `dewee:open`, `dewee:close`, `dewee:send`, `dewee:reply`, `dewee:error`, `dewee:action`, `dewee:link`.
165
184
  CSS variables: `--dw-accent`, `--dw-font`, `--dw-radius`, `--dw-width`, `--dw-height`, `--dw-z`.
166
185
 
186
+ ## Markdown images
187
+
188
+ Replies may include `![Product](https://cdn.example.com/photo.jpg)` or a same-origin relative image
189
+ URL. Images are disabled until HTTPS directory prefixes are approved in the backend. Configure the backend
190
+ with `imageSources: ["https://cdn.example.com/products/", "https://qr.example.com/images/"]` (or the comma-separated
191
+ `DEWEE_IMAGE_SOURCES` environment variable). Include a safe image directory on your site's HTTPS origin for relative images.
192
+ For external origins, also configure the widget:
193
+
194
+ ```html
195
+ <dewee-chat image-origins='["https://cdn.example.com", "https://qr.example.com"]'></dewee-chat>
196
+ ```
197
+
198
+ The script-tag equivalent is `data-image-origins`; vanilla JS uses
199
+ `chat.configure({ imageOrigins: ["https://cdn.example.com"] })`, and React, Vue and Astro accept the
200
+ `imageOrigins` prop. Unapproved or broken images fall back to alt text. The widget loads images from
201
+ `{endpoint}/image` using a short-lived encrypted ticket issued to the visitor session; the backend makes the request without cookies or Gateway credentials, rejects
202
+ redirects, and accepts PNG, JPEG, GIF and WebP only after validating the content type and file
203
+ signature. Node and PHP pin a checked public DNS address; Cloudflare Workers use the platform's
204
+ network boundary. Workers conservatively refuse sources sharing the endpoint's last two hostname
205
+ labels (including sibling subdomains), to avoid bypassing Cloudflare's own-zone protections. For
206
+ same-origin static files in the Worker's own Assets binding, a host can explicitly opt in:
207
+
208
+ ```ts
209
+ createDeweeHandler({
210
+ // ... other handler options
211
+ imageSources: ["https://chat.example.com/public-chat-images/"],
212
+ cloudflarePublicImageAssets: { binding: env.ASSETS, prefixes: ["/public-chat-images/"] },
213
+ });
214
+ ```
215
+
216
+ Only exact same-origin paths in both lists use the binding. This reads the Assets collection directly,
217
+ **bypassing Worker middleware, Cloudflare Access, and cookie-based authorization**. Every file under
218
+ the configured prefixes must be public to any valid chat visitor; keep private files elsewhere,
219
+ inspect the build output, and exclude sensitive files using `.assetsignore`. The URL query is not
220
+ passed to the binding. Sibling domains still require a separate approved image domain.
221
+
222
+ Files are limited to 2 MiB and 4096
223
+ pixels per edge, and responses are not cached. Animated GIF/PNG/WebP are refused. Raw HTML,
224
+ SVG, unsafe schemes, credentials in URLs and unapproved origins are rejected.
225
+
226
+ The proxy URL contains an encrypted, short-lived ticket, not the source URL or the visitor session.
227
+ Avoid secrets in image URLs: the ticket request contains the source URL in its body.
228
+
167
229
  ## Custom headers and fetch
168
230
 
169
231
  Behind a same-origin BFF that wants a CSRF header, cookies or tracing:
@@ -180,12 +242,12 @@ Vanilla: `chat.configure({ headers, fetch })` or the `chat.headers` / `chat.fetc
180
242
  every request to `endpoint` (config, session, history, link, chat). It cannot override `content-type`,
181
243
  `authorization` (which carries the anonymous or signed-in session token), `x-dewee-gateway-url` or
182
244
  `x-dewee-gateway-token` (case-insensitive; such keys are dropped). Header names are sent lower-cased. For a
183
- cross-origin `endpoint`, the backend's CORS preflight must allow the extra headers; the built-in handlers only
184
- allow their own. A custom `fetch` receives every request as sent, including the session bearer token of a
245
+ cross-origin `endpoint`, configure the backend's `allowedOrigins` and `allowedHeaders: ["x-csrf-token"]`.
246
+ The built-in TS and PHP handlers accept header names containing letters, digits and hyphens. A custom `fetch` receives every request as sent, including the session bearer token of a
185
247
  signed-in user, so only the trusted host page should supply it.
186
248
 
187
249
  Anonymous visitor sessions are reused until less than 10% of the token lifetime remains (at most 24 h, at least
188
- 30 s), so backends with short session TTLs do not mint a new session on every request. With `transport: "ccp"`,
250
+ 30 s, and no more than half the lifetime), so backends with short session TTLs do not mint a new session on every request. With `transport: "ccp"`,
189
251
  `/session` reports the end of the CCP edge session sealed in the token as `expiresAt` (and `expiresIn`), so the
190
252
  widget asks `/session` again before it lapses, about once an hour, although the token itself lives 30 days. That
191
253
  refresh keeps the visitor id and re-seals a refreshed edge session, so a host that keeps no memory between
@@ -394,7 +456,7 @@ How it behaves:
394
456
  memory, have the logout page send the widget's hint (`chat.reuseHint()`) along: the session in it
395
457
  is revoked when it belongs to that subject. Pass the logout request so CCP sees the visitor's
396
458
  origin (an integration with an origin allowlist needs it; without a request, the origin the
397
- session was issued for is used). Sessions held only by other instances end when they expire. The
459
+ session was issued for is used). Without a shared store, sessions held only by other instances end when they expire. The
398
460
  legacy gateway has no revoke call, so there it only forgets them.
399
461
 
400
462
  ```ts
@@ -406,6 +468,14 @@ How it behaves:
406
468
  // …then clear your own session cookie
407
469
  }
408
470
  ```
471
+ - For multiple handler instances, supply `identity.sessionStore` implementing the exported
472
+ `IdentitySessionStore` interface. Share the same protected store and visitor secret across those
473
+ instances. The default is a bounded, per-handler in-memory store. Store records contain bearer
474
+ credentials: encrypt them at rest, restrict access and never log values. The interface requires
475
+ atomic revision checks and updates across instances, unique non-reused revisions even after
476
+ eviction, conditional token deletion, and expiry by `deadline`; a simple non-atomic KV get/set is
477
+ insufficient. Keys use scoped HMAC digests, not raw host user IDs. Store failures refuse session
478
+ issuance; logout remains best effort, logs store failure and still attempts the supplied hint.
409
479
  - Fail closed. If `authenticate` returns `null`, the visitor chats anonymously, exactly as without
410
480
  `identity`, unless `required` is set, in which case no session is issued. If it throws, the answer
411
481
  is `503 SOURCE_UNAVAILABLE` and no session is issued. If the exchange for a signed-in user fails, no
@@ -451,8 +521,8 @@ links on its own. `dewee:link` fires with `{ expiresAt, deepLinks }`. A link tha
451
521
  progress answers `409 link_in_progress`, a reused idempotency key `409 idempotency_key_reused`, and a
452
522
  gateway with linking switched off `503 linking_unavailable`; the widget shows its own text for each.
453
523
 
454
- The PHP backend does not support signed-in users (it has no JWS signing), with either transport; use
455
- the Node/Workers handler. It refuses tokens that carry an identity session.
524
+ The PHP backend also supports signed-in users and a server-side logout hook; see the
525
+ [PHP identity setup](../../php/README.md#signed-in-users). Its stateless reuse uses the CCP reuse hint.
456
526
 
457
527
  ## Security model
458
528
 
@@ -468,6 +538,9 @@ the Node/Workers handler. It refuses tokens that carry an identity session.
468
538
  - Only allowed agents are reachable (the integration's, legacy: `agents`); origin allowlist, rate limits (20 messages/min per visitor, 10
469
539
  new sessions/10 min per IP by default — pass `rateLimit` for a shared store), 4000-char messages,
470
540
  optional Turnstile.
541
+ - The default rate limiter is in memory and applies per handler instance, not across replicas or
542
+ serverless invocations. For a shared backend limit, provide an atomic shared-store `rateLimit`
543
+ implementation; do not rely on the default as a cluster-wide limit.
471
544
  - The Dewee gateway enforces the shared admission limits on every message. A refusal reaches the widget
472
545
  as a typed SSE error (`POLICY_DENIED`, or `RATE_LIMITED` with `retryAfterMs`) with a fixed message, never
473
546
  the gateway's own text. The widget shows a localized error and disables sending until `retryAfterMs`
@@ -2,11 +2,11 @@
2
2
  // Astro component: <DeweeChat endpoint="/api/dewee" title="Support" showReasoning={false} />
3
3
  import type { DeweeChatConfig } from "dewee-webchat";
4
4
 
5
- type Props = Partial<DeweeChatConfig> & { class?: string };
5
+ type Props = Omit<Partial<DeweeChatConfig>, "contextProvider"> & { class?: string };
6
6
  const { class: className, ...config } = Astro.props;
7
7
  const attrs: Record<string, string> = {};
8
8
  for (const [key, value] of Object.entries(config)) {
9
- if (value === undefined || value === null) continue;
9
+ if (value === undefined || value === null || typeof value === "function") continue;
10
10
  const name = key.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
11
11
  attrs[name] = Array.isArray(value) ? JSON.stringify(value) : String(value);
12
12
  }
@@ -21,7 +21,7 @@ function isLinkErrorCode(code) {
21
21
  }
22
22
  var HEADER_GATEWAY_URL = "x-dewee-gateway-url";
23
23
  var HEADER_GATEWAY_TOKEN = "x-dewee-gateway-token";
24
- var VERSION = "0.4.2";
24
+ var VERSION = "0.5.0-beta.2";
25
25
 
26
26
  // src/actions/page-block.ts
27
27
  var ACTION_NAME_RE = /^[a-z][a-z0-9_.-]{0,63}$/;