@pocketping/sdk-node 1.0.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 CHANGED
@@ -69,7 +69,78 @@ const pp = new PocketPing({
69
69
  // Protocol version settings
70
70
  protocolVersion: '1.0',
71
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
+ },
72
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
73
144
  ```
74
145
 
75
146
  ## Architecture Options
package/dist/index.cjs CHANGED
@@ -92,6 +92,71 @@ var MemoryStorage = class {
92
92
  }
93
93
  };
94
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
+
95
160
  // src/pocketping.ts
96
161
  function getClientIp(req) {
97
162
  const forwarded = req.headers["x-forwarded-for"];
@@ -179,6 +244,34 @@ var PocketPing = class {
179
244
  res.end();
180
245
  return;
181
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
+ }
182
275
  const widgetVersion = req.headers["x-pocketping-version"];
183
276
  const versionCheck = this.checkWidgetVersion(widgetVersion);
184
277
  this.setVersionHeaders(res, versionCheck);
@@ -287,7 +380,31 @@ var PocketPing = class {
287
380
  server,
288
381
  path: "/pocketping/stream"
289
382
  });
290
- 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
+ }
291
408
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
292
409
  const sessionId = url.searchParams.get("sessionId");
293
410
  if (!sessionId) {
package/dist/index.d.cts CHANGED
@@ -56,6 +56,52 @@ interface AIProvider {
56
56
  isAvailable(): Promise<boolean>;
57
57
  }
58
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
+
59
105
  interface PocketPingConfig {
60
106
  /** Storage adapter for sessions and messages */
61
107
  storage?: Storage | 'memory';
@@ -89,6 +135,8 @@ interface PocketPingConfig {
89
135
  versionWarningMessage?: string;
90
136
  /** URL to upgrade instructions */
91
137
  versionUpgradeUrl?: string;
138
+ /** IP filtering configuration (allowlist/blocklist) */
139
+ ipFilter?: IpFilterConfig;
92
140
  }
93
141
  interface AIConfig {
94
142
  provider: AIProvider | 'openai' | 'gemini' | 'anthropic';
package/dist/index.d.ts CHANGED
@@ -56,6 +56,52 @@ interface AIProvider {
56
56
  isAvailable(): Promise<boolean>;
57
57
  }
58
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
+
59
105
  interface PocketPingConfig {
60
106
  /** Storage adapter for sessions and messages */
61
107
  storage?: Storage | 'memory';
@@ -89,6 +135,8 @@ interface PocketPingConfig {
89
135
  versionWarningMessage?: string;
90
136
  /** URL to upgrade instructions */
91
137
  versionUpgradeUrl?: string;
138
+ /** IP filtering configuration (allowlist/blocklist) */
139
+ ipFilter?: IpFilterConfig;
92
140
  }
93
141
  interface AIConfig {
94
142
  provider: AIProvider | 'openai' | 'gemini' | 'anthropic';
package/dist/index.js CHANGED
@@ -65,6 +65,71 @@ var MemoryStorage = class {
65
65
  }
66
66
  };
67
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
+
68
133
  // src/pocketping.ts
69
134
  function getClientIp(req) {
70
135
  const forwarded = req.headers["x-forwarded-for"];
@@ -152,6 +217,34 @@ var PocketPing = class {
152
217
  res.end();
153
218
  return;
154
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
+ }
155
248
  const widgetVersion = req.headers["x-pocketping-version"];
156
249
  const versionCheck = this.checkWidgetVersion(widgetVersion);
157
250
  this.setVersionHeaders(res, versionCheck);
@@ -260,7 +353,31 @@ var PocketPing = class {
260
353
  server,
261
354
  path: "/pocketping/stream"
262
355
  });
263
- 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
+ }
264
381
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
265
382
  const sessionId = url.searchParams.get("sessionId");
266
383
  if (!sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pocketping/sdk-node",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Node.js SDK for implementing PocketPing protocol",
6
6
  "main": "dist/index.cjs",
@@ -22,12 +22,16 @@
22
22
  "build": "tsup src/index.ts --format cjs,esm --dts",
23
23
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
24
24
  "test": "vitest run",
25
- "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"
26
29
  },
27
30
  "dependencies": {
28
31
  "ws": "^8.16.0"
29
32
  },
30
33
  "devDependencies": {
34
+ "@biomejs/biome": "^1.9.0",
31
35
  "@types/ws": "^8.5.10",
32
36
  "tsup": "^8.0.0",
33
37
  "typescript": "^5.3.0",