@pocketping/sdk-node 0.1.0 → 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,884 @@
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
+ // src/pocketping.ts
29
+ var import_crypto = require("crypto");
30
+ var import_ws = require("ws");
31
+
32
+ // src/storage/memory.ts
33
+ var MemoryStorage = class {
34
+ constructor() {
35
+ this.sessions = /* @__PURE__ */ new Map();
36
+ this.messages = /* @__PURE__ */ new Map();
37
+ this.messageById = /* @__PURE__ */ new Map();
38
+ }
39
+ async createSession(session) {
40
+ this.sessions.set(session.id, session);
41
+ this.messages.set(session.id, []);
42
+ }
43
+ async getSession(sessionId) {
44
+ return this.sessions.get(sessionId) ?? null;
45
+ }
46
+ async getSessionByVisitorId(visitorId) {
47
+ const visitorSessions = Array.from(this.sessions.values()).filter(
48
+ (s) => s.visitorId === visitorId
49
+ );
50
+ if (visitorSessions.length === 0) return null;
51
+ return visitorSessions.reduce(
52
+ (latest, s) => s.lastActivity > latest.lastActivity ? s : latest
53
+ );
54
+ }
55
+ async updateSession(session) {
56
+ this.sessions.set(session.id, session);
57
+ }
58
+ async deleteSession(sessionId) {
59
+ this.sessions.delete(sessionId);
60
+ this.messages.delete(sessionId);
61
+ }
62
+ async saveMessage(message) {
63
+ const sessionMessages = this.messages.get(message.sessionId) ?? [];
64
+ sessionMessages.push(message);
65
+ this.messages.set(message.sessionId, sessionMessages);
66
+ this.messageById.set(message.id, message);
67
+ }
68
+ async getMessages(sessionId, after, limit = 50) {
69
+ const sessionMessages = this.messages.get(sessionId) ?? [];
70
+ let startIndex = 0;
71
+ if (after) {
72
+ const afterIndex = sessionMessages.findIndex((m) => m.id === after);
73
+ if (afterIndex !== -1) {
74
+ startIndex = afterIndex + 1;
75
+ }
76
+ }
77
+ return sessionMessages.slice(startIndex, startIndex + limit);
78
+ }
79
+ async getMessage(messageId) {
80
+ return this.messageById.get(messageId) ?? null;
81
+ }
82
+ async cleanupOldSessions(olderThan) {
83
+ let count = 0;
84
+ for (const [id, session] of this.sessions) {
85
+ if (session.lastActivity < olderThan) {
86
+ this.sessions.delete(id);
87
+ this.messages.delete(id);
88
+ count++;
89
+ }
90
+ }
91
+ return count;
92
+ }
93
+ };
94
+
95
+ // src/pocketping.ts
96
+ function getClientIp(req) {
97
+ const forwarded = req.headers["x-forwarded-for"];
98
+ if (forwarded) {
99
+ const ip = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(",")[0];
100
+ return ip?.trim() ?? "unknown";
101
+ }
102
+ const realIp = req.headers["x-real-ip"];
103
+ if (realIp) {
104
+ return Array.isArray(realIp) ? realIp[0] ?? "unknown" : realIp;
105
+ }
106
+ return req.socket?.remoteAddress ?? "unknown";
107
+ }
108
+ function parseUserAgent(userAgent) {
109
+ if (!userAgent) {
110
+ return { deviceType: void 0, browser: void 0, os: void 0 };
111
+ }
112
+ const ua = userAgent.toLowerCase();
113
+ let deviceType;
114
+ if (["mobile", "android", "iphone", "ipod"].some((x) => ua.includes(x))) {
115
+ deviceType = "mobile";
116
+ } else if (["ipad", "tablet"].some((x) => ua.includes(x))) {
117
+ deviceType = "tablet";
118
+ } else {
119
+ deviceType = "desktop";
120
+ }
121
+ let browser;
122
+ if (ua.includes("firefox")) browser = "Firefox";
123
+ else if (ua.includes("edg")) browser = "Edge";
124
+ else if (ua.includes("chrome")) browser = "Chrome";
125
+ else if (ua.includes("safari")) browser = "Safari";
126
+ else if (ua.includes("opera") || ua.includes("opr")) browser = "Opera";
127
+ let os;
128
+ if (ua.includes("windows")) os = "Windows";
129
+ else if (ua.includes("mac os") || ua.includes("macos")) os = "macOS";
130
+ else if (ua.includes("linux")) os = "Linux";
131
+ else if (ua.includes("android")) os = "Android";
132
+ else if (ua.includes("iphone") || ua.includes("ipad")) os = "iOS";
133
+ return { deviceType, browser, os };
134
+ }
135
+ function parseVersion(version) {
136
+ return version.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
137
+ }
138
+ function compareVersions(a, b) {
139
+ const vA = parseVersion(a);
140
+ const vB = parseVersion(b);
141
+ const len = Math.max(vA.length, vB.length);
142
+ for (let i = 0; i < len; i++) {
143
+ const numA = vA[i] ?? 0;
144
+ const numB = vB[i] ?? 0;
145
+ if (numA < numB) return -1;
146
+ if (numA > numB) return 1;
147
+ }
148
+ return 0;
149
+ }
150
+ var PocketPing = class {
151
+ constructor(config = {}) {
152
+ this.wss = null;
153
+ this.sessionSockets = /* @__PURE__ */ new Map();
154
+ this.operatorOnline = false;
155
+ this.eventHandlers = /* @__PURE__ */ new Map();
156
+ this.config = config;
157
+ this.storage = this.initStorage(config.storage);
158
+ this.bridges = config.bridges ?? [];
159
+ }
160
+ initStorage(storage) {
161
+ if (!storage || storage === "memory") {
162
+ return new MemoryStorage();
163
+ }
164
+ return storage;
165
+ }
166
+ // ─────────────────────────────────────────────────────────────────
167
+ // Express/Connect Middleware
168
+ // ─────────────────────────────────────────────────────────────────
169
+ middleware() {
170
+ return async (req, res, next) => {
171
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
172
+ const path = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
173
+ res.setHeader("Access-Control-Allow-Origin", "*");
174
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
175
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-PocketPing-Version");
176
+ res.setHeader("Access-Control-Expose-Headers", "X-PocketPing-Version-Status, X-PocketPing-Min-Version, X-PocketPing-Latest-Version, X-PocketPing-Version-Message");
177
+ if (req.method === "OPTIONS") {
178
+ res.statusCode = 204;
179
+ res.end();
180
+ return;
181
+ }
182
+ const widgetVersion = req.headers["x-pocketping-version"];
183
+ const versionCheck = this.checkWidgetVersion(widgetVersion);
184
+ this.setVersionHeaders(res, versionCheck);
185
+ if (!versionCheck.canContinue) {
186
+ res.statusCode = 426;
187
+ res.setHeader("Content-Type", "application/json");
188
+ res.end(JSON.stringify({
189
+ error: "Widget version unsupported",
190
+ message: versionCheck.message,
191
+ minVersion: versionCheck.minVersion,
192
+ upgradeUrl: this.config.versionUpgradeUrl || "https://docs.pocketping.io/widget/installation"
193
+ }));
194
+ return;
195
+ }
196
+ try {
197
+ const body = await this.parseBody(req);
198
+ const query = Object.fromEntries(url.searchParams);
199
+ let result;
200
+ let sessionId;
201
+ switch (path) {
202
+ case "connect": {
203
+ const connectReq = body;
204
+ const clientIp = getClientIp(req);
205
+ const userAgent = req.headers["user-agent"];
206
+ const uaInfo = parseUserAgent(connectReq.metadata?.userAgent ?? userAgent);
207
+ if (connectReq.metadata) {
208
+ connectReq.metadata.ip = clientIp;
209
+ connectReq.metadata.deviceType = connectReq.metadata.deviceType ?? uaInfo.deviceType;
210
+ connectReq.metadata.browser = connectReq.metadata.browser ?? uaInfo.browser;
211
+ connectReq.metadata.os = connectReq.metadata.os ?? uaInfo.os;
212
+ } else {
213
+ connectReq.metadata = {
214
+ ip: clientIp,
215
+ userAgent,
216
+ ...uaInfo
217
+ };
218
+ }
219
+ const connectResult = await this.handleConnect(connectReq);
220
+ sessionId = connectResult.sessionId;
221
+ result = connectResult;
222
+ break;
223
+ }
224
+ case "message":
225
+ result = await this.handleMessage(body);
226
+ break;
227
+ case "messages":
228
+ result = await this.handleGetMessages(query);
229
+ break;
230
+ case "typing":
231
+ result = await this.handleTyping(body);
232
+ break;
233
+ case "presence":
234
+ result = await this.handlePresence();
235
+ break;
236
+ case "read":
237
+ result = await this.handleRead(body);
238
+ break;
239
+ case "identify":
240
+ result = await this.handleIdentify(body);
241
+ break;
242
+ default:
243
+ if (next) {
244
+ next();
245
+ return;
246
+ }
247
+ res.statusCode = 404;
248
+ res.end(JSON.stringify({ error: "Not found" }));
249
+ return;
250
+ }
251
+ res.setHeader("Content-Type", "application/json");
252
+ res.statusCode = 200;
253
+ res.end(JSON.stringify(result));
254
+ if (sessionId && versionCheck.status !== "ok") {
255
+ setTimeout(() => {
256
+ this.sendVersionWarning(sessionId, versionCheck);
257
+ }, 500);
258
+ }
259
+ } catch (error) {
260
+ console.error("[PocketPing] Error:", error);
261
+ res.statusCode = 500;
262
+ res.end(JSON.stringify({ error: "Internal server error" }));
263
+ }
264
+ };
265
+ }
266
+ async parseBody(req) {
267
+ if (req.body) return req.body;
268
+ return new Promise((resolve, reject) => {
269
+ let data = "";
270
+ req.on("data", (chunk) => data += chunk);
271
+ req.on("end", () => {
272
+ try {
273
+ resolve(data ? JSON.parse(data) : {});
274
+ } catch {
275
+ reject(new Error("Invalid JSON"));
276
+ }
277
+ });
278
+ req.on("error", reject);
279
+ });
280
+ }
281
+ // ─────────────────────────────────────────────────────────────────
282
+ // WebSocket
283
+ // ─────────────────────────────────────────────────────────────────
284
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
+ attachWebSocket(server) {
286
+ this.wss = new import_ws.WebSocketServer({
287
+ server,
288
+ path: "/pocketping/stream"
289
+ });
290
+ this.wss.on("connection", (ws, req) => {
291
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
292
+ const sessionId = url.searchParams.get("sessionId");
293
+ if (!sessionId) {
294
+ ws.close(4e3, "sessionId required");
295
+ return;
296
+ }
297
+ if (!this.sessionSockets.has(sessionId)) {
298
+ this.sessionSockets.set(sessionId, /* @__PURE__ */ new Set());
299
+ }
300
+ this.sessionSockets.get(sessionId).add(ws);
301
+ ws.on("close", () => {
302
+ this.sessionSockets.get(sessionId)?.delete(ws);
303
+ });
304
+ ws.on("message", async (data) => {
305
+ try {
306
+ const event = JSON.parse(data.toString());
307
+ await this.handleWebSocketMessage(sessionId, event);
308
+ } catch (err) {
309
+ console.error("[PocketPing] WS message error:", err);
310
+ }
311
+ });
312
+ });
313
+ }
314
+ async handleWebSocketMessage(sessionId, event) {
315
+ switch (event.type) {
316
+ case "typing":
317
+ this.broadcastToSession(sessionId, {
318
+ type: "typing",
319
+ data: event.data
320
+ });
321
+ break;
322
+ case "event":
323
+ const customEvent = event.data;
324
+ customEvent.sessionId = sessionId;
325
+ await this.handleCustomEvent(sessionId, customEvent);
326
+ break;
327
+ }
328
+ }
329
+ async handleCustomEvent(sessionId, event) {
330
+ const session = await this.storage.getSession(sessionId);
331
+ if (!session) {
332
+ console.warn(`[PocketPing] Event received for unknown session: ${sessionId}`);
333
+ return;
334
+ }
335
+ const handlers = this.eventHandlers.get(event.name);
336
+ if (handlers) {
337
+ for (const handler of handlers) {
338
+ try {
339
+ await handler(event, session);
340
+ } catch (err) {
341
+ console.error(`[PocketPing] Event handler error for '${event.name}':`, err);
342
+ }
343
+ }
344
+ }
345
+ const wildcardHandlers = this.eventHandlers.get("*");
346
+ if (wildcardHandlers) {
347
+ for (const handler of wildcardHandlers) {
348
+ try {
349
+ await handler(event, session);
350
+ } catch (err) {
351
+ console.error(`[PocketPing] Wildcard event handler error:`, err);
352
+ }
353
+ }
354
+ }
355
+ await this.config.onEvent?.(event, session);
356
+ await this.notifyBridgesEvent(event, session);
357
+ this.forwardToWebhook(event, session);
358
+ }
359
+ broadcastToSession(sessionId, event) {
360
+ const sockets = this.sessionSockets.get(sessionId);
361
+ if (!sockets) return;
362
+ const message = JSON.stringify(event);
363
+ for (const ws of sockets) {
364
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
365
+ ws.send(message);
366
+ }
367
+ }
368
+ }
369
+ // ─────────────────────────────────────────────────────────────────
370
+ // Protocol Handlers
371
+ // ─────────────────────────────────────────────────────────────────
372
+ async handleConnect(request) {
373
+ let session = null;
374
+ if (request.sessionId) {
375
+ session = await this.storage.getSession(request.sessionId);
376
+ }
377
+ if (!session && this.storage.getSessionByVisitorId) {
378
+ session = await this.storage.getSessionByVisitorId(request.visitorId);
379
+ }
380
+ if (!session) {
381
+ session = {
382
+ id: this.generateId(),
383
+ visitorId: request.visitorId,
384
+ createdAt: /* @__PURE__ */ new Date(),
385
+ lastActivity: /* @__PURE__ */ new Date(),
386
+ operatorOnline: this.operatorOnline,
387
+ aiActive: false,
388
+ metadata: request.metadata,
389
+ identity: request.identity
390
+ };
391
+ await this.storage.createSession(session);
392
+ await this.notifyBridges("new_session", session);
393
+ await this.config.onNewSession?.(session);
394
+ } else {
395
+ let needsUpdate = false;
396
+ if (request.metadata) {
397
+ if (session.metadata) {
398
+ request.metadata.ip = session.metadata.ip ?? request.metadata.ip;
399
+ request.metadata.country = session.metadata.country ?? request.metadata.country;
400
+ request.metadata.city = session.metadata.city ?? request.metadata.city;
401
+ }
402
+ session.metadata = request.metadata;
403
+ needsUpdate = true;
404
+ }
405
+ if (request.identity) {
406
+ session.identity = request.identity;
407
+ needsUpdate = true;
408
+ }
409
+ if (needsUpdate) {
410
+ session.lastActivity = /* @__PURE__ */ new Date();
411
+ await this.storage.updateSession(session);
412
+ }
413
+ }
414
+ const messages = await this.storage.getMessages(session.id);
415
+ return {
416
+ sessionId: session.id,
417
+ visitorId: session.visitorId,
418
+ operatorOnline: this.operatorOnline,
419
+ welcomeMessage: this.config.welcomeMessage,
420
+ messages
421
+ };
422
+ }
423
+ async handleMessage(request) {
424
+ const session = await this.storage.getSession(request.sessionId);
425
+ if (!session) {
426
+ throw new Error("Session not found");
427
+ }
428
+ const message = {
429
+ id: this.generateId(),
430
+ sessionId: request.sessionId,
431
+ content: request.content,
432
+ sender: request.sender,
433
+ timestamp: /* @__PURE__ */ new Date(),
434
+ replyTo: request.replyTo
435
+ };
436
+ await this.storage.saveMessage(message);
437
+ session.lastActivity = /* @__PURE__ */ new Date();
438
+ await this.storage.updateSession(session);
439
+ if (request.sender === "visitor") {
440
+ await this.notifyBridges("message", message, session);
441
+ }
442
+ this.broadcastToSession(request.sessionId, {
443
+ type: "message",
444
+ data: message
445
+ });
446
+ await this.config.onMessage?.(message, session);
447
+ return {
448
+ messageId: message.id,
449
+ timestamp: message.timestamp.toISOString()
450
+ };
451
+ }
452
+ async handleGetMessages(request) {
453
+ const limit = Math.min(request.limit ?? 50, 100);
454
+ const messages = await this.storage.getMessages(request.sessionId, request.after, limit + 1);
455
+ return {
456
+ messages: messages.slice(0, limit),
457
+ hasMore: messages.length > limit
458
+ };
459
+ }
460
+ async handleTyping(request) {
461
+ this.broadcastToSession(request.sessionId, {
462
+ type: "typing",
463
+ data: {
464
+ sessionId: request.sessionId,
465
+ sender: request.sender,
466
+ isTyping: request.isTyping ?? true
467
+ }
468
+ });
469
+ return { ok: true };
470
+ }
471
+ async handlePresence() {
472
+ return {
473
+ online: this.operatorOnline,
474
+ aiEnabled: !!this.config.ai,
475
+ aiActiveAfter: this.config.aiTakeoverDelay ?? 300
476
+ };
477
+ }
478
+ async handleRead(request) {
479
+ const session = await this.storage.getSession(request.sessionId);
480
+ if (!session) {
481
+ throw new Error("Session not found");
482
+ }
483
+ const status = request.status ?? "read";
484
+ const now = /* @__PURE__ */ new Date();
485
+ let updated = 0;
486
+ const messages = await this.storage.getMessages(request.sessionId);
487
+ for (const msg of messages) {
488
+ if (request.messageIds.includes(msg.id)) {
489
+ msg.status = status;
490
+ if (status === "delivered") {
491
+ msg.deliveredAt = now;
492
+ } else if (status === "read") {
493
+ msg.deliveredAt = msg.deliveredAt ?? now;
494
+ msg.readAt = now;
495
+ }
496
+ await this.storage.saveMessage(msg);
497
+ updated++;
498
+ }
499
+ }
500
+ this.broadcastToSession(request.sessionId, {
501
+ type: "read",
502
+ data: {
503
+ messageIds: request.messageIds,
504
+ status,
505
+ deliveredAt: status === "delivered" ? now.toISOString() : void 0,
506
+ readAt: status === "read" ? now.toISOString() : void 0
507
+ }
508
+ });
509
+ await this.notifyBridgesRead(request.sessionId, request.messageIds, status, session);
510
+ return { updated };
511
+ }
512
+ // ─────────────────────────────────────────────────────────────────
513
+ // User Identity
514
+ // ─────────────────────────────────────────────────────────────────
515
+ /**
516
+ * Handle user identification from widget
517
+ * Called when visitor calls PocketPing.identify()
518
+ */
519
+ async handleIdentify(request) {
520
+ if (!request.identity?.id) {
521
+ throw new Error("identity.id is required");
522
+ }
523
+ const session = await this.storage.getSession(request.sessionId);
524
+ if (!session) {
525
+ throw new Error("Session not found");
526
+ }
527
+ session.identity = request.identity;
528
+ session.lastActivity = /* @__PURE__ */ new Date();
529
+ await this.storage.updateSession(session);
530
+ await this.notifyBridgesIdentity(session);
531
+ await this.config.onIdentify?.(session);
532
+ this.forwardIdentityToWebhook(session);
533
+ return { ok: true };
534
+ }
535
+ /**
536
+ * Get a session by ID
537
+ */
538
+ async getSession(sessionId) {
539
+ return this.storage.getSession(sessionId);
540
+ }
541
+ // ─────────────────────────────────────────────────────────────────
542
+ // Operator Actions (for bridges)
543
+ // ─────────────────────────────────────────────────────────────────
544
+ async sendOperatorMessage(sessionId, content) {
545
+ const response = await this.handleMessage({
546
+ sessionId,
547
+ content,
548
+ sender: "operator"
549
+ });
550
+ const message = {
551
+ id: response.messageId,
552
+ sessionId,
553
+ content,
554
+ sender: "operator",
555
+ timestamp: new Date(response.timestamp)
556
+ };
557
+ return message;
558
+ }
559
+ setOperatorOnline(online) {
560
+ this.operatorOnline = online;
561
+ for (const sessionId of this.sessionSockets.keys()) {
562
+ this.broadcastToSession(sessionId, {
563
+ type: "presence",
564
+ data: { online }
565
+ });
566
+ }
567
+ }
568
+ // ─────────────────────────────────────────────────────────────────
569
+ // Custom Events (bidirectional)
570
+ // ─────────────────────────────────────────────────────────────────
571
+ /**
572
+ * Subscribe to custom events from widgets
573
+ * @param eventName - The name of the event to listen for, or '*' for all events
574
+ * @param handler - Callback function when event is received
575
+ * @returns Unsubscribe function
576
+ * @example
577
+ * // Listen for specific event
578
+ * pp.onEvent('clicked_pricing', async (event, session) => {
579
+ * console.log(`User ${session.visitorId} clicked pricing: ${event.data?.plan}`)
580
+ * })
581
+ *
582
+ * // Listen for all events
583
+ * pp.onEvent('*', async (event, session) => {
584
+ * console.log(`Event: ${event.name}`, event.data)
585
+ * })
586
+ */
587
+ onEvent(eventName, handler) {
588
+ if (!this.eventHandlers.has(eventName)) {
589
+ this.eventHandlers.set(eventName, /* @__PURE__ */ new Set());
590
+ }
591
+ this.eventHandlers.get(eventName).add(handler);
592
+ return () => {
593
+ this.eventHandlers.get(eventName)?.delete(handler);
594
+ };
595
+ }
596
+ /**
597
+ * Unsubscribe from a custom event
598
+ * @param eventName - The name of the event
599
+ * @param handler - The handler to remove
600
+ */
601
+ offEvent(eventName, handler) {
602
+ this.eventHandlers.get(eventName)?.delete(handler);
603
+ }
604
+ /**
605
+ * Send a custom event to a specific widget/session
606
+ * @param sessionId - The session ID to send the event to
607
+ * @param eventName - The name of the event
608
+ * @param data - Optional payload to send with the event
609
+ * @example
610
+ * // Send a promotion offer to a specific user
611
+ * pp.emitEvent('session-123', 'show_offer', {
612
+ * discount: 20,
613
+ * code: 'SAVE20',
614
+ * message: 'Special offer just for you!'
615
+ * })
616
+ */
617
+ emitEvent(sessionId, eventName, data) {
618
+ const event = {
619
+ name: eventName,
620
+ data,
621
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
622
+ sessionId
623
+ };
624
+ this.broadcastToSession(sessionId, {
625
+ type: "event",
626
+ data: event
627
+ });
628
+ }
629
+ /**
630
+ * Broadcast a custom event to all connected widgets
631
+ * @param eventName - The name of the event
632
+ * @param data - Optional payload to send with the event
633
+ * @example
634
+ * // Notify all users about maintenance
635
+ * pp.broadcastEvent('maintenance_warning', {
636
+ * message: 'Site will be down for maintenance in 5 minutes'
637
+ * })
638
+ */
639
+ broadcastEvent(eventName, data) {
640
+ const event = {
641
+ name: eventName,
642
+ data,
643
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
644
+ };
645
+ for (const sessionId of this.sessionSockets.keys()) {
646
+ event.sessionId = sessionId;
647
+ this.broadcastToSession(sessionId, {
648
+ type: "event",
649
+ data: event
650
+ });
651
+ }
652
+ }
653
+ /**
654
+ * Process a custom event server-side (runs handlers, bridges, webhooks)
655
+ * Useful for server-side automation or triggering events programmatically
656
+ * @param sessionId - The session ID to associate with the event
657
+ * @param eventName - The name of the event
658
+ * @param data - Optional payload for the event
659
+ * @example
660
+ * // Trigger event from backend logic (e.g., after purchase)
661
+ * await pp.triggerEvent('session-123', 'purchase_completed', {
662
+ * orderId: 'order-456',
663
+ * amount: 99.99
664
+ * })
665
+ */
666
+ async triggerEvent(sessionId, eventName, data) {
667
+ const event = {
668
+ name: eventName,
669
+ data,
670
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
671
+ sessionId
672
+ };
673
+ await this.handleCustomEvent(sessionId, event);
674
+ }
675
+ // ─────────────────────────────────────────────────────────────────
676
+ // Bridges
677
+ // ─────────────────────────────────────────────────────────────────
678
+ async notifyBridges(event, ...args) {
679
+ for (const bridge of this.bridges) {
680
+ try {
681
+ switch (event) {
682
+ case "new_session":
683
+ await bridge.onNewSession?.(args[0]);
684
+ break;
685
+ case "message":
686
+ await bridge.onVisitorMessage?.(args[0], args[1]);
687
+ break;
688
+ }
689
+ } catch (err) {
690
+ console.error(`[PocketPing] Bridge ${bridge.name} error:`, err);
691
+ }
692
+ }
693
+ }
694
+ async notifyBridgesRead(sessionId, messageIds, status, session) {
695
+ for (const bridge of this.bridges) {
696
+ try {
697
+ await bridge.onMessageRead?.(sessionId, messageIds, status, session);
698
+ } catch (err) {
699
+ console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
700
+ }
701
+ }
702
+ }
703
+ async notifyBridgesEvent(event, session) {
704
+ for (const bridge of this.bridges) {
705
+ try {
706
+ await bridge.onCustomEvent?.(event, session);
707
+ } catch (err) {
708
+ console.error(`[PocketPing] Bridge ${bridge.name} event notification error:`, err);
709
+ }
710
+ }
711
+ }
712
+ async notifyBridgesIdentity(session) {
713
+ for (const bridge of this.bridges) {
714
+ try {
715
+ await bridge.onIdentityUpdate?.(session);
716
+ } catch (err) {
717
+ console.error(`[PocketPing] Bridge ${bridge.name} identity notification error:`, err);
718
+ }
719
+ }
720
+ }
721
+ // ─────────────────────────────────────────────────────────────────
722
+ // Webhook Forwarding
723
+ // ─────────────────────────────────────────────────────────────────
724
+ /**
725
+ * Forward custom event to configured webhook URL (non-blocking)
726
+ * Used for integrations with Zapier, Make, n8n, or custom backends
727
+ */
728
+ forwardToWebhook(event, session) {
729
+ if (!this.config.webhookUrl) return;
730
+ const payload = {
731
+ event,
732
+ session: {
733
+ id: session.id,
734
+ visitorId: session.visitorId,
735
+ metadata: session.metadata,
736
+ identity: session.identity
737
+ },
738
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
739
+ };
740
+ const body = JSON.stringify(payload);
741
+ const headers = {
742
+ "Content-Type": "application/json"
743
+ };
744
+ if (this.config.webhookSecret) {
745
+ const signature = (0, import_crypto.createHmac)("sha256", this.config.webhookSecret).update(body).digest("hex");
746
+ headers["X-PocketPing-Signature"] = `sha256=${signature}`;
747
+ }
748
+ const timeout = this.config.webhookTimeout ?? 5e3;
749
+ const controller = new AbortController();
750
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
751
+ fetch(this.config.webhookUrl, {
752
+ method: "POST",
753
+ headers,
754
+ body,
755
+ signal: controller.signal
756
+ }).then((response) => {
757
+ clearTimeout(timeoutId);
758
+ if (!response.ok) {
759
+ console.error(`[PocketPing] Webhook returned ${response.status}: ${response.statusText}`);
760
+ }
761
+ }).catch((err) => {
762
+ clearTimeout(timeoutId);
763
+ if (err.name === "AbortError") {
764
+ console.error(`[PocketPing] Webhook timed out after ${timeout}ms`);
765
+ } else {
766
+ console.error(`[PocketPing] Webhook error:`, err.message);
767
+ }
768
+ });
769
+ }
770
+ /**
771
+ * Forward identity update to webhook as a special event
772
+ */
773
+ forwardIdentityToWebhook(session) {
774
+ if (!this.config.webhookUrl || !session.identity) return;
775
+ const event = {
776
+ name: "identify",
777
+ data: session.identity,
778
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
779
+ sessionId: session.id
780
+ };
781
+ this.forwardToWebhook(event, session);
782
+ }
783
+ addBridge(bridge) {
784
+ this.bridges.push(bridge);
785
+ bridge.init?.(this);
786
+ }
787
+ // ─────────────────────────────────────────────────────────────────
788
+ // Utilities
789
+ // ─────────────────────────────────────────────────────────────────
790
+ generateId() {
791
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
792
+ }
793
+ getStorage() {
794
+ return this.storage;
795
+ }
796
+ // ─────────────────────────────────────────────────────────────────
797
+ // Version Management
798
+ // ─────────────────────────────────────────────────────────────────
799
+ /**
800
+ * Check widget version against configured min/latest versions
801
+ * @param widgetVersion - Version from X-PocketPing-Version header
802
+ * @returns Version check result with status and headers to set
803
+ */
804
+ checkWidgetVersion(widgetVersion) {
805
+ if (!widgetVersion) {
806
+ return {
807
+ status: "ok",
808
+ canContinue: true
809
+ };
810
+ }
811
+ const { minWidgetVersion, latestWidgetVersion } = this.config;
812
+ if (!minWidgetVersion && !latestWidgetVersion) {
813
+ return {
814
+ status: "ok",
815
+ canContinue: true
816
+ };
817
+ }
818
+ let status = "ok";
819
+ let message;
820
+ let canContinue = true;
821
+ if (minWidgetVersion && compareVersions(widgetVersion, minWidgetVersion) < 0) {
822
+ status = "unsupported";
823
+ message = this.config.versionWarningMessage || `Widget version ${widgetVersion} is no longer supported. Minimum version: ${minWidgetVersion}`;
824
+ canContinue = false;
825
+ } else if (latestWidgetVersion && compareVersions(widgetVersion, latestWidgetVersion) < 0) {
826
+ const majorDiff = parseVersion(latestWidgetVersion)[0] - parseVersion(widgetVersion)[0];
827
+ if (majorDiff >= 1) {
828
+ status = "deprecated";
829
+ message = this.config.versionWarningMessage || `Widget version ${widgetVersion} is deprecated. Please update to ${latestWidgetVersion}`;
830
+ } else {
831
+ status = "outdated";
832
+ message = `A newer widget version ${latestWidgetVersion} is available`;
833
+ }
834
+ }
835
+ return {
836
+ status,
837
+ message,
838
+ minVersion: minWidgetVersion,
839
+ latestVersion: latestWidgetVersion,
840
+ canContinue
841
+ };
842
+ }
843
+ /**
844
+ * Set version warning headers on HTTP response
845
+ */
846
+ setVersionHeaders(res, versionCheck) {
847
+ if (versionCheck.status !== "ok") {
848
+ res.setHeader("X-PocketPing-Version-Status", versionCheck.status);
849
+ if (versionCheck.minVersion) {
850
+ res.setHeader("X-PocketPing-Min-Version", versionCheck.minVersion);
851
+ }
852
+ if (versionCheck.latestVersion) {
853
+ res.setHeader("X-PocketPing-Latest-Version", versionCheck.latestVersion);
854
+ }
855
+ if (versionCheck.message) {
856
+ res.setHeader("X-PocketPing-Version-Message", versionCheck.message);
857
+ }
858
+ }
859
+ }
860
+ /**
861
+ * Send version warning via WebSocket to a session
862
+ */
863
+ sendVersionWarning(sessionId, versionCheck) {
864
+ if (versionCheck.status === "ok") return;
865
+ this.broadcastToSession(sessionId, {
866
+ type: "version_warning",
867
+ data: {
868
+ severity: versionCheck.status === "unsupported" ? "error" : versionCheck.status === "deprecated" ? "warning" : "info",
869
+ message: versionCheck.message,
870
+ currentVersion: "unknown",
871
+ // Will be filled by widget
872
+ minVersion: versionCheck.minVersion,
873
+ latestVersion: versionCheck.latestVersion,
874
+ canContinue: versionCheck.canContinue,
875
+ upgradeUrl: this.config.versionUpgradeUrl || "https://docs.pocketping.io/widget/installation"
876
+ }
877
+ });
878
+ }
879
+ };
880
+ // Annotate the CommonJS export names for ESM import in node:
881
+ 0 && (module.exports = {
882
+ MemoryStorage,
883
+ PocketPing
884
+ });