pingerchips-js-server 2.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 +124 -0
- package/index.js +167 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Pingerchips Server SDK
|
|
2
|
+
|
|
3
|
+
Server-side SDK for triggering events and authenticating users with Pingerchips.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install pingerchips-js-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Initialize
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import PingerchipsServer from 'pingerchips-js-server';
|
|
17
|
+
|
|
18
|
+
const pingerchips = new PingerchipsServer('app_id', 'app_secret', {
|
|
19
|
+
appKey: 'app_key',
|
|
20
|
+
endpoint: 'https://pinger-processor.pingerchips.com/api'
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Trigger Events
|
|
25
|
+
|
|
26
|
+
Send events to channels from your server:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
await pingerchips.trigger('lobby', 'message', {
|
|
30
|
+
text: 'Hello from server!',
|
|
31
|
+
timestamp: Date.now()
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Authenticate Users (Private/Presence Channels)
|
|
36
|
+
|
|
37
|
+
Implement an auth endpoint on your server:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import express from 'express';
|
|
41
|
+
|
|
42
|
+
const app = express();
|
|
43
|
+
app.use(express.json());
|
|
44
|
+
|
|
45
|
+
app.post('/auth', (req, res) => {
|
|
46
|
+
const { socket_id, channel_name, auth_info } = req.body;
|
|
47
|
+
|
|
48
|
+
// Validate user from session/token
|
|
49
|
+
const user = validateUser(auth_info);
|
|
50
|
+
if (!user) {
|
|
51
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For presence channels, provide user data
|
|
55
|
+
const userData = channel_name.startsWith('presence-') ? {
|
|
56
|
+
user_id: user.id,
|
|
57
|
+
user_info: {
|
|
58
|
+
name: user.name,
|
|
59
|
+
avatar: user.avatar
|
|
60
|
+
}
|
|
61
|
+
} : null;
|
|
62
|
+
|
|
63
|
+
// Sign authentication with Pingerchips
|
|
64
|
+
const authData = pingerchips.authenticate(socket_id, channel_name, userData);
|
|
65
|
+
res.json(authData);
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API Reference
|
|
70
|
+
|
|
71
|
+
### `new PingerchipsServer(appId, appSecret, options)`
|
|
72
|
+
|
|
73
|
+
Create a new server instance.
|
|
74
|
+
|
|
75
|
+
**Options:**
|
|
76
|
+
- `appKey` - Your app key (required for authentication)
|
|
77
|
+
- `endpoint` - API endpoint URL
|
|
78
|
+
- `token` - API token for internal endpoints
|
|
79
|
+
- `mtls` - mTLS configuration for secure connections
|
|
80
|
+
|
|
81
|
+
### `trigger(channel, event, data)`
|
|
82
|
+
|
|
83
|
+
Send an event to a channel.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
await pingerchips.trigger('my-channel', 'my-event', { message: 'Hello' });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `authenticate(socketId, channelName, userData?)`
|
|
90
|
+
|
|
91
|
+
Generate signed authentication for private/presence channels.
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
- `socketId` - Socket ID from client
|
|
95
|
+
- `channelName` - Channel name (e.g., "private-chat" or "presence-lobby")
|
|
96
|
+
- `userData` - User data for presence channels (must include `user_id`)
|
|
97
|
+
|
|
98
|
+
**Returns:**
|
|
99
|
+
```javascript
|
|
100
|
+
{
|
|
101
|
+
auth: "app_key:hmac_signature",
|
|
102
|
+
channel_data: "{\"user_id\":\"123\",...}" // presence channels only
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## mTLS Support
|
|
107
|
+
|
|
108
|
+
For secure server-to-server communication:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const pingerchips = new PingerchipsServer('app_id', 'app_secret', {
|
|
112
|
+
endpoint: 'https://pinger-processor.pingerchips.com/api',
|
|
113
|
+
mtls: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
cert: '/path/to/client-cert.pem',
|
|
116
|
+
key: '/path/to/client-key.pem',
|
|
117
|
+
ca: '/path/to/ca-cert.pem'
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import https from "https";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
|
|
5
|
+
class PingerchipsServer {
|
|
6
|
+
constructor(appId, appSecret, options = {}) {
|
|
7
|
+
this.appId = appId;
|
|
8
|
+
this.appSecret = appSecret;
|
|
9
|
+
this.appKey = options.appKey || appId; // App key for signing
|
|
10
|
+
this.endpoint =
|
|
11
|
+
options.endpoint ||
|
|
12
|
+
process.env.PINGERCHIPS_API_ENDPOINT ||
|
|
13
|
+
process.env.NODE_ENV === "production"
|
|
14
|
+
? "https://pinger-processor.pingerchips.com"
|
|
15
|
+
: "http://localhost:4000";
|
|
16
|
+
this.token = options.token;
|
|
17
|
+
|
|
18
|
+
// mTLS configuration
|
|
19
|
+
this.mtls = {
|
|
20
|
+
enabled: options.mtls?.enabled || false,
|
|
21
|
+
cert: options.mtls?.cert,
|
|
22
|
+
key: options.mtls?.key,
|
|
23
|
+
ca: options.mtls?.ca,
|
|
24
|
+
rejectUnauthorized: options.mtls?.rejectUnauthorized !== false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
this.fetchPromise = import("node-fetch").then((mod) => mod.default);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_createHttpsAgent() {
|
|
31
|
+
if (!this.mtls.enabled) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const agentOptions = {
|
|
36
|
+
rejectUnauthorized: this.mtls.rejectUnauthorized,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Load cert, key, and ca - support both file paths and direct content
|
|
40
|
+
if (this.mtls.cert) {
|
|
41
|
+
agentOptions.cert = this._loadCertificate(this.mtls.cert);
|
|
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
|
+
}
|
|
49
|
+
|
|
50
|
+
return new https.Agent(agentOptions);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_loadCertificate(certOrPath) {
|
|
54
|
+
// If it looks like a certificate/key content (starts with -----), return as-is
|
|
55
|
+
if (
|
|
56
|
+
typeof certOrPath === "string" &&
|
|
57
|
+
certOrPath.trim().startsWith("-----")
|
|
58
|
+
) {
|
|
59
|
+
return certOrPath;
|
|
60
|
+
}
|
|
61
|
+
// Otherwise, treat as file path
|
|
62
|
+
return fs.readFileSync(certOrPath, "utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async trigger(channel, event, data) {
|
|
66
|
+
const fetch = await this.fetchPromise;
|
|
67
|
+
const url = `${this.endpoint}/apps/${this.appId}/trigger`;
|
|
68
|
+
|
|
69
|
+
const requestOptions = {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
token: this.token,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
app_id: this.appId,
|
|
77
|
+
app_secret: this.appSecret,
|
|
78
|
+
channel,
|
|
79
|
+
event,
|
|
80
|
+
data,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Add HTTPS agent if mTLS is enabled
|
|
85
|
+
if (this.mtls.enabled) {
|
|
86
|
+
requestOptions.agent = this._createHttpsAgent();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await fetch(url, requestOptions);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const error = await response.json().catch(() => ({}));
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Failed to trigger event: ${response.statusText} - ${error.error || ""}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return await response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
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.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} socketId - Socket ID from the client SDK
|
|
108
|
+
* @param {string} channelName - Channel name (e.g., "private-chat" or "presence-lobby")
|
|
109
|
+
* @param {object} userData - User data for presence channels (must include user_id)
|
|
110
|
+
* @returns {object} - Signed authentication data: { auth, channel_data? }
|
|
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
|
+
* });
|
|
130
|
+
*/
|
|
131
|
+
authenticate(socketId, channelName, userData = null) {
|
|
132
|
+
// Build the full topic name
|
|
133
|
+
const fullTopic = `app:${this.appKey}:room:${channelName}`;
|
|
134
|
+
|
|
135
|
+
// Build string to sign based on channel type
|
|
136
|
+
let stringToSign;
|
|
137
|
+
let channelData = null;
|
|
138
|
+
|
|
139
|
+
if (userData && channelName.startsWith("presence-")) {
|
|
140
|
+
// Presence channel: include user data in signature
|
|
141
|
+
channelData = JSON.stringify(userData);
|
|
142
|
+
stringToSign = `${socketId}:${fullTopic}:${channelData}`;
|
|
143
|
+
} else {
|
|
144
|
+
// Private channel: just socket_id and topic
|
|
145
|
+
stringToSign = `${socketId}:${fullTopic}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Generate HMAC-SHA256 signature using app_secret
|
|
149
|
+
const signature = crypto
|
|
150
|
+
.createHmac("sha256", this.appSecret)
|
|
151
|
+
.update(stringToSign)
|
|
152
|
+
.digest("hex");
|
|
153
|
+
|
|
154
|
+
const auth = `${this.appKey}:${signature}`;
|
|
155
|
+
|
|
156
|
+
// Build response
|
|
157
|
+
const response = { auth };
|
|
158
|
+
|
|
159
|
+
if (channelData) {
|
|
160
|
+
response.channel_data = channelData;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default PingerchipsServer;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pingerchips-js-server",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Pingerchips server SDK for Node.js - trigger events and authenticate users",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pingerchips",
|
|
12
|
+
"websocket",
|
|
13
|
+
"realtime",
|
|
14
|
+
"server",
|
|
15
|
+
"backend",
|
|
16
|
+
"pusher"
|
|
17
|
+
],
|
|
18
|
+
"author": "Pingerchips",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"node-fetch": "^3.3.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/pingerchips/pingerchips-js-server"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/pingerchips/pingerchips-js-server/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://pingerchips.com",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|