@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/README.md +305 -0
- package/dist/index.cjs +884 -0
- package/dist/index.d.cts +421 -0
- package/dist/index.d.ts +202 -3
- package/dist/index.js +408 -48
- package/package.json +33 -5
- package/dist/index.d.mts +0 -222
- package/dist/index.mjs +0 -468
package/dist/index.js
CHANGED
|
@@ -1,32 +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
|
-
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
30
4
|
|
|
31
5
|
// src/storage/memory.ts
|
|
32
6
|
var MemoryStorage = class {
|
|
@@ -131,11 +105,27 @@ function parseUserAgent(userAgent) {
|
|
|
131
105
|
else if (ua.includes("iphone") || ua.includes("ipad")) os = "iOS";
|
|
132
106
|
return { deviceType, browser, os };
|
|
133
107
|
}
|
|
108
|
+
function parseVersion(version) {
|
|
109
|
+
return version.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
110
|
+
}
|
|
111
|
+
function compareVersions(a, b) {
|
|
112
|
+
const vA = parseVersion(a);
|
|
113
|
+
const vB = parseVersion(b);
|
|
114
|
+
const len = Math.max(vA.length, vB.length);
|
|
115
|
+
for (let i = 0; i < len; i++) {
|
|
116
|
+
const numA = vA[i] ?? 0;
|
|
117
|
+
const numB = vB[i] ?? 0;
|
|
118
|
+
if (numA < numB) return -1;
|
|
119
|
+
if (numA > numB) return 1;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
134
123
|
var PocketPing = class {
|
|
135
124
|
constructor(config = {}) {
|
|
136
125
|
this.wss = null;
|
|
137
126
|
this.sessionSockets = /* @__PURE__ */ new Map();
|
|
138
127
|
this.operatorOnline = false;
|
|
128
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
139
129
|
this.config = config;
|
|
140
130
|
this.storage = this.initStorage(config.storage);
|
|
141
131
|
this.bridges = config.bridges ?? [];
|
|
@@ -155,16 +145,32 @@ var PocketPing = class {
|
|
|
155
145
|
const path = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
156
146
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
157
147
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
158
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
148
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-PocketPing-Version");
|
|
149
|
+
res.setHeader("Access-Control-Expose-Headers", "X-PocketPing-Version-Status, X-PocketPing-Min-Version, X-PocketPing-Latest-Version, X-PocketPing-Version-Message");
|
|
159
150
|
if (req.method === "OPTIONS") {
|
|
160
151
|
res.statusCode = 204;
|
|
161
152
|
res.end();
|
|
162
153
|
return;
|
|
163
154
|
}
|
|
155
|
+
const widgetVersion = req.headers["x-pocketping-version"];
|
|
156
|
+
const versionCheck = this.checkWidgetVersion(widgetVersion);
|
|
157
|
+
this.setVersionHeaders(res, versionCheck);
|
|
158
|
+
if (!versionCheck.canContinue) {
|
|
159
|
+
res.statusCode = 426;
|
|
160
|
+
res.setHeader("Content-Type", "application/json");
|
|
161
|
+
res.end(JSON.stringify({
|
|
162
|
+
error: "Widget version unsupported",
|
|
163
|
+
message: versionCheck.message,
|
|
164
|
+
minVersion: versionCheck.minVersion,
|
|
165
|
+
upgradeUrl: this.config.versionUpgradeUrl || "https://docs.pocketping.io/widget/installation"
|
|
166
|
+
}));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
164
169
|
try {
|
|
165
170
|
const body = await this.parseBody(req);
|
|
166
171
|
const query = Object.fromEntries(url.searchParams);
|
|
167
172
|
let result;
|
|
173
|
+
let sessionId;
|
|
168
174
|
switch (path) {
|
|
169
175
|
case "connect": {
|
|
170
176
|
const connectReq = body;
|
|
@@ -183,7 +189,9 @@ var PocketPing = class {
|
|
|
183
189
|
...uaInfo
|
|
184
190
|
};
|
|
185
191
|
}
|
|
186
|
-
|
|
192
|
+
const connectResult = await this.handleConnect(connectReq);
|
|
193
|
+
sessionId = connectResult.sessionId;
|
|
194
|
+
result = connectResult;
|
|
187
195
|
break;
|
|
188
196
|
}
|
|
189
197
|
case "message":
|
|
@@ -201,6 +209,9 @@ var PocketPing = class {
|
|
|
201
209
|
case "read":
|
|
202
210
|
result = await this.handleRead(body);
|
|
203
211
|
break;
|
|
212
|
+
case "identify":
|
|
213
|
+
result = await this.handleIdentify(body);
|
|
214
|
+
break;
|
|
204
215
|
default:
|
|
205
216
|
if (next) {
|
|
206
217
|
next();
|
|
@@ -213,6 +224,11 @@ var PocketPing = class {
|
|
|
213
224
|
res.setHeader("Content-Type", "application/json");
|
|
214
225
|
res.statusCode = 200;
|
|
215
226
|
res.end(JSON.stringify(result));
|
|
227
|
+
if (sessionId && versionCheck.status !== "ok") {
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
this.sendVersionWarning(sessionId, versionCheck);
|
|
230
|
+
}, 500);
|
|
231
|
+
}
|
|
216
232
|
} catch (error) {
|
|
217
233
|
console.error("[PocketPing] Error:", error);
|
|
218
234
|
res.statusCode = 500;
|
|
@@ -240,7 +256,7 @@ var PocketPing = class {
|
|
|
240
256
|
// ─────────────────────────────────────────────────────────────────
|
|
241
257
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
242
258
|
attachWebSocket(server) {
|
|
243
|
-
this.wss = new
|
|
259
|
+
this.wss = new WebSocketServer({
|
|
244
260
|
server,
|
|
245
261
|
path: "/pocketping/stream"
|
|
246
262
|
});
|
|
@@ -276,14 +292,49 @@ var PocketPing = class {
|
|
|
276
292
|
data: event.data
|
|
277
293
|
});
|
|
278
294
|
break;
|
|
295
|
+
case "event":
|
|
296
|
+
const customEvent = event.data;
|
|
297
|
+
customEvent.sessionId = sessionId;
|
|
298
|
+
await this.handleCustomEvent(sessionId, customEvent);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async handleCustomEvent(sessionId, event) {
|
|
303
|
+
const session = await this.storage.getSession(sessionId);
|
|
304
|
+
if (!session) {
|
|
305
|
+
console.warn(`[PocketPing] Event received for unknown session: ${sessionId}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const handlers = this.eventHandlers.get(event.name);
|
|
309
|
+
if (handlers) {
|
|
310
|
+
for (const handler of handlers) {
|
|
311
|
+
try {
|
|
312
|
+
await handler(event, session);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error(`[PocketPing] Event handler error for '${event.name}':`, err);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const wildcardHandlers = this.eventHandlers.get("*");
|
|
319
|
+
if (wildcardHandlers) {
|
|
320
|
+
for (const handler of wildcardHandlers) {
|
|
321
|
+
try {
|
|
322
|
+
await handler(event, session);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(`[PocketPing] Wildcard event handler error:`, err);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
279
327
|
}
|
|
328
|
+
await this.config.onEvent?.(event, session);
|
|
329
|
+
await this.notifyBridgesEvent(event, session);
|
|
330
|
+
this.forwardToWebhook(event, session);
|
|
280
331
|
}
|
|
281
332
|
broadcastToSession(sessionId, event) {
|
|
282
333
|
const sockets = this.sessionSockets.get(sessionId);
|
|
283
334
|
if (!sockets) return;
|
|
284
335
|
const message = JSON.stringify(event);
|
|
285
336
|
for (const ws of sockets) {
|
|
286
|
-
if (ws.readyState ===
|
|
337
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
287
338
|
ws.send(message);
|
|
288
339
|
}
|
|
289
340
|
}
|
|
@@ -307,20 +358,31 @@ var PocketPing = class {
|
|
|
307
358
|
lastActivity: /* @__PURE__ */ new Date(),
|
|
308
359
|
operatorOnline: this.operatorOnline,
|
|
309
360
|
aiActive: false,
|
|
310
|
-
metadata: request.metadata
|
|
361
|
+
metadata: request.metadata,
|
|
362
|
+
identity: request.identity
|
|
311
363
|
};
|
|
312
364
|
await this.storage.createSession(session);
|
|
313
365
|
await this.notifyBridges("new_session", session);
|
|
314
366
|
await this.config.onNewSession?.(session);
|
|
315
|
-
} else
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
367
|
+
} else {
|
|
368
|
+
let needsUpdate = false;
|
|
369
|
+
if (request.metadata) {
|
|
370
|
+
if (session.metadata) {
|
|
371
|
+
request.metadata.ip = session.metadata.ip ?? request.metadata.ip;
|
|
372
|
+
request.metadata.country = session.metadata.country ?? request.metadata.country;
|
|
373
|
+
request.metadata.city = session.metadata.city ?? request.metadata.city;
|
|
374
|
+
}
|
|
375
|
+
session.metadata = request.metadata;
|
|
376
|
+
needsUpdate = true;
|
|
377
|
+
}
|
|
378
|
+
if (request.identity) {
|
|
379
|
+
session.identity = request.identity;
|
|
380
|
+
needsUpdate = true;
|
|
381
|
+
}
|
|
382
|
+
if (needsUpdate) {
|
|
383
|
+
session.lastActivity = /* @__PURE__ */ new Date();
|
|
384
|
+
await this.storage.updateSession(session);
|
|
320
385
|
}
|
|
321
|
-
session.metadata = request.metadata;
|
|
322
|
-
session.lastActivity = /* @__PURE__ */ new Date();
|
|
323
|
-
await this.storage.updateSession(session);
|
|
324
386
|
}
|
|
325
387
|
const messages = await this.storage.getMessages(session.id);
|
|
326
388
|
return {
|
|
@@ -417,10 +479,39 @@ var PocketPing = class {
|
|
|
417
479
|
readAt: status === "read" ? now.toISOString() : void 0
|
|
418
480
|
}
|
|
419
481
|
});
|
|
420
|
-
await this.notifyBridgesRead(request.sessionId, request.messageIds, status);
|
|
482
|
+
await this.notifyBridgesRead(request.sessionId, request.messageIds, status, session);
|
|
421
483
|
return { updated };
|
|
422
484
|
}
|
|
423
485
|
// ─────────────────────────────────────────────────────────────────
|
|
486
|
+
// User Identity
|
|
487
|
+
// ─────────────────────────────────────────────────────────────────
|
|
488
|
+
/**
|
|
489
|
+
* Handle user identification from widget
|
|
490
|
+
* Called when visitor calls PocketPing.identify()
|
|
491
|
+
*/
|
|
492
|
+
async handleIdentify(request) {
|
|
493
|
+
if (!request.identity?.id) {
|
|
494
|
+
throw new Error("identity.id is required");
|
|
495
|
+
}
|
|
496
|
+
const session = await this.storage.getSession(request.sessionId);
|
|
497
|
+
if (!session) {
|
|
498
|
+
throw new Error("Session not found");
|
|
499
|
+
}
|
|
500
|
+
session.identity = request.identity;
|
|
501
|
+
session.lastActivity = /* @__PURE__ */ new Date();
|
|
502
|
+
await this.storage.updateSession(session);
|
|
503
|
+
await this.notifyBridgesIdentity(session);
|
|
504
|
+
await this.config.onIdentify?.(session);
|
|
505
|
+
this.forwardIdentityToWebhook(session);
|
|
506
|
+
return { ok: true };
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get a session by ID
|
|
510
|
+
*/
|
|
511
|
+
async getSession(sessionId) {
|
|
512
|
+
return this.storage.getSession(sessionId);
|
|
513
|
+
}
|
|
514
|
+
// ─────────────────────────────────────────────────────────────────
|
|
424
515
|
// Operator Actions (for bridges)
|
|
425
516
|
// ─────────────────────────────────────────────────────────────────
|
|
426
517
|
async sendOperatorMessage(sessionId, content) {
|
|
@@ -448,6 +539,113 @@ var PocketPing = class {
|
|
|
448
539
|
}
|
|
449
540
|
}
|
|
450
541
|
// ─────────────────────────────────────────────────────────────────
|
|
542
|
+
// Custom Events (bidirectional)
|
|
543
|
+
// ─────────────────────────────────────────────────────────────────
|
|
544
|
+
/**
|
|
545
|
+
* Subscribe to custom events from widgets
|
|
546
|
+
* @param eventName - The name of the event to listen for, or '*' for all events
|
|
547
|
+
* @param handler - Callback function when event is received
|
|
548
|
+
* @returns Unsubscribe function
|
|
549
|
+
* @example
|
|
550
|
+
* // Listen for specific event
|
|
551
|
+
* pp.onEvent('clicked_pricing', async (event, session) => {
|
|
552
|
+
* console.log(`User ${session.visitorId} clicked pricing: ${event.data?.plan}`)
|
|
553
|
+
* })
|
|
554
|
+
*
|
|
555
|
+
* // Listen for all events
|
|
556
|
+
* pp.onEvent('*', async (event, session) => {
|
|
557
|
+
* console.log(`Event: ${event.name}`, event.data)
|
|
558
|
+
* })
|
|
559
|
+
*/
|
|
560
|
+
onEvent(eventName, handler) {
|
|
561
|
+
if (!this.eventHandlers.has(eventName)) {
|
|
562
|
+
this.eventHandlers.set(eventName, /* @__PURE__ */ new Set());
|
|
563
|
+
}
|
|
564
|
+
this.eventHandlers.get(eventName).add(handler);
|
|
565
|
+
return () => {
|
|
566
|
+
this.eventHandlers.get(eventName)?.delete(handler);
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Unsubscribe from a custom event
|
|
571
|
+
* @param eventName - The name of the event
|
|
572
|
+
* @param handler - The handler to remove
|
|
573
|
+
*/
|
|
574
|
+
offEvent(eventName, handler) {
|
|
575
|
+
this.eventHandlers.get(eventName)?.delete(handler);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Send a custom event to a specific widget/session
|
|
579
|
+
* @param sessionId - The session ID to send the event to
|
|
580
|
+
* @param eventName - The name of the event
|
|
581
|
+
* @param data - Optional payload to send with the event
|
|
582
|
+
* @example
|
|
583
|
+
* // Send a promotion offer to a specific user
|
|
584
|
+
* pp.emitEvent('session-123', 'show_offer', {
|
|
585
|
+
* discount: 20,
|
|
586
|
+
* code: 'SAVE20',
|
|
587
|
+
* message: 'Special offer just for you!'
|
|
588
|
+
* })
|
|
589
|
+
*/
|
|
590
|
+
emitEvent(sessionId, eventName, data) {
|
|
591
|
+
const event = {
|
|
592
|
+
name: eventName,
|
|
593
|
+
data,
|
|
594
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
595
|
+
sessionId
|
|
596
|
+
};
|
|
597
|
+
this.broadcastToSession(sessionId, {
|
|
598
|
+
type: "event",
|
|
599
|
+
data: event
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Broadcast a custom event to all connected widgets
|
|
604
|
+
* @param eventName - The name of the event
|
|
605
|
+
* @param data - Optional payload to send with the event
|
|
606
|
+
* @example
|
|
607
|
+
* // Notify all users about maintenance
|
|
608
|
+
* pp.broadcastEvent('maintenance_warning', {
|
|
609
|
+
* message: 'Site will be down for maintenance in 5 minutes'
|
|
610
|
+
* })
|
|
611
|
+
*/
|
|
612
|
+
broadcastEvent(eventName, data) {
|
|
613
|
+
const event = {
|
|
614
|
+
name: eventName,
|
|
615
|
+
data,
|
|
616
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
617
|
+
};
|
|
618
|
+
for (const sessionId of this.sessionSockets.keys()) {
|
|
619
|
+
event.sessionId = sessionId;
|
|
620
|
+
this.broadcastToSession(sessionId, {
|
|
621
|
+
type: "event",
|
|
622
|
+
data: event
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Process a custom event server-side (runs handlers, bridges, webhooks)
|
|
628
|
+
* Useful for server-side automation or triggering events programmatically
|
|
629
|
+
* @param sessionId - The session ID to associate with the event
|
|
630
|
+
* @param eventName - The name of the event
|
|
631
|
+
* @param data - Optional payload for the event
|
|
632
|
+
* @example
|
|
633
|
+
* // Trigger event from backend logic (e.g., after purchase)
|
|
634
|
+
* await pp.triggerEvent('session-123', 'purchase_completed', {
|
|
635
|
+
* orderId: 'order-456',
|
|
636
|
+
* amount: 99.99
|
|
637
|
+
* })
|
|
638
|
+
*/
|
|
639
|
+
async triggerEvent(sessionId, eventName, data) {
|
|
640
|
+
const event = {
|
|
641
|
+
name: eventName,
|
|
642
|
+
data,
|
|
643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
644
|
+
sessionId
|
|
645
|
+
};
|
|
646
|
+
await this.handleCustomEvent(sessionId, event);
|
|
647
|
+
}
|
|
648
|
+
// ─────────────────────────────────────────────────────────────────
|
|
451
649
|
// Bridges
|
|
452
650
|
// ─────────────────────────────────────────────────────────────────
|
|
453
651
|
async notifyBridges(event, ...args) {
|
|
@@ -458,7 +656,7 @@ var PocketPing = class {
|
|
|
458
656
|
await bridge.onNewSession?.(args[0]);
|
|
459
657
|
break;
|
|
460
658
|
case "message":
|
|
461
|
-
await bridge.
|
|
659
|
+
await bridge.onVisitorMessage?.(args[0], args[1]);
|
|
462
660
|
break;
|
|
463
661
|
}
|
|
464
662
|
} catch (err) {
|
|
@@ -466,15 +664,95 @@ var PocketPing = class {
|
|
|
466
664
|
}
|
|
467
665
|
}
|
|
468
666
|
}
|
|
469
|
-
async notifyBridgesRead(sessionId, messageIds, status) {
|
|
667
|
+
async notifyBridgesRead(sessionId, messageIds, status, session) {
|
|
470
668
|
for (const bridge of this.bridges) {
|
|
471
669
|
try {
|
|
472
|
-
await bridge.onMessageRead?.(sessionId, messageIds, status);
|
|
670
|
+
await bridge.onMessageRead?.(sessionId, messageIds, status, session);
|
|
473
671
|
} catch (err) {
|
|
474
672
|
console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
|
|
475
673
|
}
|
|
476
674
|
}
|
|
477
675
|
}
|
|
676
|
+
async notifyBridgesEvent(event, session) {
|
|
677
|
+
for (const bridge of this.bridges) {
|
|
678
|
+
try {
|
|
679
|
+
await bridge.onCustomEvent?.(event, session);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.error(`[PocketPing] Bridge ${bridge.name} event notification error:`, err);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async notifyBridgesIdentity(session) {
|
|
686
|
+
for (const bridge of this.bridges) {
|
|
687
|
+
try {
|
|
688
|
+
await bridge.onIdentityUpdate?.(session);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`[PocketPing] Bridge ${bridge.name} identity notification error:`, err);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ─────────────────────────────────────────────────────────────────
|
|
695
|
+
// Webhook Forwarding
|
|
696
|
+
// ─────────────────────────────────────────────────────────────────
|
|
697
|
+
/**
|
|
698
|
+
* Forward custom event to configured webhook URL (non-blocking)
|
|
699
|
+
* Used for integrations with Zapier, Make, n8n, or custom backends
|
|
700
|
+
*/
|
|
701
|
+
forwardToWebhook(event, session) {
|
|
702
|
+
if (!this.config.webhookUrl) return;
|
|
703
|
+
const payload = {
|
|
704
|
+
event,
|
|
705
|
+
session: {
|
|
706
|
+
id: session.id,
|
|
707
|
+
visitorId: session.visitorId,
|
|
708
|
+
metadata: session.metadata,
|
|
709
|
+
identity: session.identity
|
|
710
|
+
},
|
|
711
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
712
|
+
};
|
|
713
|
+
const body = JSON.stringify(payload);
|
|
714
|
+
const headers = {
|
|
715
|
+
"Content-Type": "application/json"
|
|
716
|
+
};
|
|
717
|
+
if (this.config.webhookSecret) {
|
|
718
|
+
const signature = createHmac("sha256", this.config.webhookSecret).update(body).digest("hex");
|
|
719
|
+
headers["X-PocketPing-Signature"] = `sha256=${signature}`;
|
|
720
|
+
}
|
|
721
|
+
const timeout = this.config.webhookTimeout ?? 5e3;
|
|
722
|
+
const controller = new AbortController();
|
|
723
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
724
|
+
fetch(this.config.webhookUrl, {
|
|
725
|
+
method: "POST",
|
|
726
|
+
headers,
|
|
727
|
+
body,
|
|
728
|
+
signal: controller.signal
|
|
729
|
+
}).then((response) => {
|
|
730
|
+
clearTimeout(timeoutId);
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
console.error(`[PocketPing] Webhook returned ${response.status}: ${response.statusText}`);
|
|
733
|
+
}
|
|
734
|
+
}).catch((err) => {
|
|
735
|
+
clearTimeout(timeoutId);
|
|
736
|
+
if (err.name === "AbortError") {
|
|
737
|
+
console.error(`[PocketPing] Webhook timed out after ${timeout}ms`);
|
|
738
|
+
} else {
|
|
739
|
+
console.error(`[PocketPing] Webhook error:`, err.message);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Forward identity update to webhook as a special event
|
|
745
|
+
*/
|
|
746
|
+
forwardIdentityToWebhook(session) {
|
|
747
|
+
if (!this.config.webhookUrl || !session.identity) return;
|
|
748
|
+
const event = {
|
|
749
|
+
name: "identify",
|
|
750
|
+
data: session.identity,
|
|
751
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
752
|
+
sessionId: session.id
|
|
753
|
+
};
|
|
754
|
+
this.forwardToWebhook(event, session);
|
|
755
|
+
}
|
|
478
756
|
addBridge(bridge) {
|
|
479
757
|
this.bridges.push(bridge);
|
|
480
758
|
bridge.init?.(this);
|
|
@@ -488,9 +766,91 @@ var PocketPing = class {
|
|
|
488
766
|
getStorage() {
|
|
489
767
|
return this.storage;
|
|
490
768
|
}
|
|
769
|
+
// ─────────────────────────────────────────────────────────────────
|
|
770
|
+
// Version Management
|
|
771
|
+
// ─────────────────────────────────────────────────────────────────
|
|
772
|
+
/**
|
|
773
|
+
* Check widget version against configured min/latest versions
|
|
774
|
+
* @param widgetVersion - Version from X-PocketPing-Version header
|
|
775
|
+
* @returns Version check result with status and headers to set
|
|
776
|
+
*/
|
|
777
|
+
checkWidgetVersion(widgetVersion) {
|
|
778
|
+
if (!widgetVersion) {
|
|
779
|
+
return {
|
|
780
|
+
status: "ok",
|
|
781
|
+
canContinue: true
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const { minWidgetVersion, latestWidgetVersion } = this.config;
|
|
785
|
+
if (!minWidgetVersion && !latestWidgetVersion) {
|
|
786
|
+
return {
|
|
787
|
+
status: "ok",
|
|
788
|
+
canContinue: true
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
let status = "ok";
|
|
792
|
+
let message;
|
|
793
|
+
let canContinue = true;
|
|
794
|
+
if (minWidgetVersion && compareVersions(widgetVersion, minWidgetVersion) < 0) {
|
|
795
|
+
status = "unsupported";
|
|
796
|
+
message = this.config.versionWarningMessage || `Widget version ${widgetVersion} is no longer supported. Minimum version: ${minWidgetVersion}`;
|
|
797
|
+
canContinue = false;
|
|
798
|
+
} else if (latestWidgetVersion && compareVersions(widgetVersion, latestWidgetVersion) < 0) {
|
|
799
|
+
const majorDiff = parseVersion(latestWidgetVersion)[0] - parseVersion(widgetVersion)[0];
|
|
800
|
+
if (majorDiff >= 1) {
|
|
801
|
+
status = "deprecated";
|
|
802
|
+
message = this.config.versionWarningMessage || `Widget version ${widgetVersion} is deprecated. Please update to ${latestWidgetVersion}`;
|
|
803
|
+
} else {
|
|
804
|
+
status = "outdated";
|
|
805
|
+
message = `A newer widget version ${latestWidgetVersion} is available`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
status,
|
|
810
|
+
message,
|
|
811
|
+
minVersion: minWidgetVersion,
|
|
812
|
+
latestVersion: latestWidgetVersion,
|
|
813
|
+
canContinue
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Set version warning headers on HTTP response
|
|
818
|
+
*/
|
|
819
|
+
setVersionHeaders(res, versionCheck) {
|
|
820
|
+
if (versionCheck.status !== "ok") {
|
|
821
|
+
res.setHeader("X-PocketPing-Version-Status", versionCheck.status);
|
|
822
|
+
if (versionCheck.minVersion) {
|
|
823
|
+
res.setHeader("X-PocketPing-Min-Version", versionCheck.minVersion);
|
|
824
|
+
}
|
|
825
|
+
if (versionCheck.latestVersion) {
|
|
826
|
+
res.setHeader("X-PocketPing-Latest-Version", versionCheck.latestVersion);
|
|
827
|
+
}
|
|
828
|
+
if (versionCheck.message) {
|
|
829
|
+
res.setHeader("X-PocketPing-Version-Message", versionCheck.message);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Send version warning via WebSocket to a session
|
|
835
|
+
*/
|
|
836
|
+
sendVersionWarning(sessionId, versionCheck) {
|
|
837
|
+
if (versionCheck.status === "ok") return;
|
|
838
|
+
this.broadcastToSession(sessionId, {
|
|
839
|
+
type: "version_warning",
|
|
840
|
+
data: {
|
|
841
|
+
severity: versionCheck.status === "unsupported" ? "error" : versionCheck.status === "deprecated" ? "warning" : "info",
|
|
842
|
+
message: versionCheck.message,
|
|
843
|
+
currentVersion: "unknown",
|
|
844
|
+
// Will be filled by widget
|
|
845
|
+
minVersion: versionCheck.minVersion,
|
|
846
|
+
latestVersion: versionCheck.latestVersion,
|
|
847
|
+
canContinue: versionCheck.canContinue,
|
|
848
|
+
upgradeUrl: this.config.versionUpgradeUrl || "https://docs.pocketping.io/widget/installation"
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
}
|
|
491
852
|
};
|
|
492
|
-
|
|
493
|
-
0 && (module.exports = {
|
|
853
|
+
export {
|
|
494
854
|
MemoryStorage,
|
|
495
855
|
PocketPing
|
|
496
|
-
}
|
|
856
|
+
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pocketping/sdk-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.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
|
],
|
|
11
19
|
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist && find src -name '*.js' -o -name '*.js.map' -o -name '*.d.ts' -o -name '*.d.ts.map' | xargs rm -f 2>/dev/null || true",
|
|
21
|
+
"prebuild": "pnpm clean",
|
|
12
22
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
13
23
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
14
|
-
"test": "vitest"
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest"
|
|
15
26
|
},
|
|
16
27
|
"dependencies": {
|
|
17
28
|
"ws": "^8.16.0"
|
|
@@ -20,7 +31,7 @@
|
|
|
20
31
|
"@types/ws": "^8.5.10",
|
|
21
32
|
"tsup": "^8.0.0",
|
|
22
33
|
"typescript": "^5.3.0",
|
|
23
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^4.0.18"
|
|
24
35
|
},
|
|
25
36
|
"peerDependencies": {
|
|
26
37
|
"express": "^4.18.0 || ^5.0.0"
|
|
@@ -43,5 +54,22 @@
|
|
|
43
54
|
"type": "git",
|
|
44
55
|
"url": "https://github.com/Ruwad-io/pocketping.git",
|
|
45
56
|
"directory": "packages/sdk-node"
|
|
57
|
+
},
|
|
58
|
+
"release": {
|
|
59
|
+
"extends": "semantic-release-monorepo",
|
|
60
|
+
"branches": [
|
|
61
|
+
"main"
|
|
62
|
+
],
|
|
63
|
+
"plugins": [
|
|
64
|
+
"@semantic-release/commit-analyzer",
|
|
65
|
+
"@semantic-release/release-notes-generator",
|
|
66
|
+
[
|
|
67
|
+
"@semantic-release/exec",
|
|
68
|
+
{
|
|
69
|
+
"prepareCmd": "npm pkg set version=${nextRelease.version}"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"@semantic-release/github"
|
|
73
|
+
]
|
|
46
74
|
}
|
|
47
75
|
}
|