@pocketping/sdk-node 0.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/dist/index.js ADDED
@@ -0,0 +1,496 @@
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_ws = require("ws");
30
+
31
+ // src/storage/memory.ts
32
+ var MemoryStorage = class {
33
+ constructor() {
34
+ this.sessions = /* @__PURE__ */ new Map();
35
+ this.messages = /* @__PURE__ */ new Map();
36
+ this.messageById = /* @__PURE__ */ new Map();
37
+ }
38
+ async createSession(session) {
39
+ this.sessions.set(session.id, session);
40
+ this.messages.set(session.id, []);
41
+ }
42
+ async getSession(sessionId) {
43
+ return this.sessions.get(sessionId) ?? null;
44
+ }
45
+ async getSessionByVisitorId(visitorId) {
46
+ const visitorSessions = Array.from(this.sessions.values()).filter(
47
+ (s) => s.visitorId === visitorId
48
+ );
49
+ if (visitorSessions.length === 0) return null;
50
+ return visitorSessions.reduce(
51
+ (latest, s) => s.lastActivity > latest.lastActivity ? s : latest
52
+ );
53
+ }
54
+ async updateSession(session) {
55
+ this.sessions.set(session.id, session);
56
+ }
57
+ async deleteSession(sessionId) {
58
+ this.sessions.delete(sessionId);
59
+ this.messages.delete(sessionId);
60
+ }
61
+ async saveMessage(message) {
62
+ const sessionMessages = this.messages.get(message.sessionId) ?? [];
63
+ sessionMessages.push(message);
64
+ this.messages.set(message.sessionId, sessionMessages);
65
+ this.messageById.set(message.id, message);
66
+ }
67
+ async getMessages(sessionId, after, limit = 50) {
68
+ const sessionMessages = this.messages.get(sessionId) ?? [];
69
+ let startIndex = 0;
70
+ if (after) {
71
+ const afterIndex = sessionMessages.findIndex((m) => m.id === after);
72
+ if (afterIndex !== -1) {
73
+ startIndex = afterIndex + 1;
74
+ }
75
+ }
76
+ return sessionMessages.slice(startIndex, startIndex + limit);
77
+ }
78
+ async getMessage(messageId) {
79
+ return this.messageById.get(messageId) ?? null;
80
+ }
81
+ async cleanupOldSessions(olderThan) {
82
+ let count = 0;
83
+ for (const [id, session] of this.sessions) {
84
+ if (session.lastActivity < olderThan) {
85
+ this.sessions.delete(id);
86
+ this.messages.delete(id);
87
+ count++;
88
+ }
89
+ }
90
+ return count;
91
+ }
92
+ };
93
+
94
+ // src/pocketping.ts
95
+ function getClientIp(req) {
96
+ const forwarded = req.headers["x-forwarded-for"];
97
+ if (forwarded) {
98
+ const ip = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(",")[0];
99
+ return ip?.trim() ?? "unknown";
100
+ }
101
+ const realIp = req.headers["x-real-ip"];
102
+ if (realIp) {
103
+ return Array.isArray(realIp) ? realIp[0] ?? "unknown" : realIp;
104
+ }
105
+ return req.socket?.remoteAddress ?? "unknown";
106
+ }
107
+ function parseUserAgent(userAgent) {
108
+ if (!userAgent) {
109
+ return { deviceType: void 0, browser: void 0, os: void 0 };
110
+ }
111
+ const ua = userAgent.toLowerCase();
112
+ let deviceType;
113
+ if (["mobile", "android", "iphone", "ipod"].some((x) => ua.includes(x))) {
114
+ deviceType = "mobile";
115
+ } else if (["ipad", "tablet"].some((x) => ua.includes(x))) {
116
+ deviceType = "tablet";
117
+ } else {
118
+ deviceType = "desktop";
119
+ }
120
+ let browser;
121
+ if (ua.includes("firefox")) browser = "Firefox";
122
+ else if (ua.includes("edg")) browser = "Edge";
123
+ else if (ua.includes("chrome")) browser = "Chrome";
124
+ else if (ua.includes("safari")) browser = "Safari";
125
+ else if (ua.includes("opera") || ua.includes("opr")) browser = "Opera";
126
+ let os;
127
+ if (ua.includes("windows")) os = "Windows";
128
+ else if (ua.includes("mac os") || ua.includes("macos")) os = "macOS";
129
+ else if (ua.includes("linux")) os = "Linux";
130
+ else if (ua.includes("android")) os = "Android";
131
+ else if (ua.includes("iphone") || ua.includes("ipad")) os = "iOS";
132
+ return { deviceType, browser, os };
133
+ }
134
+ var PocketPing = class {
135
+ constructor(config = {}) {
136
+ this.wss = null;
137
+ this.sessionSockets = /* @__PURE__ */ new Map();
138
+ this.operatorOnline = false;
139
+ this.config = config;
140
+ this.storage = this.initStorage(config.storage);
141
+ this.bridges = config.bridges ?? [];
142
+ }
143
+ initStorage(storage) {
144
+ if (!storage || storage === "memory") {
145
+ return new MemoryStorage();
146
+ }
147
+ return storage;
148
+ }
149
+ // ─────────────────────────────────────────────────────────────────
150
+ // Express/Connect Middleware
151
+ // ─────────────────────────────────────────────────────────────────
152
+ middleware() {
153
+ return async (req, res, next) => {
154
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
155
+ const path = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
156
+ res.setHeader("Access-Control-Allow-Origin", "*");
157
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
158
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
159
+ if (req.method === "OPTIONS") {
160
+ res.statusCode = 204;
161
+ res.end();
162
+ return;
163
+ }
164
+ try {
165
+ const body = await this.parseBody(req);
166
+ const query = Object.fromEntries(url.searchParams);
167
+ let result;
168
+ switch (path) {
169
+ case "connect": {
170
+ const connectReq = body;
171
+ const clientIp = getClientIp(req);
172
+ const userAgent = req.headers["user-agent"];
173
+ const uaInfo = parseUserAgent(connectReq.metadata?.userAgent ?? userAgent);
174
+ if (connectReq.metadata) {
175
+ connectReq.metadata.ip = clientIp;
176
+ connectReq.metadata.deviceType = connectReq.metadata.deviceType ?? uaInfo.deviceType;
177
+ connectReq.metadata.browser = connectReq.metadata.browser ?? uaInfo.browser;
178
+ connectReq.metadata.os = connectReq.metadata.os ?? uaInfo.os;
179
+ } else {
180
+ connectReq.metadata = {
181
+ ip: clientIp,
182
+ userAgent,
183
+ ...uaInfo
184
+ };
185
+ }
186
+ result = await this.handleConnect(connectReq);
187
+ break;
188
+ }
189
+ case "message":
190
+ result = await this.handleMessage(body);
191
+ break;
192
+ case "messages":
193
+ result = await this.handleGetMessages(query);
194
+ break;
195
+ case "typing":
196
+ result = await this.handleTyping(body);
197
+ break;
198
+ case "presence":
199
+ result = await this.handlePresence();
200
+ break;
201
+ case "read":
202
+ result = await this.handleRead(body);
203
+ break;
204
+ default:
205
+ if (next) {
206
+ next();
207
+ return;
208
+ }
209
+ res.statusCode = 404;
210
+ res.end(JSON.stringify({ error: "Not found" }));
211
+ return;
212
+ }
213
+ res.setHeader("Content-Type", "application/json");
214
+ res.statusCode = 200;
215
+ res.end(JSON.stringify(result));
216
+ } catch (error) {
217
+ console.error("[PocketPing] Error:", error);
218
+ res.statusCode = 500;
219
+ res.end(JSON.stringify({ error: "Internal server error" }));
220
+ }
221
+ };
222
+ }
223
+ async parseBody(req) {
224
+ if (req.body) return req.body;
225
+ return new Promise((resolve, reject) => {
226
+ let data = "";
227
+ req.on("data", (chunk) => data += chunk);
228
+ req.on("end", () => {
229
+ try {
230
+ resolve(data ? JSON.parse(data) : {});
231
+ } catch {
232
+ reject(new Error("Invalid JSON"));
233
+ }
234
+ });
235
+ req.on("error", reject);
236
+ });
237
+ }
238
+ // ─────────────────────────────────────────────────────────────────
239
+ // WebSocket
240
+ // ─────────────────────────────────────────────────────────────────
241
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
242
+ attachWebSocket(server) {
243
+ this.wss = new import_ws.WebSocketServer({
244
+ server,
245
+ path: "/pocketping/stream"
246
+ });
247
+ this.wss.on("connection", (ws, req) => {
248
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
249
+ const sessionId = url.searchParams.get("sessionId");
250
+ if (!sessionId) {
251
+ ws.close(4e3, "sessionId required");
252
+ return;
253
+ }
254
+ if (!this.sessionSockets.has(sessionId)) {
255
+ this.sessionSockets.set(sessionId, /* @__PURE__ */ new Set());
256
+ }
257
+ this.sessionSockets.get(sessionId).add(ws);
258
+ ws.on("close", () => {
259
+ this.sessionSockets.get(sessionId)?.delete(ws);
260
+ });
261
+ ws.on("message", async (data) => {
262
+ try {
263
+ const event = JSON.parse(data.toString());
264
+ await this.handleWebSocketMessage(sessionId, event);
265
+ } catch (err) {
266
+ console.error("[PocketPing] WS message error:", err);
267
+ }
268
+ });
269
+ });
270
+ }
271
+ async handleWebSocketMessage(sessionId, event) {
272
+ switch (event.type) {
273
+ case "typing":
274
+ this.broadcastToSession(sessionId, {
275
+ type: "typing",
276
+ data: event.data
277
+ });
278
+ break;
279
+ }
280
+ }
281
+ broadcastToSession(sessionId, event) {
282
+ const sockets = this.sessionSockets.get(sessionId);
283
+ if (!sockets) return;
284
+ const message = JSON.stringify(event);
285
+ for (const ws of sockets) {
286
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
287
+ ws.send(message);
288
+ }
289
+ }
290
+ }
291
+ // ─────────────────────────────────────────────────────────────────
292
+ // Protocol Handlers
293
+ // ─────────────────────────────────────────────────────────────────
294
+ async handleConnect(request) {
295
+ let session = null;
296
+ if (request.sessionId) {
297
+ session = await this.storage.getSession(request.sessionId);
298
+ }
299
+ if (!session && this.storage.getSessionByVisitorId) {
300
+ session = await this.storage.getSessionByVisitorId(request.visitorId);
301
+ }
302
+ if (!session) {
303
+ session = {
304
+ id: this.generateId(),
305
+ visitorId: request.visitorId,
306
+ createdAt: /* @__PURE__ */ new Date(),
307
+ lastActivity: /* @__PURE__ */ new Date(),
308
+ operatorOnline: this.operatorOnline,
309
+ aiActive: false,
310
+ metadata: request.metadata
311
+ };
312
+ await this.storage.createSession(session);
313
+ await this.notifyBridges("new_session", session);
314
+ await this.config.onNewSession?.(session);
315
+ } else if (request.metadata) {
316
+ if (session.metadata) {
317
+ request.metadata.ip = session.metadata.ip ?? request.metadata.ip;
318
+ request.metadata.country = session.metadata.country ?? request.metadata.country;
319
+ request.metadata.city = session.metadata.city ?? request.metadata.city;
320
+ }
321
+ session.metadata = request.metadata;
322
+ session.lastActivity = /* @__PURE__ */ new Date();
323
+ await this.storage.updateSession(session);
324
+ }
325
+ const messages = await this.storage.getMessages(session.id);
326
+ return {
327
+ sessionId: session.id,
328
+ visitorId: session.visitorId,
329
+ operatorOnline: this.operatorOnline,
330
+ welcomeMessage: this.config.welcomeMessage,
331
+ messages
332
+ };
333
+ }
334
+ async handleMessage(request) {
335
+ const session = await this.storage.getSession(request.sessionId);
336
+ if (!session) {
337
+ throw new Error("Session not found");
338
+ }
339
+ const message = {
340
+ id: this.generateId(),
341
+ sessionId: request.sessionId,
342
+ content: request.content,
343
+ sender: request.sender,
344
+ timestamp: /* @__PURE__ */ new Date(),
345
+ replyTo: request.replyTo
346
+ };
347
+ await this.storage.saveMessage(message);
348
+ session.lastActivity = /* @__PURE__ */ new Date();
349
+ await this.storage.updateSession(session);
350
+ if (request.sender === "visitor") {
351
+ await this.notifyBridges("message", message, session);
352
+ }
353
+ this.broadcastToSession(request.sessionId, {
354
+ type: "message",
355
+ data: message
356
+ });
357
+ await this.config.onMessage?.(message, session);
358
+ return {
359
+ messageId: message.id,
360
+ timestamp: message.timestamp.toISOString()
361
+ };
362
+ }
363
+ async handleGetMessages(request) {
364
+ const limit = Math.min(request.limit ?? 50, 100);
365
+ const messages = await this.storage.getMessages(request.sessionId, request.after, limit + 1);
366
+ return {
367
+ messages: messages.slice(0, limit),
368
+ hasMore: messages.length > limit
369
+ };
370
+ }
371
+ async handleTyping(request) {
372
+ this.broadcastToSession(request.sessionId, {
373
+ type: "typing",
374
+ data: {
375
+ sessionId: request.sessionId,
376
+ sender: request.sender,
377
+ isTyping: request.isTyping ?? true
378
+ }
379
+ });
380
+ return { ok: true };
381
+ }
382
+ async handlePresence() {
383
+ return {
384
+ online: this.operatorOnline,
385
+ aiEnabled: !!this.config.ai,
386
+ aiActiveAfter: this.config.aiTakeoverDelay ?? 300
387
+ };
388
+ }
389
+ async handleRead(request) {
390
+ const session = await this.storage.getSession(request.sessionId);
391
+ if (!session) {
392
+ throw new Error("Session not found");
393
+ }
394
+ const status = request.status ?? "read";
395
+ const now = /* @__PURE__ */ new Date();
396
+ let updated = 0;
397
+ const messages = await this.storage.getMessages(request.sessionId);
398
+ for (const msg of messages) {
399
+ if (request.messageIds.includes(msg.id)) {
400
+ msg.status = status;
401
+ if (status === "delivered") {
402
+ msg.deliveredAt = now;
403
+ } else if (status === "read") {
404
+ msg.deliveredAt = msg.deliveredAt ?? now;
405
+ msg.readAt = now;
406
+ }
407
+ await this.storage.saveMessage(msg);
408
+ updated++;
409
+ }
410
+ }
411
+ this.broadcastToSession(request.sessionId, {
412
+ type: "read",
413
+ data: {
414
+ messageIds: request.messageIds,
415
+ status,
416
+ deliveredAt: status === "delivered" ? now.toISOString() : void 0,
417
+ readAt: status === "read" ? now.toISOString() : void 0
418
+ }
419
+ });
420
+ await this.notifyBridgesRead(request.sessionId, request.messageIds, status);
421
+ return { updated };
422
+ }
423
+ // ─────────────────────────────────────────────────────────────────
424
+ // Operator Actions (for bridges)
425
+ // ─────────────────────────────────────────────────────────────────
426
+ async sendOperatorMessage(sessionId, content) {
427
+ const response = await this.handleMessage({
428
+ sessionId,
429
+ content,
430
+ sender: "operator"
431
+ });
432
+ const message = {
433
+ id: response.messageId,
434
+ sessionId,
435
+ content,
436
+ sender: "operator",
437
+ timestamp: new Date(response.timestamp)
438
+ };
439
+ return message;
440
+ }
441
+ setOperatorOnline(online) {
442
+ this.operatorOnline = online;
443
+ for (const sessionId of this.sessionSockets.keys()) {
444
+ this.broadcastToSession(sessionId, {
445
+ type: "presence",
446
+ data: { online }
447
+ });
448
+ }
449
+ }
450
+ // ─────────────────────────────────────────────────────────────────
451
+ // Bridges
452
+ // ─────────────────────────────────────────────────────────────────
453
+ async notifyBridges(event, ...args) {
454
+ for (const bridge of this.bridges) {
455
+ try {
456
+ switch (event) {
457
+ case "new_session":
458
+ await bridge.onNewSession?.(args[0]);
459
+ break;
460
+ case "message":
461
+ await bridge.onMessage?.(args[0], args[1]);
462
+ break;
463
+ }
464
+ } catch (err) {
465
+ console.error(`[PocketPing] Bridge ${bridge.name} error:`, err);
466
+ }
467
+ }
468
+ }
469
+ async notifyBridgesRead(sessionId, messageIds, status) {
470
+ for (const bridge of this.bridges) {
471
+ try {
472
+ await bridge.onMessageRead?.(sessionId, messageIds, status);
473
+ } catch (err) {
474
+ console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
475
+ }
476
+ }
477
+ }
478
+ addBridge(bridge) {
479
+ this.bridges.push(bridge);
480
+ bridge.init?.(this);
481
+ }
482
+ // ─────────────────────────────────────────────────────────────────
483
+ // Utilities
484
+ // ─────────────────────────────────────────────────────────────────
485
+ generateId() {
486
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
487
+ }
488
+ getStorage() {
489
+ return this.storage;
490
+ }
491
+ };
492
+ // Annotate the CommonJS export names for ESM import in node:
493
+ 0 && (module.exports = {
494
+ MemoryStorage,
495
+ PocketPing
496
+ });