orqo-node-sdk 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/README.md +547 -0
- package/README.pdf +0 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.js +174 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/instance.d.ts +166 -0
- package/dist/instance.js +365 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.js +18 -0
- package/dist/webhook-handler.d.ts +54 -0
- package/dist/webhook-handler.js +161 -0
- package/package.json +36 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Handler — Zero-dependency event dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Creates a request handler that verifies HMAC signatures
|
|
5
|
+
* and dispatches typed events to registered callbacks.
|
|
6
|
+
*
|
|
7
|
+
* Works with any HTTP framework (Express, Hono, Fastify, etc.)
|
|
8
|
+
* via a generic interface.
|
|
9
|
+
*
|
|
10
|
+
* @example Express
|
|
11
|
+
* ```ts
|
|
12
|
+
* import express from 'express';
|
|
13
|
+
* import { createWebhookHandler } from '@orqo/sdk';
|
|
14
|
+
*
|
|
15
|
+
* const app = express();
|
|
16
|
+
* app.use(express.json());
|
|
17
|
+
*
|
|
18
|
+
* app.post('/webhooks/orqo', createWebhookHandler({
|
|
19
|
+
* secret: 'my-hmac-secret',
|
|
20
|
+
* onConnectionChanged: (e) => console.log(`${e.instanceId} → ${e.state}`),
|
|
21
|
+
* onMessageReceived: (e) => console.log(`From ${e.from}: ${e.text}`),
|
|
22
|
+
* onMessageProcessed: (e) => console.log(`Processed: ${e.messageId}`),
|
|
23
|
+
* onHandoff: (e) => console.log(`Handoff: ${e.sessionId}`),
|
|
24
|
+
* }));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Creates a webhook handler function compatible with Express/Hono/etc.
|
|
29
|
+
*
|
|
30
|
+
* The handler:
|
|
31
|
+
* 1. Verifies the HMAC signature (if secret is configured)
|
|
32
|
+
* 2. Parses the event
|
|
33
|
+
* 3. Dispatches to the appropriate typed callback
|
|
34
|
+
* 4. Returns 200 OK
|
|
35
|
+
*/
|
|
36
|
+
export function createWebhookHandler(options) {
|
|
37
|
+
return async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
// 1. Verify HMAC signature
|
|
40
|
+
if (options.secret) {
|
|
41
|
+
const signature = getHeader(req, 'x-webhook-signature');
|
|
42
|
+
if (!signature) {
|
|
43
|
+
sendResponse(res, 401, { error: 'Missing X-Webhook-Signature header' });
|
|
44
|
+
options.onVerificationFailed?.(new Error('Missing signature'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
|
|
48
|
+
const isValid = await verifyHmac(options.secret, body, signature);
|
|
49
|
+
if (!isValid) {
|
|
50
|
+
sendResponse(res, 403, { error: 'Invalid signature' });
|
|
51
|
+
options.onVerificationFailed?.(new Error('Invalid HMAC signature'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2. Parse event
|
|
56
|
+
const event = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body);
|
|
57
|
+
if (!event || !event.event) {
|
|
58
|
+
sendResponse(res, 400, { error: 'Invalid event payload' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// 3. Dispatch to typed callbacks
|
|
62
|
+
switch (event.event) {
|
|
63
|
+
case 'connection:changed':
|
|
64
|
+
await options.onConnectionChanged?.(event);
|
|
65
|
+
break;
|
|
66
|
+
case 'message:received':
|
|
67
|
+
await options.onMessageReceived?.(event);
|
|
68
|
+
break;
|
|
69
|
+
case 'message:processed':
|
|
70
|
+
await options.onMessageProcessed?.(event);
|
|
71
|
+
break;
|
|
72
|
+
case 'session:handoff':
|
|
73
|
+
await options.onHandoff?.(event);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
// Always call onEvent for catch-all
|
|
77
|
+
await options.onEvent?.(event);
|
|
78
|
+
// 4. Acknowledge
|
|
79
|
+
sendResponse(res, 200, { received: true });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
sendResponse(res, 500, { error: 'Webhook handler error' });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// HMAC Verification (works in Node.js and Web Crypto)
|
|
88
|
+
// ============================================================================
|
|
89
|
+
async function verifyHmac(secret, body, signature) {
|
|
90
|
+
// Expected format: "sha256=<hex>"
|
|
91
|
+
const expectedHex = signature.startsWith('sha256=')
|
|
92
|
+
? signature.slice(7)
|
|
93
|
+
: signature;
|
|
94
|
+
// Try Web Crypto API first (universal), then Node.js crypto
|
|
95
|
+
if (typeof globalThis.crypto?.subtle !== 'undefined') {
|
|
96
|
+
return verifyHmacWebCrypto(secret, body, expectedHex);
|
|
97
|
+
}
|
|
98
|
+
// Node.js fallback
|
|
99
|
+
try {
|
|
100
|
+
const crypto = await import('crypto');
|
|
101
|
+
const expected = crypto
|
|
102
|
+
.createHmac('sha256', secret)
|
|
103
|
+
.update(body)
|
|
104
|
+
.digest('hex');
|
|
105
|
+
return timingSafeEqual(expected, expectedHex);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function verifyHmacWebCrypto(secret, body, expectedHex) {
|
|
112
|
+
const encoder = new TextEncoder();
|
|
113
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
114
|
+
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
|
|
115
|
+
const computed = Array.from(new Uint8Array(sig))
|
|
116
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
117
|
+
.join('');
|
|
118
|
+
return timingSafeEqual(computed, expectedHex);
|
|
119
|
+
}
|
|
120
|
+
/** Constant-time string comparison to prevent timing attacks. */
|
|
121
|
+
function timingSafeEqual(a, b) {
|
|
122
|
+
if (a.length !== b.length)
|
|
123
|
+
return false;
|
|
124
|
+
let result = 0;
|
|
125
|
+
for (let i = 0; i < a.length; i++) {
|
|
126
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
127
|
+
}
|
|
128
|
+
return result === 0;
|
|
129
|
+
}
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Helpers
|
|
132
|
+
// ============================================================================
|
|
133
|
+
function getHeader(req, name) {
|
|
134
|
+
if (!req.headers)
|
|
135
|
+
return null;
|
|
136
|
+
// Express-style: req.headers[name]
|
|
137
|
+
if (typeof req.headers === 'object' && !('get' in req.headers)) {
|
|
138
|
+
const headers = req.headers;
|
|
139
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
140
|
+
return Array.isArray(value) ? value[0] : value ?? null;
|
|
141
|
+
}
|
|
142
|
+
// Hono/Fetch-style: req.headers.get(name)
|
|
143
|
+
if ('get' in req.headers && typeof req.headers.get === 'function') {
|
|
144
|
+
return req.headers.get(name);
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function sendResponse(res, status, body) {
|
|
149
|
+
// Express-style
|
|
150
|
+
if (res.status && res.json) {
|
|
151
|
+
const r = res.status(status);
|
|
152
|
+
if (r && r.json)
|
|
153
|
+
r.json(body);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Node http-style
|
|
157
|
+
if (res.writeHead && res.end) {
|
|
158
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
159
|
+
res.end(JSON.stringify(body));
|
|
160
|
+
}
|
|
161
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orqo-node-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Orqo Gateway — TypeScript SDK for plug-and-play WhatsApp integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"orqo",
|
|
25
|
+
"whatsapp",
|
|
26
|
+
"sdk",
|
|
27
|
+
"bot",
|
|
28
|
+
"gateway"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.3.0",
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
35
|
+
}
|
|
36
|
+
}
|