@pocketping/sdk-node 0.2.0 → 1.1.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/README.md +376 -0
- package/dist/{index.mjs → index.cjs} +158 -13
- package/dist/{index.d.mts → index.d.cts} +74 -4
- package/dist/index.d.ts +74 -4
- package/dist/index.js +130 -41
- package/package.json +34 -5
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# PocketPing Node.js SDK
|
|
2
|
+
|
|
3
|
+
Node.js SDK for PocketPing - real-time customer chat with mobile notifications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pocketping/sdk-node
|
|
9
|
+
|
|
10
|
+
# Or with pnpm
|
|
11
|
+
pnpm add @pocketping/sdk-node
|
|
12
|
+
|
|
13
|
+
# Or with yarn
|
|
14
|
+
yarn add @pocketping/sdk-node
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start with Express
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import express from 'express';
|
|
21
|
+
import { createServer } from 'http';
|
|
22
|
+
import { PocketPing } from '@pocketping/sdk-node';
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
const server = createServer(app);
|
|
26
|
+
|
|
27
|
+
app.use(express.json());
|
|
28
|
+
|
|
29
|
+
// Initialize PocketPing
|
|
30
|
+
const pp = new PocketPing({
|
|
31
|
+
welcomeMessage: 'Hi! How can we help you today?',
|
|
32
|
+
onNewSession: (session) => {
|
|
33
|
+
console.log(`New session: ${session.id}`);
|
|
34
|
+
},
|
|
35
|
+
onMessage: (message, session) => {
|
|
36
|
+
console.log(`Message from ${message.sender}: ${message.content}`);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Mount PocketPing routes
|
|
41
|
+
app.use('/pocketping', pp.middleware());
|
|
42
|
+
|
|
43
|
+
// Attach WebSocket for real-time communication
|
|
44
|
+
pp.attachWebSocket(server);
|
|
45
|
+
|
|
46
|
+
server.listen(3000, () => {
|
|
47
|
+
console.log('Server running on http://localhost:3000');
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration Options
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const pp = new PocketPing({
|
|
55
|
+
// Welcome message shown to new visitors
|
|
56
|
+
welcomeMessage: 'Hi! How can we help you?',
|
|
57
|
+
|
|
58
|
+
// Callbacks
|
|
59
|
+
onNewSession: (session) => { /* ... */ },
|
|
60
|
+
onMessage: (message, session) => { /* ... */ },
|
|
61
|
+
onEvent: (event, session) => { /* ... */ },
|
|
62
|
+
|
|
63
|
+
// Custom storage (default: in-memory)
|
|
64
|
+
storage: new MemoryStorage(),
|
|
65
|
+
|
|
66
|
+
// Bridge server for notifications (Telegram, Discord, Slack)
|
|
67
|
+
bridgeServerUrl: 'http://localhost:3001',
|
|
68
|
+
|
|
69
|
+
// Protocol version settings
|
|
70
|
+
protocolVersion: '1.0',
|
|
71
|
+
minSupportedVersion: '0.1',
|
|
72
|
+
|
|
73
|
+
// IP filtering (see IP Filtering section below)
|
|
74
|
+
ipFilter: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
mode: 'blocklist',
|
|
77
|
+
blocklist: ['203.0.113.0/24'],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## IP Filtering
|
|
83
|
+
|
|
84
|
+
Block or allow specific IP addresses or CIDR ranges:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const pp = new PocketPing({
|
|
88
|
+
ipFilter: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
mode: 'blocklist', // 'allowlist' | 'blocklist' | 'both'
|
|
91
|
+
blocklist: [
|
|
92
|
+
'203.0.113.0/24', // CIDR range
|
|
93
|
+
'198.51.100.50', // Single IP
|
|
94
|
+
],
|
|
95
|
+
allowlist: [
|
|
96
|
+
'10.0.0.0/8', // Internal network
|
|
97
|
+
],
|
|
98
|
+
logBlocked: true, // Log blocked requests (default: true)
|
|
99
|
+
blockedStatusCode: 403,
|
|
100
|
+
blockedMessage: 'Forbidden',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Or with a custom filter function
|
|
105
|
+
const pp = new PocketPing({
|
|
106
|
+
ipFilter: {
|
|
107
|
+
enabled: true,
|
|
108
|
+
mode: 'blocklist',
|
|
109
|
+
customFilter: (ip, request) => {
|
|
110
|
+
// Return true to allow, false to block, null to defer to list-based filtering
|
|
111
|
+
if (ip.startsWith('192.168.')) return true; // Always allow local
|
|
112
|
+
return null; // Use blocklist/allowlist
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Modes
|
|
119
|
+
|
|
120
|
+
| Mode | Behavior |
|
|
121
|
+
|------|----------|
|
|
122
|
+
| `blocklist` | Block IPs in blocklist, allow all others (default) |
|
|
123
|
+
| `allowlist` | Only allow IPs in allowlist, block all others |
|
|
124
|
+
| `both` | Allowlist takes precedence, then blocklist is applied |
|
|
125
|
+
|
|
126
|
+
### CIDR Support
|
|
127
|
+
|
|
128
|
+
The SDK supports CIDR notation for IP ranges:
|
|
129
|
+
- Single IP: `192.168.1.1` (treated as `/32`)
|
|
130
|
+
- Class C: `192.168.1.0/24` (256 addresses)
|
|
131
|
+
- Class B: `172.16.0.0/16` (65,536 addresses)
|
|
132
|
+
- Class A: `10.0.0.0/8` (16M addresses)
|
|
133
|
+
|
|
134
|
+
### Manual IP Check
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Check IP manually
|
|
138
|
+
const result = pp.checkIpFilter('192.168.1.50');
|
|
139
|
+
// result: { allowed: boolean, reason: string, matchedRule?: string }
|
|
140
|
+
|
|
141
|
+
// Get client IP from request headers
|
|
142
|
+
const clientIp = pp.getClientIp(request.headers);
|
|
143
|
+
// Checks: CF-Connecting-IP, X-Real-IP, X-Forwarded-For
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Architecture Options
|
|
147
|
+
|
|
148
|
+
### 1. Embedded Mode (Simple)
|
|
149
|
+
|
|
150
|
+
SDK handles everything directly - best for single server deployments:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { PocketPing } from '@pocketping/sdk-node';
|
|
154
|
+
|
|
155
|
+
const pp = new PocketPing({
|
|
156
|
+
welcomeMessage: 'Hello!',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.use('/pocketping', pp.middleware());
|
|
160
|
+
pp.attachWebSocket(server);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 2. Bridge Server Mode (Recommended)
|
|
164
|
+
|
|
165
|
+
SDK connects to a dedicated bridge server for notifications:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const pp = new PocketPing({
|
|
169
|
+
welcomeMessage: 'Hello!',
|
|
170
|
+
bridgeServerUrl: process.env.BRIDGE_SERVER_URL,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The bridge server handles Telegram, Discord, and Slack integrations, keeping your main server lightweight.
|
|
175
|
+
|
|
176
|
+
## Custom Storage
|
|
177
|
+
|
|
178
|
+
Implement the `Storage` interface for persistence:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { Storage, Session, Message } from '@pocketping/sdk-node';
|
|
182
|
+
|
|
183
|
+
class PostgresStorage implements Storage {
|
|
184
|
+
async createSession(session: Session): Promise<void> {
|
|
185
|
+
// Your implementation
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getSession(sessionId: string): Promise<Session | null> {
|
|
189
|
+
// Your implementation
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async saveMessage(message: Message): Promise<void> {
|
|
193
|
+
// Your implementation
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getMessages(sessionId: string, options?: { after?: string; limit?: number }): Promise<Message[]> {
|
|
197
|
+
// Your implementation
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ... implement other methods
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pp = new PocketPing({
|
|
204
|
+
storage: new PostgresStorage(),
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Events / Callbacks
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const pp = new PocketPing({
|
|
212
|
+
onNewSession: (session) => {
|
|
213
|
+
console.log(`New session: ${session.id}`);
|
|
214
|
+
// Notify your team, log to analytics, etc.
|
|
215
|
+
},
|
|
216
|
+
onMessage: (message, session) => {
|
|
217
|
+
console.log(`Message from ${message.sender}: ${message.content}`);
|
|
218
|
+
},
|
|
219
|
+
onEvent: (event, session) => {
|
|
220
|
+
console.log(`Custom event: ${event.name}`, event.data);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Custom Events
|
|
226
|
+
|
|
227
|
+
PocketPing supports bidirectional custom events between your website and backend.
|
|
228
|
+
|
|
229
|
+
### Listening for Events (Widget -> Backend)
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Using callback in config
|
|
233
|
+
const pp = new PocketPing({
|
|
234
|
+
onEvent: (event, session) => {
|
|
235
|
+
console.log(`Event ${event.name} from session ${session.id}`);
|
|
236
|
+
console.log(`Data:`, event.data);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Or using subscription
|
|
241
|
+
pp.onEvent('clicked_pricing', (event, session) => {
|
|
242
|
+
console.log(`User interested in: ${event.data?.plan}`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Subscribe to all events
|
|
246
|
+
pp.onEvent('*', (event, session) => {
|
|
247
|
+
console.log(`Event: ${event.name}`, event.data);
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Sending Events (Backend -> Widget)
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Send to a specific session
|
|
255
|
+
await pp.emitEvent('session-123', 'show_offer', {
|
|
256
|
+
discount: 20,
|
|
257
|
+
code: 'SAVE20',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Broadcast to all connected sessions
|
|
261
|
+
await pp.broadcastEvent('announcement', {
|
|
262
|
+
message: 'New feature launched!',
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## User Identification
|
|
267
|
+
|
|
268
|
+
Track and identify users across sessions:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// On the frontend (widget)
|
|
272
|
+
PocketPing.identify({
|
|
273
|
+
userId: 'user_123',
|
|
274
|
+
email: 'john@example.com',
|
|
275
|
+
name: 'John Doe',
|
|
276
|
+
plan: 'pro',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Get current identity
|
|
280
|
+
const identity = PocketPing.getIdentity();
|
|
281
|
+
|
|
282
|
+
// Reset identity (e.g., on logout)
|
|
283
|
+
PocketPing.reset();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
User identity is automatically included in session metadata and forwarded to bridges.
|
|
287
|
+
|
|
288
|
+
## Operator Presence
|
|
289
|
+
|
|
290
|
+
Control operator online status:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// Set operator as online
|
|
294
|
+
pp.setOperatorOnline(true);
|
|
295
|
+
|
|
296
|
+
// Set operator as offline
|
|
297
|
+
pp.setOperatorOnline(false);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
When using the bridge server, presence is managed automatically via Telegram/Discord/Slack commands.
|
|
301
|
+
|
|
302
|
+
## API Reference
|
|
303
|
+
|
|
304
|
+
### PocketPing Class
|
|
305
|
+
|
|
306
|
+
| Method | Description |
|
|
307
|
+
|--------|-------------|
|
|
308
|
+
| `middleware()` | Returns Express middleware for HTTP routes |
|
|
309
|
+
| `attachWebSocket(server)` | Attaches WebSocket handler for real-time communication |
|
|
310
|
+
| `setOperatorOnline(online)` | Sets operator online/offline status |
|
|
311
|
+
| `onEvent(name, callback)` | Subscribe to custom events |
|
|
312
|
+
| `offEvent(name, callback)` | Unsubscribe from custom events |
|
|
313
|
+
| `emitEvent(sessionId, name, data)` | Send event to specific session |
|
|
314
|
+
| `broadcastEvent(name, data)` | Broadcast event to all sessions |
|
|
315
|
+
| `getSession(sessionId)` | Get session by ID |
|
|
316
|
+
| `getMessages(sessionId, options)` | Get messages for a session |
|
|
317
|
+
|
|
318
|
+
### Types
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
interface Session {
|
|
322
|
+
id: string;
|
|
323
|
+
visitorId: string;
|
|
324
|
+
metadata: SessionMetadata;
|
|
325
|
+
createdAt: Date;
|
|
326
|
+
lastActivity: Date;
|
|
327
|
+
status: 'active' | 'closed';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface Message {
|
|
331
|
+
id: string;
|
|
332
|
+
sessionId: string;
|
|
333
|
+
sender: 'visitor' | 'operator' | 'system' | 'ai';
|
|
334
|
+
content: string;
|
|
335
|
+
timestamp: Date;
|
|
336
|
+
metadata?: Record<string, unknown>;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface CustomEvent {
|
|
340
|
+
name: string;
|
|
341
|
+
data?: Record<string, unknown>;
|
|
342
|
+
timestamp: Date;
|
|
343
|
+
sessionId?: string;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Widget Integration
|
|
348
|
+
|
|
349
|
+
Add the widget to your website:
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<script src="https://unpkg.com/@pocketping/widget"></script>
|
|
353
|
+
<script>
|
|
354
|
+
PocketPing.init({
|
|
355
|
+
endpoint: '/pocketping',
|
|
356
|
+
theme: 'light', // or 'dark'
|
|
357
|
+
primaryColor: '#667eea',
|
|
358
|
+
});
|
|
359
|
+
</script>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Or via npm:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { init } from '@pocketping/widget';
|
|
366
|
+
|
|
367
|
+
init({
|
|
368
|
+
endpoint: '/pocketping',
|
|
369
|
+
theme: 'dark',
|
|
370
|
+
primaryColor: '#667eea',
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT
|
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MemoryStorage: () => MemoryStorage,
|
|
24
|
+
PocketPing: () => PocketPing
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
1
28
|
// src/pocketping.ts
|
|
2
|
-
|
|
3
|
-
|
|
29
|
+
var import_crypto = require("crypto");
|
|
30
|
+
var import_ws = require("ws");
|
|
4
31
|
|
|
5
32
|
// src/storage/memory.ts
|
|
6
33
|
var MemoryStorage = class {
|
|
@@ -65,6 +92,71 @@ var MemoryStorage = class {
|
|
|
65
92
|
}
|
|
66
93
|
};
|
|
67
94
|
|
|
95
|
+
// src/utils/ip-filter.ts
|
|
96
|
+
function ipToNumber(ip) {
|
|
97
|
+
const parts = ip.split(".");
|
|
98
|
+
if (parts.length !== 4) return null;
|
|
99
|
+
let num = 0;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
const n = parseInt(part, 10);
|
|
102
|
+
if (isNaN(n) || n < 0 || n > 255) return null;
|
|
103
|
+
num = num << 8 | n;
|
|
104
|
+
}
|
|
105
|
+
return num >>> 0;
|
|
106
|
+
}
|
|
107
|
+
function parseCidr(cidr) {
|
|
108
|
+
const [ip, bits] = cidr.split("/");
|
|
109
|
+
const base = ipToNumber(ip);
|
|
110
|
+
if (base === null) return null;
|
|
111
|
+
const prefix = bits ? parseInt(bits, 10) : 32;
|
|
112
|
+
if (isNaN(prefix) || prefix < 0 || prefix > 32) return null;
|
|
113
|
+
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
114
|
+
return { base: (base & mask) >>> 0, mask };
|
|
115
|
+
}
|
|
116
|
+
function ipMatchesCidr(ip, cidr) {
|
|
117
|
+
const ipNum = ipToNumber(ip);
|
|
118
|
+
if (ipNum === null) return false;
|
|
119
|
+
const parsed = parseCidr(cidr);
|
|
120
|
+
if (!parsed) return false;
|
|
121
|
+
return (ipNum & parsed.mask) >>> 0 === parsed.base;
|
|
122
|
+
}
|
|
123
|
+
function ipMatchesAny(ip, list) {
|
|
124
|
+
return list.some((entry) => ipMatchesCidr(ip, entry));
|
|
125
|
+
}
|
|
126
|
+
function shouldAllowIp(ip, config) {
|
|
127
|
+
const { mode = "blocklist", allowlist = [], blocklist = [] } = config;
|
|
128
|
+
switch (mode) {
|
|
129
|
+
case "allowlist":
|
|
130
|
+
if (ipMatchesAny(ip, allowlist)) {
|
|
131
|
+
return { allowed: true, reason: "allowlist" };
|
|
132
|
+
}
|
|
133
|
+
return { allowed: false, reason: "not_in_allowlist" };
|
|
134
|
+
case "blocklist":
|
|
135
|
+
if (ipMatchesAny(ip, blocklist)) {
|
|
136
|
+
return { allowed: false, reason: "blocklist" };
|
|
137
|
+
}
|
|
138
|
+
return { allowed: true, reason: "default" };
|
|
139
|
+
case "both":
|
|
140
|
+
if (ipMatchesAny(ip, allowlist)) {
|
|
141
|
+
return { allowed: true, reason: "allowlist" };
|
|
142
|
+
}
|
|
143
|
+
if (ipMatchesAny(ip, blocklist)) {
|
|
144
|
+
return { allowed: false, reason: "blocklist" };
|
|
145
|
+
}
|
|
146
|
+
return { allowed: true, reason: "default" };
|
|
147
|
+
default:
|
|
148
|
+
return { allowed: true, reason: "default" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function checkIpFilter(ip, config, requestInfo) {
|
|
152
|
+
if (config.customFilter) {
|
|
153
|
+
const customResult = await config.customFilter(ip, requestInfo);
|
|
154
|
+
if (customResult === true) return { allowed: true, reason: "custom" };
|
|
155
|
+
if (customResult === false) return { allowed: false, reason: "custom" };
|
|
156
|
+
}
|
|
157
|
+
return shouldAllowIp(ip, config);
|
|
158
|
+
}
|
|
159
|
+
|
|
68
160
|
// src/pocketping.ts
|
|
69
161
|
function getClientIp(req) {
|
|
70
162
|
const forwarded = req.headers["x-forwarded-for"];
|
|
@@ -152,6 +244,34 @@ var PocketPing = class {
|
|
|
152
244
|
res.end();
|
|
153
245
|
return;
|
|
154
246
|
}
|
|
247
|
+
if (this.config.ipFilter?.enabled) {
|
|
248
|
+
const clientIp = getClientIp(req);
|
|
249
|
+
const filterResult = await checkIpFilter(clientIp, this.config.ipFilter, {
|
|
250
|
+
path
|
|
251
|
+
});
|
|
252
|
+
if (!filterResult.allowed) {
|
|
253
|
+
if (this.config.ipFilter.logBlocked !== false) {
|
|
254
|
+
const logEvent = {
|
|
255
|
+
type: "blocked",
|
|
256
|
+
ip: clientIp,
|
|
257
|
+
reason: filterResult.reason,
|
|
258
|
+
path,
|
|
259
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
260
|
+
};
|
|
261
|
+
if (this.config.ipFilter.logger) {
|
|
262
|
+
this.config.ipFilter.logger(logEvent);
|
|
263
|
+
} else {
|
|
264
|
+
console.log(`[PocketPing] IP blocked: ${clientIp} - reason: ${filterResult.reason}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
res.statusCode = this.config.ipFilter.blockedStatusCode ?? 403;
|
|
268
|
+
res.setHeader("Content-Type", "application/json");
|
|
269
|
+
res.end(JSON.stringify({
|
|
270
|
+
error: this.config.ipFilter.blockedMessage ?? "Forbidden"
|
|
271
|
+
}));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
155
275
|
const widgetVersion = req.headers["x-pocketping-version"];
|
|
156
276
|
const versionCheck = this.checkWidgetVersion(widgetVersion);
|
|
157
277
|
this.setVersionHeaders(res, versionCheck);
|
|
@@ -256,11 +376,35 @@ var PocketPing = class {
|
|
|
256
376
|
// ─────────────────────────────────────────────────────────────────
|
|
257
377
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
378
|
attachWebSocket(server) {
|
|
259
|
-
this.wss = new WebSocketServer({
|
|
379
|
+
this.wss = new import_ws.WebSocketServer({
|
|
260
380
|
server,
|
|
261
381
|
path: "/pocketping/stream"
|
|
262
382
|
});
|
|
263
|
-
this.wss.on("connection", (ws, req) => {
|
|
383
|
+
this.wss.on("connection", async (ws, req) => {
|
|
384
|
+
if (this.config.ipFilter?.enabled) {
|
|
385
|
+
const clientIp = getClientIp(req);
|
|
386
|
+
const filterResult = await checkIpFilter(clientIp, this.config.ipFilter, {
|
|
387
|
+
path: "/pocketping/stream"
|
|
388
|
+
});
|
|
389
|
+
if (!filterResult.allowed) {
|
|
390
|
+
if (this.config.ipFilter.logBlocked !== false) {
|
|
391
|
+
const logEvent = {
|
|
392
|
+
type: "blocked",
|
|
393
|
+
ip: clientIp,
|
|
394
|
+
reason: filterResult.reason,
|
|
395
|
+
path: "/pocketping/stream",
|
|
396
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
397
|
+
};
|
|
398
|
+
if (this.config.ipFilter.logger) {
|
|
399
|
+
this.config.ipFilter.logger(logEvent);
|
|
400
|
+
} else {
|
|
401
|
+
console.log(`[PocketPing] WS IP blocked: ${clientIp} - reason: ${filterResult.reason}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
ws.close(4003, this.config.ipFilter.blockedMessage ?? "Forbidden");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
264
408
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
265
409
|
const sessionId = url.searchParams.get("sessionId");
|
|
266
410
|
if (!sessionId) {
|
|
@@ -334,7 +478,7 @@ var PocketPing = class {
|
|
|
334
478
|
if (!sockets) return;
|
|
335
479
|
const message = JSON.stringify(event);
|
|
336
480
|
for (const ws of sockets) {
|
|
337
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
481
|
+
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
338
482
|
ws.send(message);
|
|
339
483
|
}
|
|
340
484
|
}
|
|
@@ -479,7 +623,7 @@ var PocketPing = class {
|
|
|
479
623
|
readAt: status === "read" ? now.toISOString() : void 0
|
|
480
624
|
}
|
|
481
625
|
});
|
|
482
|
-
await this.notifyBridgesRead(request.sessionId, request.messageIds, status);
|
|
626
|
+
await this.notifyBridgesRead(request.sessionId, request.messageIds, status, session);
|
|
483
627
|
return { updated };
|
|
484
628
|
}
|
|
485
629
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -656,7 +800,7 @@ var PocketPing = class {
|
|
|
656
800
|
await bridge.onNewSession?.(args[0]);
|
|
657
801
|
break;
|
|
658
802
|
case "message":
|
|
659
|
-
await bridge.
|
|
803
|
+
await bridge.onVisitorMessage?.(args[0], args[1]);
|
|
660
804
|
break;
|
|
661
805
|
}
|
|
662
806
|
} catch (err) {
|
|
@@ -664,10 +808,10 @@ var PocketPing = class {
|
|
|
664
808
|
}
|
|
665
809
|
}
|
|
666
810
|
}
|
|
667
|
-
async notifyBridgesRead(sessionId, messageIds, status) {
|
|
811
|
+
async notifyBridgesRead(sessionId, messageIds, status, session) {
|
|
668
812
|
for (const bridge of this.bridges) {
|
|
669
813
|
try {
|
|
670
|
-
await bridge.onMessageRead?.(sessionId, messageIds, status);
|
|
814
|
+
await bridge.onMessageRead?.(sessionId, messageIds, status, session);
|
|
671
815
|
} catch (err) {
|
|
672
816
|
console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
|
|
673
817
|
}
|
|
@@ -676,7 +820,7 @@ var PocketPing = class {
|
|
|
676
820
|
async notifyBridgesEvent(event, session) {
|
|
677
821
|
for (const bridge of this.bridges) {
|
|
678
822
|
try {
|
|
679
|
-
await bridge.
|
|
823
|
+
await bridge.onCustomEvent?.(event, session);
|
|
680
824
|
} catch (err) {
|
|
681
825
|
console.error(`[PocketPing] Bridge ${bridge.name} event notification error:`, err);
|
|
682
826
|
}
|
|
@@ -715,7 +859,7 @@ var PocketPing = class {
|
|
|
715
859
|
"Content-Type": "application/json"
|
|
716
860
|
};
|
|
717
861
|
if (this.config.webhookSecret) {
|
|
718
|
-
const signature = createHmac("sha256", this.config.webhookSecret).update(body).digest("hex");
|
|
862
|
+
const signature = (0, import_crypto.createHmac)("sha256", this.config.webhookSecret).update(body).digest("hex");
|
|
719
863
|
headers["X-PocketPing-Signature"] = `sha256=${signature}`;
|
|
720
864
|
}
|
|
721
865
|
const timeout = this.config.webhookTimeout ?? 5e3;
|
|
@@ -850,7 +994,8 @@ var PocketPing = class {
|
|
|
850
994
|
});
|
|
851
995
|
}
|
|
852
996
|
};
|
|
853
|
-
export
|
|
997
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
998
|
+
0 && (module.exports = {
|
|
854
999
|
MemoryStorage,
|
|
855
1000
|
PocketPing
|
|
856
|
-
};
|
|
1001
|
+
});
|
|
@@ -28,13 +28,15 @@ interface Bridge {
|
|
|
28
28
|
/** Called when a new chat session is created */
|
|
29
29
|
onNewSession?(session: Session): void | Promise<void>;
|
|
30
30
|
/** Called when a visitor sends a message */
|
|
31
|
-
|
|
31
|
+
onVisitorMessage?(message: Message, session: Session): void | Promise<void>;
|
|
32
|
+
/** Called when an operator sends a message (for cross-bridge sync) */
|
|
33
|
+
onOperatorMessage?(message: Message, session: Session, sourceBridge?: string, operatorName?: string): void | Promise<void>;
|
|
32
34
|
/** Called when visitor starts/stops typing */
|
|
33
35
|
onTyping?(sessionId: string, isTyping: boolean): void | Promise<void>;
|
|
34
36
|
/** Called when messages are marked as delivered/read */
|
|
35
|
-
onMessageRead?(sessionId: string, messageIds: string[], status: MessageStatus): void | Promise<void>;
|
|
37
|
+
onMessageRead?(sessionId: string, messageIds: string[], status: MessageStatus, session: Session): void | Promise<void>;
|
|
36
38
|
/** Called when a custom event is triggered from the widget */
|
|
37
|
-
|
|
39
|
+
onCustomEvent?(event: CustomEvent, session: Session): void | Promise<void>;
|
|
38
40
|
/** Called when a user identifies themselves via PocketPing.identify() */
|
|
39
41
|
onIdentityUpdate?(session: Session): void | Promise<void>;
|
|
40
42
|
/** Cleanup when bridge is removed */
|
|
@@ -54,6 +56,52 @@ interface AIProvider {
|
|
|
54
56
|
isAvailable(): Promise<boolean>;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* IP Filtering utilities for PocketPing SDK
|
|
61
|
+
* Supports CIDR notation and individual IP addresses
|
|
62
|
+
*/
|
|
63
|
+
type IpFilterMode = 'allowlist' | 'blocklist' | 'both';
|
|
64
|
+
interface IpFilterConfig {
|
|
65
|
+
/** Enable/disable IP filtering (default: false) */
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
/** Filter mode (default: 'blocklist') */
|
|
68
|
+
mode?: IpFilterMode;
|
|
69
|
+
/** IPs/CIDRs to allow (e.g., ['192.168.1.0/24', '10.0.0.1']) */
|
|
70
|
+
allowlist?: string[];
|
|
71
|
+
/** IPs/CIDRs to block (e.g., ['203.0.113.0/24', '198.51.100.50']) */
|
|
72
|
+
blocklist?: string[];
|
|
73
|
+
/** Custom filter callback for advanced logic */
|
|
74
|
+
customFilter?: IpFilterCallback;
|
|
75
|
+
/** Log blocked requests for security auditing (default: true) */
|
|
76
|
+
logBlocked?: boolean;
|
|
77
|
+
/** Custom logger function */
|
|
78
|
+
logger?: (event: IpFilterLogEvent) => void;
|
|
79
|
+
/** HTTP status code for blocked requests (default: 403) */
|
|
80
|
+
blockedStatusCode?: number;
|
|
81
|
+
/** Response message for blocked requests (default: 'Forbidden') */
|
|
82
|
+
blockedMessage?: string;
|
|
83
|
+
/** Trust proxy headers (X-Forwarded-For, etc.) (default: true) */
|
|
84
|
+
trustProxy?: boolean;
|
|
85
|
+
/** Ordered list of headers to check for client IP */
|
|
86
|
+
proxyHeaders?: string[];
|
|
87
|
+
}
|
|
88
|
+
interface IpFilterLogEvent {
|
|
89
|
+
type: 'blocked' | 'allowed';
|
|
90
|
+
ip: string;
|
|
91
|
+
reason: 'allowlist' | 'blocklist' | 'custom' | 'not_in_allowlist' | 'default';
|
|
92
|
+
path: string;
|
|
93
|
+
timestamp: Date;
|
|
94
|
+
sessionId?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Custom IP filter callback
|
|
98
|
+
* Return true to allow, false to block, undefined to defer to list-based filtering
|
|
99
|
+
*/
|
|
100
|
+
type IpFilterCallback = (ip: string, request: {
|
|
101
|
+
path: string;
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
}) => boolean | undefined | Promise<boolean | undefined>;
|
|
104
|
+
|
|
57
105
|
interface PocketPingConfig {
|
|
58
106
|
/** Storage adapter for sessions and messages */
|
|
59
107
|
storage?: Storage | 'memory';
|
|
@@ -87,6 +135,8 @@ interface PocketPingConfig {
|
|
|
87
135
|
versionWarningMessage?: string;
|
|
88
136
|
/** URL to upgrade instructions */
|
|
89
137
|
versionUpgradeUrl?: string;
|
|
138
|
+
/** IP filtering configuration (allowlist/blocklist) */
|
|
139
|
+
ipFilter?: IpFilterConfig;
|
|
90
140
|
}
|
|
91
141
|
interface AIConfig {
|
|
92
142
|
provider: AIProvider | 'openai' | 'gemini' | 'anthropic';
|
|
@@ -153,12 +203,32 @@ interface ConnectRequest {
|
|
|
153
203
|
/** User identity if already identified */
|
|
154
204
|
identity?: UserIdentity;
|
|
155
205
|
}
|
|
206
|
+
/** Tracked element configuration (for SaaS auto-tracking) */
|
|
207
|
+
interface TrackedElement {
|
|
208
|
+
/** CSS selector for the element(s) to track */
|
|
209
|
+
selector: string;
|
|
210
|
+
/** DOM event to listen for (default: 'click') */
|
|
211
|
+
event?: 'click' | 'submit' | 'focus' | 'change' | 'mouseenter';
|
|
212
|
+
/** Event name sent to backend */
|
|
213
|
+
name: string;
|
|
214
|
+
/** If provided, opens widget with this message when triggered */
|
|
215
|
+
widgetMessage?: string;
|
|
216
|
+
/** Additional data to send with the event */
|
|
217
|
+
data?: Record<string, unknown>;
|
|
218
|
+
}
|
|
219
|
+
/** Options for trigger() method */
|
|
220
|
+
interface TriggerOptions {
|
|
221
|
+
/** If provided, opens the widget and shows this message */
|
|
222
|
+
widgetMessage?: string;
|
|
223
|
+
}
|
|
156
224
|
interface ConnectResponse {
|
|
157
225
|
sessionId: string;
|
|
158
226
|
visitorId: string;
|
|
159
227
|
operatorOnline: boolean;
|
|
160
228
|
welcomeMessage?: string;
|
|
161
229
|
messages: Message[];
|
|
230
|
+
/** Tracked elements configuration (for SaaS auto-tracking) */
|
|
231
|
+
trackedElements?: TrackedElement[];
|
|
162
232
|
}
|
|
163
233
|
interface SendMessageRequest {
|
|
164
234
|
sessionId: string;
|
|
@@ -396,4 +466,4 @@ declare class MemoryStorage implements Storage {
|
|
|
396
466
|
cleanupOldSessions(olderThan: Date): Promise<number>;
|
|
397
467
|
}
|
|
398
468
|
|
|
399
|
-
export { type AIProvider, type Bridge, type ConnectRequest, type ConnectResponse, type CustomEvent, type CustomEventHandler, MemoryStorage, type Message, PocketPing, type PocketPingConfig, type PresenceResponse, type SendMessageRequest, type SendMessageResponse, type Session, type Storage, type WebhookPayload };
|
|
469
|
+
export { type AIProvider, type Bridge, type ConnectRequest, type ConnectResponse, type CustomEvent, type CustomEventHandler, MemoryStorage, type Message, PocketPing, type PocketPingConfig, type PresenceResponse, type SendMessageRequest, type SendMessageResponse, type Session, type Storage, type TrackedElement, type TriggerOptions, type WebhookPayload };
|
package/dist/index.d.ts
CHANGED
|
@@ -28,13 +28,15 @@ interface Bridge {
|
|
|
28
28
|
/** Called when a new chat session is created */
|
|
29
29
|
onNewSession?(session: Session): void | Promise<void>;
|
|
30
30
|
/** Called when a visitor sends a message */
|
|
31
|
-
|
|
31
|
+
onVisitorMessage?(message: Message, session: Session): void | Promise<void>;
|
|
32
|
+
/** Called when an operator sends a message (for cross-bridge sync) */
|
|
33
|
+
onOperatorMessage?(message: Message, session: Session, sourceBridge?: string, operatorName?: string): void | Promise<void>;
|
|
32
34
|
/** Called when visitor starts/stops typing */
|
|
33
35
|
onTyping?(sessionId: string, isTyping: boolean): void | Promise<void>;
|
|
34
36
|
/** Called when messages are marked as delivered/read */
|
|
35
|
-
onMessageRead?(sessionId: string, messageIds: string[], status: MessageStatus): void | Promise<void>;
|
|
37
|
+
onMessageRead?(sessionId: string, messageIds: string[], status: MessageStatus, session: Session): void | Promise<void>;
|
|
36
38
|
/** Called when a custom event is triggered from the widget */
|
|
37
|
-
|
|
39
|
+
onCustomEvent?(event: CustomEvent, session: Session): void | Promise<void>;
|
|
38
40
|
/** Called when a user identifies themselves via PocketPing.identify() */
|
|
39
41
|
onIdentityUpdate?(session: Session): void | Promise<void>;
|
|
40
42
|
/** Cleanup when bridge is removed */
|
|
@@ -54,6 +56,52 @@ interface AIProvider {
|
|
|
54
56
|
isAvailable(): Promise<boolean>;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* IP Filtering utilities for PocketPing SDK
|
|
61
|
+
* Supports CIDR notation and individual IP addresses
|
|
62
|
+
*/
|
|
63
|
+
type IpFilterMode = 'allowlist' | 'blocklist' | 'both';
|
|
64
|
+
interface IpFilterConfig {
|
|
65
|
+
/** Enable/disable IP filtering (default: false) */
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
/** Filter mode (default: 'blocklist') */
|
|
68
|
+
mode?: IpFilterMode;
|
|
69
|
+
/** IPs/CIDRs to allow (e.g., ['192.168.1.0/24', '10.0.0.1']) */
|
|
70
|
+
allowlist?: string[];
|
|
71
|
+
/** IPs/CIDRs to block (e.g., ['203.0.113.0/24', '198.51.100.50']) */
|
|
72
|
+
blocklist?: string[];
|
|
73
|
+
/** Custom filter callback for advanced logic */
|
|
74
|
+
customFilter?: IpFilterCallback;
|
|
75
|
+
/** Log blocked requests for security auditing (default: true) */
|
|
76
|
+
logBlocked?: boolean;
|
|
77
|
+
/** Custom logger function */
|
|
78
|
+
logger?: (event: IpFilterLogEvent) => void;
|
|
79
|
+
/** HTTP status code for blocked requests (default: 403) */
|
|
80
|
+
blockedStatusCode?: number;
|
|
81
|
+
/** Response message for blocked requests (default: 'Forbidden') */
|
|
82
|
+
blockedMessage?: string;
|
|
83
|
+
/** Trust proxy headers (X-Forwarded-For, etc.) (default: true) */
|
|
84
|
+
trustProxy?: boolean;
|
|
85
|
+
/** Ordered list of headers to check for client IP */
|
|
86
|
+
proxyHeaders?: string[];
|
|
87
|
+
}
|
|
88
|
+
interface IpFilterLogEvent {
|
|
89
|
+
type: 'blocked' | 'allowed';
|
|
90
|
+
ip: string;
|
|
91
|
+
reason: 'allowlist' | 'blocklist' | 'custom' | 'not_in_allowlist' | 'default';
|
|
92
|
+
path: string;
|
|
93
|
+
timestamp: Date;
|
|
94
|
+
sessionId?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Custom IP filter callback
|
|
98
|
+
* Return true to allow, false to block, undefined to defer to list-based filtering
|
|
99
|
+
*/
|
|
100
|
+
type IpFilterCallback = (ip: string, request: {
|
|
101
|
+
path: string;
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
}) => boolean | undefined | Promise<boolean | undefined>;
|
|
104
|
+
|
|
57
105
|
interface PocketPingConfig {
|
|
58
106
|
/** Storage adapter for sessions and messages */
|
|
59
107
|
storage?: Storage | 'memory';
|
|
@@ -87,6 +135,8 @@ interface PocketPingConfig {
|
|
|
87
135
|
versionWarningMessage?: string;
|
|
88
136
|
/** URL to upgrade instructions */
|
|
89
137
|
versionUpgradeUrl?: string;
|
|
138
|
+
/** IP filtering configuration (allowlist/blocklist) */
|
|
139
|
+
ipFilter?: IpFilterConfig;
|
|
90
140
|
}
|
|
91
141
|
interface AIConfig {
|
|
92
142
|
provider: AIProvider | 'openai' | 'gemini' | 'anthropic';
|
|
@@ -153,12 +203,32 @@ interface ConnectRequest {
|
|
|
153
203
|
/** User identity if already identified */
|
|
154
204
|
identity?: UserIdentity;
|
|
155
205
|
}
|
|
206
|
+
/** Tracked element configuration (for SaaS auto-tracking) */
|
|
207
|
+
interface TrackedElement {
|
|
208
|
+
/** CSS selector for the element(s) to track */
|
|
209
|
+
selector: string;
|
|
210
|
+
/** DOM event to listen for (default: 'click') */
|
|
211
|
+
event?: 'click' | 'submit' | 'focus' | 'change' | 'mouseenter';
|
|
212
|
+
/** Event name sent to backend */
|
|
213
|
+
name: string;
|
|
214
|
+
/** If provided, opens widget with this message when triggered */
|
|
215
|
+
widgetMessage?: string;
|
|
216
|
+
/** Additional data to send with the event */
|
|
217
|
+
data?: Record<string, unknown>;
|
|
218
|
+
}
|
|
219
|
+
/** Options for trigger() method */
|
|
220
|
+
interface TriggerOptions {
|
|
221
|
+
/** If provided, opens the widget and shows this message */
|
|
222
|
+
widgetMessage?: string;
|
|
223
|
+
}
|
|
156
224
|
interface ConnectResponse {
|
|
157
225
|
sessionId: string;
|
|
158
226
|
visitorId: string;
|
|
159
227
|
operatorOnline: boolean;
|
|
160
228
|
welcomeMessage?: string;
|
|
161
229
|
messages: Message[];
|
|
230
|
+
/** Tracked elements configuration (for SaaS auto-tracking) */
|
|
231
|
+
trackedElements?: TrackedElement[];
|
|
162
232
|
}
|
|
163
233
|
interface SendMessageRequest {
|
|
164
234
|
sessionId: string;
|
|
@@ -396,4 +466,4 @@ declare class MemoryStorage implements Storage {
|
|
|
396
466
|
cleanupOldSessions(olderThan: Date): Promise<number>;
|
|
397
467
|
}
|
|
398
468
|
|
|
399
|
-
export { type AIProvider, type Bridge, type ConnectRequest, type ConnectResponse, type CustomEvent, type CustomEventHandler, MemoryStorage, type Message, PocketPing, type PocketPingConfig, type PresenceResponse, type SendMessageRequest, type SendMessageResponse, type Session, type Storage, type WebhookPayload };
|
|
469
|
+
export { type AIProvider, type Bridge, type ConnectRequest, type ConnectResponse, type CustomEvent, type CustomEventHandler, MemoryStorage, type Message, PocketPing, type PocketPingConfig, type PresenceResponse, type SendMessageRequest, type SendMessageResponse, type Session, type Storage, type TrackedElement, type TriggerOptions, type WebhookPayload };
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,6 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
MemoryStorage: () => MemoryStorage,
|
|
24
|
-
PocketPing: () => PocketPing
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(index_exports);
|
|
27
|
-
|
|
28
1
|
// src/pocketping.ts
|
|
29
|
-
|
|
30
|
-
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
31
4
|
|
|
32
5
|
// src/storage/memory.ts
|
|
33
6
|
var MemoryStorage = class {
|
|
@@ -92,6 +65,71 @@ var MemoryStorage = class {
|
|
|
92
65
|
}
|
|
93
66
|
};
|
|
94
67
|
|
|
68
|
+
// src/utils/ip-filter.ts
|
|
69
|
+
function ipToNumber(ip) {
|
|
70
|
+
const parts = ip.split(".");
|
|
71
|
+
if (parts.length !== 4) return null;
|
|
72
|
+
let num = 0;
|
|
73
|
+
for (const part of parts) {
|
|
74
|
+
const n = parseInt(part, 10);
|
|
75
|
+
if (isNaN(n) || n < 0 || n > 255) return null;
|
|
76
|
+
num = num << 8 | n;
|
|
77
|
+
}
|
|
78
|
+
return num >>> 0;
|
|
79
|
+
}
|
|
80
|
+
function parseCidr(cidr) {
|
|
81
|
+
const [ip, bits] = cidr.split("/");
|
|
82
|
+
const base = ipToNumber(ip);
|
|
83
|
+
if (base === null) return null;
|
|
84
|
+
const prefix = bits ? parseInt(bits, 10) : 32;
|
|
85
|
+
if (isNaN(prefix) || prefix < 0 || prefix > 32) return null;
|
|
86
|
+
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
87
|
+
return { base: (base & mask) >>> 0, mask };
|
|
88
|
+
}
|
|
89
|
+
function ipMatchesCidr(ip, cidr) {
|
|
90
|
+
const ipNum = ipToNumber(ip);
|
|
91
|
+
if (ipNum === null) return false;
|
|
92
|
+
const parsed = parseCidr(cidr);
|
|
93
|
+
if (!parsed) return false;
|
|
94
|
+
return (ipNum & parsed.mask) >>> 0 === parsed.base;
|
|
95
|
+
}
|
|
96
|
+
function ipMatchesAny(ip, list) {
|
|
97
|
+
return list.some((entry) => ipMatchesCidr(ip, entry));
|
|
98
|
+
}
|
|
99
|
+
function shouldAllowIp(ip, config) {
|
|
100
|
+
const { mode = "blocklist", allowlist = [], blocklist = [] } = config;
|
|
101
|
+
switch (mode) {
|
|
102
|
+
case "allowlist":
|
|
103
|
+
if (ipMatchesAny(ip, allowlist)) {
|
|
104
|
+
return { allowed: true, reason: "allowlist" };
|
|
105
|
+
}
|
|
106
|
+
return { allowed: false, reason: "not_in_allowlist" };
|
|
107
|
+
case "blocklist":
|
|
108
|
+
if (ipMatchesAny(ip, blocklist)) {
|
|
109
|
+
return { allowed: false, reason: "blocklist" };
|
|
110
|
+
}
|
|
111
|
+
return { allowed: true, reason: "default" };
|
|
112
|
+
case "both":
|
|
113
|
+
if (ipMatchesAny(ip, allowlist)) {
|
|
114
|
+
return { allowed: true, reason: "allowlist" };
|
|
115
|
+
}
|
|
116
|
+
if (ipMatchesAny(ip, blocklist)) {
|
|
117
|
+
return { allowed: false, reason: "blocklist" };
|
|
118
|
+
}
|
|
119
|
+
return { allowed: true, reason: "default" };
|
|
120
|
+
default:
|
|
121
|
+
return { allowed: true, reason: "default" };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function checkIpFilter(ip, config, requestInfo) {
|
|
125
|
+
if (config.customFilter) {
|
|
126
|
+
const customResult = await config.customFilter(ip, requestInfo);
|
|
127
|
+
if (customResult === true) return { allowed: true, reason: "custom" };
|
|
128
|
+
if (customResult === false) return { allowed: false, reason: "custom" };
|
|
129
|
+
}
|
|
130
|
+
return shouldAllowIp(ip, config);
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
// src/pocketping.ts
|
|
96
134
|
function getClientIp(req) {
|
|
97
135
|
const forwarded = req.headers["x-forwarded-for"];
|
|
@@ -179,6 +217,34 @@ var PocketPing = class {
|
|
|
179
217
|
res.end();
|
|
180
218
|
return;
|
|
181
219
|
}
|
|
220
|
+
if (this.config.ipFilter?.enabled) {
|
|
221
|
+
const clientIp = getClientIp(req);
|
|
222
|
+
const filterResult = await checkIpFilter(clientIp, this.config.ipFilter, {
|
|
223
|
+
path
|
|
224
|
+
});
|
|
225
|
+
if (!filterResult.allowed) {
|
|
226
|
+
if (this.config.ipFilter.logBlocked !== false) {
|
|
227
|
+
const logEvent = {
|
|
228
|
+
type: "blocked",
|
|
229
|
+
ip: clientIp,
|
|
230
|
+
reason: filterResult.reason,
|
|
231
|
+
path,
|
|
232
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
233
|
+
};
|
|
234
|
+
if (this.config.ipFilter.logger) {
|
|
235
|
+
this.config.ipFilter.logger(logEvent);
|
|
236
|
+
} else {
|
|
237
|
+
console.log(`[PocketPing] IP blocked: ${clientIp} - reason: ${filterResult.reason}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
res.statusCode = this.config.ipFilter.blockedStatusCode ?? 403;
|
|
241
|
+
res.setHeader("Content-Type", "application/json");
|
|
242
|
+
res.end(JSON.stringify({
|
|
243
|
+
error: this.config.ipFilter.blockedMessage ?? "Forbidden"
|
|
244
|
+
}));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
182
248
|
const widgetVersion = req.headers["x-pocketping-version"];
|
|
183
249
|
const versionCheck = this.checkWidgetVersion(widgetVersion);
|
|
184
250
|
this.setVersionHeaders(res, versionCheck);
|
|
@@ -283,11 +349,35 @@ var PocketPing = class {
|
|
|
283
349
|
// ─────────────────────────────────────────────────────────────────
|
|
284
350
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
351
|
attachWebSocket(server) {
|
|
286
|
-
this.wss = new
|
|
352
|
+
this.wss = new WebSocketServer({
|
|
287
353
|
server,
|
|
288
354
|
path: "/pocketping/stream"
|
|
289
355
|
});
|
|
290
|
-
this.wss.on("connection", (ws, req) => {
|
|
356
|
+
this.wss.on("connection", async (ws, req) => {
|
|
357
|
+
if (this.config.ipFilter?.enabled) {
|
|
358
|
+
const clientIp = getClientIp(req);
|
|
359
|
+
const filterResult = await checkIpFilter(clientIp, this.config.ipFilter, {
|
|
360
|
+
path: "/pocketping/stream"
|
|
361
|
+
});
|
|
362
|
+
if (!filterResult.allowed) {
|
|
363
|
+
if (this.config.ipFilter.logBlocked !== false) {
|
|
364
|
+
const logEvent = {
|
|
365
|
+
type: "blocked",
|
|
366
|
+
ip: clientIp,
|
|
367
|
+
reason: filterResult.reason,
|
|
368
|
+
path: "/pocketping/stream",
|
|
369
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
370
|
+
};
|
|
371
|
+
if (this.config.ipFilter.logger) {
|
|
372
|
+
this.config.ipFilter.logger(logEvent);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(`[PocketPing] WS IP blocked: ${clientIp} - reason: ${filterResult.reason}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
ws.close(4003, this.config.ipFilter.blockedMessage ?? "Forbidden");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
291
381
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
292
382
|
const sessionId = url.searchParams.get("sessionId");
|
|
293
383
|
if (!sessionId) {
|
|
@@ -361,7 +451,7 @@ var PocketPing = class {
|
|
|
361
451
|
if (!sockets) return;
|
|
362
452
|
const message = JSON.stringify(event);
|
|
363
453
|
for (const ws of sockets) {
|
|
364
|
-
if (ws.readyState ===
|
|
454
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
365
455
|
ws.send(message);
|
|
366
456
|
}
|
|
367
457
|
}
|
|
@@ -506,7 +596,7 @@ var PocketPing = class {
|
|
|
506
596
|
readAt: status === "read" ? now.toISOString() : void 0
|
|
507
597
|
}
|
|
508
598
|
});
|
|
509
|
-
await this.notifyBridgesRead(request.sessionId, request.messageIds, status);
|
|
599
|
+
await this.notifyBridgesRead(request.sessionId, request.messageIds, status, session);
|
|
510
600
|
return { updated };
|
|
511
601
|
}
|
|
512
602
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -683,7 +773,7 @@ var PocketPing = class {
|
|
|
683
773
|
await bridge.onNewSession?.(args[0]);
|
|
684
774
|
break;
|
|
685
775
|
case "message":
|
|
686
|
-
await bridge.
|
|
776
|
+
await bridge.onVisitorMessage?.(args[0], args[1]);
|
|
687
777
|
break;
|
|
688
778
|
}
|
|
689
779
|
} catch (err) {
|
|
@@ -691,10 +781,10 @@ var PocketPing = class {
|
|
|
691
781
|
}
|
|
692
782
|
}
|
|
693
783
|
}
|
|
694
|
-
async notifyBridgesRead(sessionId, messageIds, status) {
|
|
784
|
+
async notifyBridgesRead(sessionId, messageIds, status, session) {
|
|
695
785
|
for (const bridge of this.bridges) {
|
|
696
786
|
try {
|
|
697
|
-
await bridge.onMessageRead?.(sessionId, messageIds, status);
|
|
787
|
+
await bridge.onMessageRead?.(sessionId, messageIds, status, session);
|
|
698
788
|
} catch (err) {
|
|
699
789
|
console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
|
|
700
790
|
}
|
|
@@ -703,7 +793,7 @@ var PocketPing = class {
|
|
|
703
793
|
async notifyBridgesEvent(event, session) {
|
|
704
794
|
for (const bridge of this.bridges) {
|
|
705
795
|
try {
|
|
706
|
-
await bridge.
|
|
796
|
+
await bridge.onCustomEvent?.(event, session);
|
|
707
797
|
} catch (err) {
|
|
708
798
|
console.error(`[PocketPing] Bridge ${bridge.name} event notification error:`, err);
|
|
709
799
|
}
|
|
@@ -742,7 +832,7 @@ var PocketPing = class {
|
|
|
742
832
|
"Content-Type": "application/json"
|
|
743
833
|
};
|
|
744
834
|
if (this.config.webhookSecret) {
|
|
745
|
-
const signature =
|
|
835
|
+
const signature = createHmac("sha256", this.config.webhookSecret).update(body).digest("hex");
|
|
746
836
|
headers["X-PocketPing-Signature"] = `sha256=${signature}`;
|
|
747
837
|
}
|
|
748
838
|
const timeout = this.config.webhookTimeout ?? 5e3;
|
|
@@ -877,8 +967,7 @@ var PocketPing = class {
|
|
|
877
967
|
});
|
|
878
968
|
}
|
|
879
969
|
};
|
|
880
|
-
|
|
881
|
-
0 && (module.exports = {
|
|
970
|
+
export {
|
|
882
971
|
MemoryStorage,
|
|
883
972
|
PocketPing
|
|
884
|
-
}
|
|
973
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pocketping/sdk-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Node.js SDK for implementing PocketPing protocol",
|
|
5
|
-
"main": "dist/index.
|
|
6
|
-
"module": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
8
16
|
"files": [
|
|
9
17
|
"dist"
|
|
10
18
|
],
|
|
@@ -14,16 +22,20 @@
|
|
|
14
22
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
15
23
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
16
24
|
"test": "vitest run",
|
|
17
|
-
"test:watch": "vitest"
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"lint": "biome check src tests",
|
|
27
|
+
"lint:fix": "biome check --write src tests",
|
|
28
|
+
"format": "biome format --write src tests"
|
|
18
29
|
},
|
|
19
30
|
"dependencies": {
|
|
20
31
|
"ws": "^8.16.0"
|
|
21
32
|
},
|
|
22
33
|
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^1.9.0",
|
|
23
35
|
"@types/ws": "^8.5.10",
|
|
24
36
|
"tsup": "^8.0.0",
|
|
25
37
|
"typescript": "^5.3.0",
|
|
26
|
-
"vitest": "^
|
|
38
|
+
"vitest": "^4.0.18"
|
|
27
39
|
},
|
|
28
40
|
"peerDependencies": {
|
|
29
41
|
"express": "^4.18.0 || ^5.0.0"
|
|
@@ -46,5 +58,22 @@
|
|
|
46
58
|
"type": "git",
|
|
47
59
|
"url": "https://github.com/Ruwad-io/pocketping.git",
|
|
48
60
|
"directory": "packages/sdk-node"
|
|
61
|
+
},
|
|
62
|
+
"release": {
|
|
63
|
+
"extends": "semantic-release-monorepo",
|
|
64
|
+
"branches": [
|
|
65
|
+
"main"
|
|
66
|
+
],
|
|
67
|
+
"plugins": [
|
|
68
|
+
"@semantic-release/commit-analyzer",
|
|
69
|
+
"@semantic-release/release-notes-generator",
|
|
70
|
+
[
|
|
71
|
+
"@semantic-release/exec",
|
|
72
|
+
{
|
|
73
|
+
"prepareCmd": "npm pkg set version=${nextRelease.version}"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"@semantic-release/github"
|
|
77
|
+
]
|
|
49
78
|
}
|
|
50
79
|
}
|