@pmeweb/plugin-chat 1.0.0 → 1.2.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.
package/src/worker.js CHANGED
@@ -10,7 +10,11 @@
10
10
  // - The host sent to pmeweb-chat is the one this request ARRIVED on, never a header or a body
11
11
  // field: it is how pmeweb-chat finds the client's settings.
12
12
  // - start: Turnstile (@pmeweb/turnstile, action `chat`, solved on this host), then CHAT_LIMIT keyed
13
- // on the visitor's IP, then pmeweb-chat. The IP goes to siteverify and the limiter only.
13
+ // on the visitor's IP, then pmeweb-chat, which applies the global limits (conversation starts per
14
+ // minute, conversations per day, messages, the daily cap; edited in the portal admin since 1.2.0).
15
+ // The IP goes to siteverify, the limiters and, in the `x-pmw-visitor-ip` header only, to
16
+ // pmeweb-chat, which counts conversations per visitor per day (10) and keeps only a daily hash
17
+ // of it. pmeweb-chat trusts that header because only service bindings reach it.
14
18
  // Only named fields go on; `sms` is sent only as `voipms` (pmeweb-chat refuses any other value),
15
19
  // `photos` only as true. pmeweb-chat's status and JSON pass through (426 rpc_version, 503
16
20
  // not_configured, ...): the widget reads them.
@@ -28,7 +32,7 @@ import { verifyTurnstile } from '@pmeweb/turnstile/worker'
28
32
  import { RPC_HEADER, RPC_VERSION } from './protocol.js'
29
33
 
30
34
  /** This package's version, reported by the health route (a test holds it equal to package.json). */
31
- export const VERSION = '1.0.0'
35
+ export const VERSION = '1.2.0'
32
36
  export { RPC_VERSION }
33
37
 
34
38
  const SERVICE = 'https://pmeweb-chat.internal'
@@ -52,10 +56,14 @@ function json(body, status = 200, headers = {}) {
52
56
 
53
57
  const langOf = (v) => (v === 'en' || v === 'fr' ? v : null)
54
58
 
55
- async function allowed(request, env) {
56
- if (!env.CHAT_LIMIT) return true
59
+ const VISITOR_IP_HEADER = 'x-pmw-visitor-ip'
60
+
61
+ /** CHAT_LIMIT, the flood guard at the site's edge (starts, sockets, photos, the contact form). */
62
+ async function allowed(request, env, { start = false } = {}) {
63
+ const [limiter, prefix] = [env.CHAT_LIMIT, 'chat']
64
+ if (!limiter) return true
57
65
  try {
58
- return (await env.CHAT_LIMIT.limit({ key: `chat:${request.headers.get('cf-connecting-ip') ?? 'unknown'}` })).success
66
+ return (await limiter.limit({ key: `${prefix}:${request.headers.get('cf-connecting-ip') ?? 'unknown'}` })).success
59
67
  } catch (e) {
60
68
  // The limiter is approximate by design; a failure of it lets a Turnstile-verified visitor
61
69
  // through, and pmeweb-chat still caps turns and the day's spend.
@@ -102,7 +110,7 @@ async function start(request, url, env, { config, lang }) {
102
110
  console.warn(`[chat] turnstile refused: ${verdict.reason}`)
103
111
  return json({ ok: false, error: 'turnstile' }, 403)
104
112
  }
105
- if (!(await allowed(request, env))) return json({ ok: false, error: 'too_many' }, 429)
113
+ if (!(await allowed(request, env, { start: !lead }))) return json({ ok: false, error: 'too_many' }, 429)
106
114
 
107
115
  const page = typeof b.page === 'string' && b.page.startsWith('/') && !b.page.startsWith('//') && b.page.length <= 512 ? b.page : null
108
116
  const body = { host: url.hostname.toLowerCase(), lang: langOf(b.lang) ?? langOf(lang) ?? 'fr', page }
@@ -110,11 +118,14 @@ async function start(request, url, env, { config, lang }) {
110
118
  if (config?.photos === true) body.photos = true
111
119
  if (lead) body.fallbackLead = lead
112
120
 
121
+ const headers = { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) }
122
+ const ip = request.headers.get('cf-connecting-ip')
123
+ if (ip) headers[VISITOR_IP_HEADER] = ip
113
124
  let res
114
125
  try {
115
126
  res = await env.CHAT.fetch(`${SERVICE}/start`, {
116
127
  method: 'POST',
117
- headers: { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) },
128
+ headers,
118
129
  body: JSON.stringify(body),
119
130
  })
120
131
  } catch {
@@ -135,7 +146,7 @@ async function socket(request, url, env) {
135
146
  const q = new URLSearchParams()
136
147
  for (const k of ['c', 'k']) q.set(k, url.searchParams.get(k) ?? '')
137
148
  const headers = new Headers(request.headers)
138
- for (const h of ['cookie', 'authorization']) headers.delete(h)
149
+ for (const h of ['cookie', 'authorization', VISITOR_IP_HEADER]) headers.delete(h)
139
150
  headers.set(RPC_HEADER, String(RPC_VERSION))
140
151
  try {
141
152
  return await env.CHAT.fetch(new Request(`${SERVICE}/ws?${q}`, { method: 'GET', headers }))