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.
@@ -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
+ }