@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 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
- import { createHmac } from "crypto";
3
- import { WebSocketServer, WebSocket } from "ws";
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.onMessage?.(args[0], args[1]);
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.onEvent?.(event, session);
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
- onMessage?(message: Message, session: Session): void | Promise<void>;
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
- onEvent?(event: CustomEvent, session: Session): void | Promise<void>;
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
- onMessage?(message: Message, session: Session): void | Promise<void>;
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
- onEvent?(event: CustomEvent, session: Session): void | Promise<void>;
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
- var import_crypto = require("crypto");
30
- var import_ws = require("ws");
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 import_ws.WebSocketServer({
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 === import_ws.WebSocket.OPEN) {
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.onMessage?.(args[0], args[1]);
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.onEvent?.(event, session);
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 = (0, import_crypto.createHmac)("sha256", this.config.webhookSecret).update(body).digest("hex");
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
- // Annotate the CommonJS export names for ESM import in node:
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": "0.2.0",
3
+ "version": "1.1.0",
4
+ "type": "module",
4
5
  "description": "Node.js SDK for implementing PocketPing protocol",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
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": "^1.2.0"
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
  }