pingerchips-js-server 2.0.0 → 2.1.1
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 +61 -47
- package/index.js +156 -86
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pingerchips-js-server
|
|
2
2
|
|
|
3
|
-
Server-side SDK for
|
|
3
|
+
Server-side SDK for Pingerchips. Trigger events and authenticate users for private/presence channels.
|
|
4
|
+
|
|
5
|
+
Requires Node.js 18+ (uses native `fetch`).
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -15,24 +17,23 @@ npm install pingerchips-js-server
|
|
|
15
17
|
```javascript
|
|
16
18
|
import PingerchipsServer from 'pingerchips-js-server';
|
|
17
19
|
|
|
18
|
-
const pingerchips = new PingerchipsServer('
|
|
19
|
-
appKey: 'app_key',
|
|
20
|
-
endpoint: 'https://pinger-processor.pingerchips.com/api'
|
|
21
|
-
});
|
|
20
|
+
const pingerchips = new PingerchipsServer('app_key', 'app_secret');
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Defaults to `https://queue.pingerchips.com` in production (`NODE_ENV=production`) and `http://localhost:4000` otherwise. Override with `options.endpoint` or the `PINGERCHIPS_API_ENDPOINT` env var.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
### Trigger events
|
|
27
26
|
|
|
28
27
|
```javascript
|
|
29
|
-
await pingerchips.trigger('lobby', 'message', {
|
|
28
|
+
await pingerchips.trigger('lobby', 'new-message', {
|
|
30
29
|
text: 'Hello from server!',
|
|
31
30
|
timestamp: Date.now()
|
|
32
31
|
});
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Failed requests are retried on 5xx and network errors (default: 2 retries).
|
|
35
|
+
|
|
36
|
+
### Authenticate users (private/presence channels)
|
|
36
37
|
|
|
37
38
|
Implement an auth endpoint on your server:
|
|
38
39
|
|
|
@@ -45,76 +46,89 @@ app.use(express.json());
|
|
|
45
46
|
app.post('/auth', (req, res) => {
|
|
46
47
|
const { socket_id, channel_name, auth_info } = req.body;
|
|
47
48
|
|
|
48
|
-
// Validate user from session/token
|
|
49
49
|
const user = validateUser(auth_info);
|
|
50
|
-
if (!user) {
|
|
51
|
-
return res.status(403).json({ error: 'Unauthorized' });
|
|
52
|
-
}
|
|
50
|
+
if (!user) return res.status(403).json({ error: 'Unauthorized' });
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
user_info: {
|
|
58
|
-
name: user.name,
|
|
59
|
-
avatar: user.avatar
|
|
60
|
-
}
|
|
61
|
-
} : null;
|
|
52
|
+
const userData = channel_name.startsWith('presence-')
|
|
53
|
+
? { user_id: user.id, user_info: { name: user.name } }
|
|
54
|
+
: null;
|
|
62
55
|
|
|
63
|
-
// Sign authentication with Pingerchips
|
|
64
56
|
const authData = pingerchips.authenticate(socket_id, channel_name, userData);
|
|
65
57
|
res.json(authData);
|
|
66
58
|
});
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
|
|
61
|
+
Configure the client to use it:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const client = new Pingerchips('app_key', {
|
|
65
|
+
authEndpoint: 'https://your-server.com/auth'
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
70
|
|
|
71
|
-
### `new PingerchipsServer(
|
|
71
|
+
### `new PingerchipsServer(appKey, appSecret, options?)`
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
| Parameter | Type | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `appKey` | string | Your app key |
|
|
76
|
+
| `appSecret` | string | Your app secret — used for HMAC signing, never sent over the wire |
|
|
74
77
|
|
|
75
78
|
**Options:**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
|
|
80
|
+
| Option | Type | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `endpoint` | string | auto | API base URL |
|
|
83
|
+
| `requestTimeout` | number | `10000` | Request timeout in ms |
|
|
84
|
+
| `retries` | number | `2` | Retries on 5xx / network error |
|
|
85
|
+
| `token` | string | — | Internal API token (legacy) |
|
|
86
|
+
| `mtls` | object | — | mTLS config (see below) |
|
|
80
87
|
|
|
81
88
|
### `trigger(channel, event, data)`
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
Trigger an event on a channel.
|
|
91
|
+
|
|
92
|
+
- `channel` — max 200 chars
|
|
93
|
+
- `event` — max 200 chars
|
|
94
|
+
- `data` — must be JSON serializable, max 64KB
|
|
84
95
|
|
|
85
96
|
```javascript
|
|
86
|
-
await pingerchips.trigger('
|
|
97
|
+
await pingerchips.trigger('room-42', 'message', { text: 'hi' });
|
|
87
98
|
```
|
|
88
99
|
|
|
89
100
|
### `authenticate(socketId, channelName, userData?)`
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
Sign a channel auth request. Call from your auth endpoint.
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
- `
|
|
95
|
-
- `channelName` - Channel name (e.g., "private-chat" or "presence-lobby")
|
|
96
|
-
- `userData` - User data for presence channels (must include `user_id`)
|
|
104
|
+
- `channelName` must start with `private-` or `presence-`
|
|
105
|
+
- `userData` required for presence channels, must include `user_id`
|
|
97
106
|
|
|
98
|
-
**Returns:**
|
|
99
107
|
```javascript
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
// Private channel
|
|
109
|
+
const auth = pingerchips.authenticate(socketId, 'private-chat');
|
|
110
|
+
// → { auth: "app_key:hmac_signature" }
|
|
111
|
+
|
|
112
|
+
// Presence channel
|
|
113
|
+
const auth = pingerchips.authenticate(socketId, 'presence-lobby', {
|
|
114
|
+
user_id: '123',
|
|
115
|
+
user_info: { name: 'Alice' }
|
|
116
|
+
});
|
|
117
|
+
// → { auth: "app_key:hmac_signature", channel_data: "{\"user_id\":\"123\",...}" }
|
|
104
118
|
```
|
|
105
119
|
|
|
106
|
-
## mTLS
|
|
120
|
+
## mTLS
|
|
107
121
|
|
|
108
122
|
For secure server-to-server communication:
|
|
109
123
|
|
|
110
124
|
```javascript
|
|
111
|
-
const pingerchips = new PingerchipsServer('
|
|
112
|
-
endpoint: 'https://pinger-processor.pingerchips.com/api',
|
|
125
|
+
const pingerchips = new PingerchipsServer('app_key', 'app_secret', {
|
|
113
126
|
mtls: {
|
|
114
127
|
enabled: true,
|
|
115
|
-
cert: '/path/to/client-cert.pem',
|
|
128
|
+
cert: '/path/to/client-cert.pem', // or PEM string directly
|
|
116
129
|
key: '/path/to/client-key.pem',
|
|
117
|
-
ca: '/path/to/ca-cert.pem'
|
|
130
|
+
ca: '/path/to/ca-cert.pem',
|
|
131
|
+
rejectUnauthorized: true // default — do not disable
|
|
118
132
|
}
|
|
119
133
|
});
|
|
120
134
|
```
|
package/index.js
CHANGED
|
@@ -3,164 +3,234 @@ import https from "https";
|
|
|
3
3
|
import crypto from "crypto";
|
|
4
4
|
|
|
5
5
|
class PingerchipsServer {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} appKey - App key (used for signing and routing)
|
|
8
|
+
* @param {string} appSecret - App secret (used for HMAC signing)
|
|
9
|
+
* @param {object} options
|
|
10
|
+
*/
|
|
11
|
+
constructor(appKey, appSecret, options = {}) {
|
|
12
|
+
this.appKey = appKey;
|
|
8
13
|
this.appSecret = appSecret;
|
|
9
|
-
this.appKey = options.appKey || appId; // App key for signing
|
|
10
14
|
this.endpoint =
|
|
11
15
|
options.endpoint ||
|
|
12
16
|
process.env.PINGERCHIPS_API_ENDPOINT ||
|
|
13
|
-
process.env.NODE_ENV === "production"
|
|
14
|
-
? "https://
|
|
15
|
-
: "http://localhost:4000";
|
|
17
|
+
(process.env.NODE_ENV === "production"
|
|
18
|
+
? "https://queue.pingerchips.com"
|
|
19
|
+
: "http://localhost:4000");
|
|
16
20
|
this.token = options.token;
|
|
21
|
+
this.requestTimeout = options.requestTimeout || 10000;
|
|
22
|
+
this.retries = options.retries ?? 2; // retry 5xx up to N times
|
|
17
23
|
|
|
18
|
-
// mTLS configuration
|
|
19
24
|
this.mtls = {
|
|
20
25
|
enabled: options.mtls?.enabled || false,
|
|
21
26
|
cert: options.mtls?.cert,
|
|
22
27
|
key: options.mtls?.key,
|
|
23
28
|
ca: options.mtls?.ca,
|
|
24
|
-
rejectUnauthorized: options.mtls?.rejectUnauthorized
|
|
29
|
+
rejectUnauthorized: options.mtls?.rejectUnauthorized ?? true,
|
|
25
30
|
};
|
|
26
|
-
|
|
27
|
-
this.fetchPromise = import("node-fetch").then((mod) => mod.default);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
_createHttpsAgent() {
|
|
31
|
-
if (!this.mtls.enabled)
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
+
if (!this.mtls.enabled) return undefined;
|
|
34
35
|
|
|
35
36
|
const agentOptions = {
|
|
36
37
|
rejectUnauthorized: this.mtls.rejectUnauthorized,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
if (this.mtls.
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
if (this.mtls.key) {
|
|
44
|
-
agentOptions.key = this._loadCertificate(this.mtls.key);
|
|
45
|
-
}
|
|
46
|
-
if (this.mtls.ca) {
|
|
47
|
-
agentOptions.ca = this._loadCertificate(this.mtls.ca);
|
|
48
|
-
}
|
|
40
|
+
if (this.mtls.cert) agentOptions.cert = this._loadCertificate(this.mtls.cert);
|
|
41
|
+
if (this.mtls.key) agentOptions.key = this._loadCertificate(this.mtls.key);
|
|
42
|
+
if (this.mtls.ca) agentOptions.ca = this._loadCertificate(this.mtls.ca);
|
|
49
43
|
|
|
50
44
|
return new https.Agent(agentOptions);
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
_loadCertificate(certOrPath) {
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
typeof certOrPath === "string" &&
|
|
57
|
-
certOrPath.trim().startsWith("-----")
|
|
58
|
-
) {
|
|
48
|
+
if (typeof certOrPath === "string" && certOrPath.trim().startsWith("-----")) {
|
|
59
49
|
return certOrPath;
|
|
60
50
|
}
|
|
61
|
-
// Otherwise, treat as file path
|
|
62
51
|
return fs.readFileSync(certOrPath, "utf8");
|
|
63
52
|
}
|
|
64
53
|
|
|
54
|
+
_validateTriggerInput(channel, event, data) {
|
|
55
|
+
if (!channel || typeof channel !== "string") {
|
|
56
|
+
throw new TypeError("channel must be a non-empty string");
|
|
57
|
+
}
|
|
58
|
+
if (!event || typeof event !== "string") {
|
|
59
|
+
throw new TypeError("event must be a non-empty string");
|
|
60
|
+
}
|
|
61
|
+
if (channel.length > 200) {
|
|
62
|
+
throw new Error("channel name must be 200 characters or less");
|
|
63
|
+
}
|
|
64
|
+
if (event.length > 200) {
|
|
65
|
+
throw new Error("event name must be 200 characters or less");
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const serialized = JSON.stringify(data);
|
|
69
|
+
if (serialized.length > 65536) {
|
|
70
|
+
throw new Error("data payload must be 64KB or less");
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
throw new TypeError("data must be JSON serializable");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_generateSignature(method, path, body) {
|
|
78
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
79
|
+
const bodyString = JSON.stringify(body);
|
|
80
|
+
const stringToSign = `${method}\n${path}\n${timestamp}\n${bodyString}`;
|
|
81
|
+
|
|
82
|
+
const signature = crypto
|
|
83
|
+
.createHmac("sha256", this.appSecret)
|
|
84
|
+
.update(stringToSign)
|
|
85
|
+
.digest("hex");
|
|
86
|
+
|
|
87
|
+
return { signature, timestamp };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async _fetchWithRetry(url, requestOptions, retriesLeft) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
...requestOptions,
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Retry on 5xx if retries remain
|
|
101
|
+
if (response.status >= 500 && retriesLeft > 0) {
|
|
102
|
+
return this._fetchWithRetry(url, requestOptions, retriesLeft - 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return response;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err.name === "AbortError") {
|
|
108
|
+
throw new Error(`Request timeout after ${this.requestTimeout}ms`);
|
|
109
|
+
}
|
|
110
|
+
// Retry on network errors
|
|
111
|
+
if (retriesLeft > 0) {
|
|
112
|
+
return this._fetchWithRetry(url, requestOptions, retriesLeft - 1);
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Trigger an event on a channel.
|
|
122
|
+
* Uses HMAC-SHA256 signature authentication.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} channel
|
|
125
|
+
* @param {string} event
|
|
126
|
+
* @param {any} data - Must be JSON serializable, max 64KB
|
|
127
|
+
* @returns {Promise<object>}
|
|
128
|
+
*/
|
|
65
129
|
async trigger(channel, event, data) {
|
|
66
|
-
|
|
67
|
-
|
|
130
|
+
this._validateTriggerInput(channel, event, data);
|
|
131
|
+
|
|
132
|
+
const path = `/api/apps/${this.appKey}/trigger`;
|
|
133
|
+
const url = `${this.endpoint}${path}`;
|
|
134
|
+
const body = { channel, event, data };
|
|
135
|
+
const { signature, timestamp } = this._generateSignature("POST", path, body);
|
|
68
136
|
|
|
69
137
|
const requestOptions = {
|
|
70
138
|
method: "POST",
|
|
71
139
|
headers: {
|
|
72
140
|
"Content-Type": "application/json",
|
|
73
|
-
|
|
141
|
+
"X-App-Key": this.appKey,
|
|
142
|
+
"X-Signature": signature,
|
|
143
|
+
"X-Timestamp": timestamp.toString(),
|
|
74
144
|
},
|
|
75
|
-
body: JSON.stringify(
|
|
76
|
-
app_id: this.appId,
|
|
77
|
-
app_secret: this.appSecret,
|
|
78
|
-
channel,
|
|
79
|
-
event,
|
|
80
|
-
data,
|
|
81
|
-
}),
|
|
145
|
+
body: JSON.stringify(body),
|
|
82
146
|
};
|
|
83
147
|
|
|
84
|
-
|
|
148
|
+
if (this.token) {
|
|
149
|
+
requestOptions.headers["token"] = this.token;
|
|
150
|
+
}
|
|
151
|
+
|
|
85
152
|
if (this.mtls.enabled) {
|
|
86
153
|
requestOptions.agent = this._createHttpsAgent();
|
|
87
154
|
}
|
|
88
155
|
|
|
89
|
-
const response = await
|
|
156
|
+
const response = await this._fetchWithRetry(url, requestOptions, this.retries);
|
|
90
157
|
|
|
91
158
|
if (!response.ok) {
|
|
92
159
|
const error = await response.json().catch(() => ({}));
|
|
93
160
|
throw new Error(
|
|
94
|
-
`Failed to trigger event: ${response.statusText} - ${error.error || ""}
|
|
161
|
+
`Failed to trigger event: ${response.status} ${response.statusText} - ${error.error || ""}`,
|
|
95
162
|
);
|
|
96
163
|
}
|
|
97
164
|
|
|
98
|
-
return
|
|
165
|
+
return response.json();
|
|
99
166
|
}
|
|
100
167
|
|
|
101
168
|
/**
|
|
102
|
-
* Authenticate a user for a private or presence channel
|
|
103
|
-
*
|
|
104
|
-
* This method is called by your auth endpoint to sign user data.
|
|
105
|
-
* The signature proves that your server authorized the user to join the channel.
|
|
169
|
+
* Authenticate a user for a private or presence channel.
|
|
170
|
+
* Call this from your auth endpoint.
|
|
106
171
|
*
|
|
107
172
|
* @param {string} socketId - Socket ID from the client SDK
|
|
108
|
-
* @param {string} channelName -
|
|
109
|
-
* @param {object} userData -
|
|
110
|
-
* @returns {
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* // In your Express.js auth endpoint:
|
|
114
|
-
* app.post('/auth', (req, res) => {
|
|
115
|
-
* const { socket_id, channel_name, auth_info } = req.body;
|
|
116
|
-
*
|
|
117
|
-
* // Validate user from session/token
|
|
118
|
-
* const user = req.session.user;
|
|
119
|
-
* if (!user) return res.status(403).json({ error: 'Unauthorized' });
|
|
120
|
-
*
|
|
121
|
-
* // For presence channels, provide user data
|
|
122
|
-
* const userData = channel_name.startsWith('presence-') ? {
|
|
123
|
-
* user_id: user.id,
|
|
124
|
-
* user_info: { name: user.name, avatar: user.avatar }
|
|
125
|
-
* } : null;
|
|
126
|
-
*
|
|
127
|
-
* const authData = pingerchips.authenticate(socket_id, channel_name, userData);
|
|
128
|
-
* res.json(authData);
|
|
129
|
-
* });
|
|
173
|
+
* @param {string} channelName - Must start with "private-" or "presence-"
|
|
174
|
+
* @param {object|null} userData - Required for presence channels, must include user_id
|
|
175
|
+
* @returns {{ auth: string, channel_data?: string, user_data?: string }}
|
|
130
176
|
*/
|
|
131
177
|
authenticate(socketId, channelName, userData = null) {
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
if (!socketId || typeof socketId !== "string") {
|
|
179
|
+
throw new TypeError("socketId must be a non-empty string");
|
|
180
|
+
}
|
|
181
|
+
if (!channelName || typeof channelName !== "string") {
|
|
182
|
+
throw new TypeError("channelName must be a non-empty string");
|
|
183
|
+
}
|
|
134
184
|
|
|
135
|
-
|
|
185
|
+
const isPrivate = channelName.startsWith("private-");
|
|
186
|
+
const isPresence = channelName.startsWith("presence-");
|
|
187
|
+
|
|
188
|
+
if (!isPrivate && !isPresence) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Public channels do not require authentication. Only private-* and presence-* channels need auth.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isPresence && !userData) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Presence channels require userData with at least user_id field",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (userData) {
|
|
201
|
+
if (typeof userData !== "object" || userData === null) {
|
|
202
|
+
throw new TypeError("userData must be an object");
|
|
203
|
+
}
|
|
204
|
+
if (isPresence && !userData.user_id) {
|
|
205
|
+
throw new Error("userData must include user_id for presence channels");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fullTopic = `app:${this.appKey}:room:${channelName}`;
|
|
210
|
+
let userDataString = null;
|
|
136
211
|
let stringToSign;
|
|
137
|
-
let channelData = null;
|
|
138
212
|
|
|
139
|
-
if (userData
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
stringToSign = `${socketId}:${fullTopic}:${channelData}`;
|
|
213
|
+
if (userData) {
|
|
214
|
+
userDataString = JSON.stringify(userData);
|
|
215
|
+
stringToSign = `${socketId}:${fullTopic}:${userDataString}`;
|
|
143
216
|
} else {
|
|
144
|
-
// Private channel: just socket_id and topic
|
|
145
217
|
stringToSign = `${socketId}:${fullTopic}`;
|
|
146
218
|
}
|
|
147
219
|
|
|
148
|
-
// Generate HMAC-SHA256 signature using app_secret
|
|
149
220
|
const signature = crypto
|
|
150
221
|
.createHmac("sha256", this.appSecret)
|
|
151
222
|
.update(stringToSign)
|
|
152
223
|
.digest("hex");
|
|
153
224
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
// Build response
|
|
157
|
-
const response = { auth };
|
|
225
|
+
const result = { auth: `${this.appKey}:${signature}` };
|
|
158
226
|
|
|
159
|
-
if (
|
|
160
|
-
|
|
227
|
+
if (isPresence && userDataString) {
|
|
228
|
+
result.channel_data = userDataString;
|
|
229
|
+
} else if (userDataString) {
|
|
230
|
+
result.user_data = userDataString;
|
|
161
231
|
}
|
|
162
232
|
|
|
163
|
-
return
|
|
233
|
+
return result;
|
|
164
234
|
}
|
|
165
235
|
}
|
|
166
236
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pingerchips-js-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Pingerchips server SDK for Node.js - trigger events and authenticate users",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"author": "Pingerchips",
|
|
19
19
|
"license": "MIT",
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"node-fetch": "^3.3.0"
|
|
22
|
-
},
|
|
20
|
+
"dependencies": {},
|
|
23
21
|
"repository": {
|
|
24
22
|
"type": "git",
|
|
25
23
|
"url": "https://github.com/pingerchips/pingerchips-js-server"
|