@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/INSTALL.md +13 -5
- package/README.md +67 -16
- package/dist/trigger.iife.js +1 -1
- package/dist/widget.js +2 -2
- package/manifest.json +23 -8
- package/package.json +1 -1
- package/src/Chat.astro +3 -0
- package/src/chat.css +36 -1
- package/src/colors.js +37 -13
- package/src/strings.js +16 -12
- package/src/trigger.js +44 -11
- package/src/widget.css +120 -47
- package/src/widget.js +67 -16
- package/src/worker.js +19 -8
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
|
|
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.
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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 }))
|