@nextxai/sdk 0.2.0 → 0.3.0-beta.31
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/CHANGELOG.md +30 -0
- package/README.md +26 -0
- package/dist/cjs/phone.d.ts +2 -0
- package/dist/cjs/phone.js +4 -0
- package/dist/cjs/schema.d.ts +756 -19
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/webhooks.d.ts +92 -0
- package/dist/cjs/webhooks.js +81 -0
- package/dist/esm/phone.d.ts +2 -0
- package/dist/esm/phone.js +4 -0
- package/dist/esm/schema.d.ts +756 -19
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/webhooks.d.ts +92 -0
- package/dist/esm/webhooks.js +75 -0
- package/package.json +12 -2
package/dist/cjs/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0-beta.31";
|
package/dist/cjs/version.js
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Hai lần giao cách nhau quá ngưỡng này (giây) bị coi là phát lại. */
|
|
2
|
+
export declare const DEFAULT_TOLERANCE_SEC = 300;
|
|
3
|
+
/** Tổng đài viên trong dữ liệu công khai. */
|
|
4
|
+
export interface WebhookAgent {
|
|
5
|
+
id: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
name: string | null;
|
|
8
|
+
email: string | null;
|
|
9
|
+
extension: string;
|
|
10
|
+
}
|
|
11
|
+
/** `data.object` của các sự kiện `call.*` — cùng hình dạng với `GET /api/v1/call/partner/calls/{id}`. */
|
|
12
|
+
export interface WebhookCall {
|
|
13
|
+
callId: string;
|
|
14
|
+
/** 1 = ringing · 2 = answered · 3 = completed/missed · 4 = recording_ready · 5 = summary_ready — sắp xếp khi tới lệch thứ tự. */
|
|
15
|
+
sequence: number;
|
|
16
|
+
/** queued · initiated · ringing · in-progress · completed · busy · no-answer · failed · canceled · abandoned */
|
|
17
|
+
status: string;
|
|
18
|
+
direction: "inbound" | "outbound";
|
|
19
|
+
/** E.164 khi chuẩn hoá được. */
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
agent: WebhookAgent | null;
|
|
23
|
+
queueId: string | null;
|
|
24
|
+
queueName: string | null;
|
|
25
|
+
startedAt: string | null;
|
|
26
|
+
answeredAt: string | null;
|
|
27
|
+
endedAt: string | null;
|
|
28
|
+
durationSec: number | null;
|
|
29
|
+
talkSec: number | null;
|
|
30
|
+
waitSec: number | null;
|
|
31
|
+
/** Mã Q.850 (16 = gác bình thường, 17 = bận, 19 = không trả lời, 41 = lỗi tạm thời…). */
|
|
32
|
+
hangupCause: number | null;
|
|
33
|
+
hangupCauseName: string | null;
|
|
34
|
+
/** `contactRef` bạn gửi lúc gọi ra — trả lại nguyên văn. */
|
|
35
|
+
contactRef: string | null;
|
|
36
|
+
/** `metadata` bạn gửi lúc gọi ra — trả lại nguyên văn. */
|
|
37
|
+
metadata: Record<string, string> | null;
|
|
38
|
+
crmContactId: string | null;
|
|
39
|
+
hasRecording: boolean;
|
|
40
|
+
hasSummary: boolean;
|
|
41
|
+
/** Tóm tắt AI — có từ sự kiện `call.summary_ready` (null trước đó). */
|
|
42
|
+
summary: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** `data.object` của `agent.status_changed`. */
|
|
45
|
+
export interface WebhookAgentStatus {
|
|
46
|
+
agent: WebhookAgent;
|
|
47
|
+
from: string;
|
|
48
|
+
to: string;
|
|
49
|
+
reason: string | null;
|
|
50
|
+
changedAt: string;
|
|
51
|
+
}
|
|
52
|
+
export interface WebhookObjectMap {
|
|
53
|
+
"call.ringing": WebhookCall;
|
|
54
|
+
"call.answered": WebhookCall;
|
|
55
|
+
"call.completed": WebhookCall;
|
|
56
|
+
"call.missed": WebhookCall;
|
|
57
|
+
"call.recording_ready": WebhookCall;
|
|
58
|
+
"call.summary_ready": WebhookCall;
|
|
59
|
+
"agent.status_changed": WebhookAgentStatus;
|
|
60
|
+
}
|
|
61
|
+
interface Envelope<K extends string, T> {
|
|
62
|
+
id: string;
|
|
63
|
+
type: K;
|
|
64
|
+
api_version: string;
|
|
65
|
+
created_at: string;
|
|
66
|
+
workspace_id: string;
|
|
67
|
+
livemode: boolean;
|
|
68
|
+
data: {
|
|
69
|
+
object: T;
|
|
70
|
+
};
|
|
71
|
+
metadata?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** Sự kiện đã kiểm chữ ký. Thu hẹp kiểu bằng `event.type`. Loại sự kiện khác (CRM, Bán hàng…) có `data.object` là `unknown`. */
|
|
74
|
+
export type WebhookEvent = {
|
|
75
|
+
[K in keyof WebhookObjectMap]: Envelope<K, WebhookObjectMap[K]>;
|
|
76
|
+
}[keyof WebhookObjectMap] | Envelope<string & {}, unknown>;
|
|
77
|
+
export declare class WebhookVerificationError extends Error {
|
|
78
|
+
constructor(message: string);
|
|
79
|
+
}
|
|
80
|
+
type HeaderBag = Headers | Record<string, string | string[] | undefined>;
|
|
81
|
+
/** Ký một thân webhook — dùng để viết test cho phần nhận webhook của bạn. */
|
|
82
|
+
export declare function signWebhook(secret: string, id: string, timestamp: number, body: string | Buffer): string;
|
|
83
|
+
/**
|
|
84
|
+
* Kiểm `webhook-id` / `webhook-timestamp` / `webhook-signature` rồi trả sự kiện đã parse.
|
|
85
|
+
* Ném {@link WebhookVerificationError} khi thiếu header, chữ ký sai, hoặc lệch giờ quá `toleranceSec`.
|
|
86
|
+
* Dùng `webhook-id` (cũng là `event.id`) để bỏ qua lần giao trùng — NextX giao lại khi bạn không trả 2xx.
|
|
87
|
+
*/
|
|
88
|
+
export declare function verifyWebhook(body: string | Buffer | Uint8Array, headers: HeaderBag, secret: string, options?: {
|
|
89
|
+
toleranceSec?: number;
|
|
90
|
+
now?: Date;
|
|
91
|
+
}): WebhookEvent;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookVerificationError = exports.DEFAULT_TOLERANCE_SEC = void 0;
|
|
4
|
+
exports.signWebhook = signWebhook;
|
|
5
|
+
exports.verifyWebhook = verifyWebhook;
|
|
6
|
+
/// <reference types="node" />
|
|
7
|
+
/**
|
|
8
|
+
* Kiểm chữ ký webhook NextX theo chuẩn mở Standard Webhooks (https://www.standardwebhooks.com) — chỉ chạy trên máy chủ.
|
|
9
|
+
*
|
|
10
|
+
* import { verifyWebhook } from "@nextxai/sdk/webhooks";
|
|
11
|
+
*
|
|
12
|
+
* app.post("/nextx/webhook", express.raw({ type: "application/json" }), (req, res) => {
|
|
13
|
+
* const event = verifyWebhook(req.body, req.headers, process.env.NEXTX_WEBHOOK_SECRET!);
|
|
14
|
+
* if (event.type === "call.completed") saveCall(event.data.object);
|
|
15
|
+
* res.sendStatus(204);
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* Phải truyền THÂN GỐC (Buffer/chuỗi đúng từng byte) — JSON đã parse rồi stringify lại sẽ lệch chữ ký.
|
|
19
|
+
* Bí mật dạng `whsec_…` lấy ở developers.nextx.vn › Webhooks (xoay khoá ⇒ header có nhiều chữ ký, khớp một là đủ).
|
|
20
|
+
*/
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
/** Hai lần giao cách nhau quá ngưỡng này (giây) bị coi là phát lại. */
|
|
23
|
+
exports.DEFAULT_TOLERANCE_SEC = 300;
|
|
24
|
+
class WebhookVerificationError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "WebhookVerificationError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
31
|
+
function header(headers, name) {
|
|
32
|
+
if (typeof headers.get === "function")
|
|
33
|
+
return headers.get(name) ?? undefined;
|
|
34
|
+
const bag = headers;
|
|
35
|
+
const hit = Object.keys(bag).find((k) => k.toLowerCase() === name);
|
|
36
|
+
const v = hit === undefined ? undefined : bag[hit];
|
|
37
|
+
return Array.isArray(v) ? v[0] : v;
|
|
38
|
+
}
|
|
39
|
+
function keyOf(secret) {
|
|
40
|
+
if (!secret?.startsWith("whsec_"))
|
|
41
|
+
throw new WebhookVerificationError("secret phải có dạng whsec_…");
|
|
42
|
+
const key = Buffer.from(secret.slice(6), "base64");
|
|
43
|
+
if (key.length < 24 || key.length > 64)
|
|
44
|
+
throw new WebhookVerificationError("secret không hợp lệ");
|
|
45
|
+
return key;
|
|
46
|
+
}
|
|
47
|
+
/** Ký một thân webhook — dùng để viết test cho phần nhận webhook của bạn. */
|
|
48
|
+
function signWebhook(secret, id, timestamp, body) {
|
|
49
|
+
const mac = (0, node_crypto_1.createHmac)("sha256", keyOf(secret));
|
|
50
|
+
mac.update(`${id}.${timestamp}.`);
|
|
51
|
+
mac.update(body);
|
|
52
|
+
return `v1,${mac.digest("base64")}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Kiểm `webhook-id` / `webhook-timestamp` / `webhook-signature` rồi trả sự kiện đã parse.
|
|
56
|
+
* Ném {@link WebhookVerificationError} khi thiếu header, chữ ký sai, hoặc lệch giờ quá `toleranceSec`.
|
|
57
|
+
* Dùng `webhook-id` (cũng là `event.id`) để bỏ qua lần giao trùng — NextX giao lại khi bạn không trả 2xx.
|
|
58
|
+
*/
|
|
59
|
+
function verifyWebhook(body, headers, secret, options = {}) {
|
|
60
|
+
const id = header(headers, "webhook-id");
|
|
61
|
+
const ts = header(headers, "webhook-timestamp");
|
|
62
|
+
const sig = header(headers, "webhook-signature");
|
|
63
|
+
if (!id || !ts || !sig)
|
|
64
|
+
throw new WebhookVerificationError("thiếu header webhook-id / webhook-timestamp / webhook-signature");
|
|
65
|
+
const timestamp = Number(ts);
|
|
66
|
+
const now = Math.floor((options.now ?? new Date()).getTime() / 1000);
|
|
67
|
+
if (!Number.isInteger(timestamp) || Math.abs(now - timestamp) > (options.toleranceSec ?? exports.DEFAULT_TOLERANCE_SEC))
|
|
68
|
+
throw new WebhookVerificationError("webhook-timestamp lệch quá ngưỡng");
|
|
69
|
+
const raw = typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body);
|
|
70
|
+
const expected = Buffer.from(signWebhook(secret, id, timestamp, raw));
|
|
71
|
+
const ok = sig
|
|
72
|
+
.split(" ")
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.some((s) => {
|
|
75
|
+
const candidate = Buffer.from(s);
|
|
76
|
+
return candidate.length === expected.length && (0, node_crypto_1.timingSafeEqual)(candidate, expected);
|
|
77
|
+
});
|
|
78
|
+
if (!ok)
|
|
79
|
+
throw new WebhookVerificationError("chữ ký không khớp");
|
|
80
|
+
return JSON.parse(raw.toString("utf8"));
|
|
81
|
+
}
|
package/dist/esm/phone.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export declare class NextXPhone {
|
|
|
73
73
|
call(number: string, opts?: {
|
|
74
74
|
contactRef?: string;
|
|
75
75
|
}): void;
|
|
76
|
+
/** Nhấc cuộc đang đổ chuông (sự kiện `incoming`) — y như tổng đài viên bấm "Nghe" trong khung. */
|
|
77
|
+
answer(): void;
|
|
76
78
|
/** Gác cuộc gọi đang diễn ra. */
|
|
77
79
|
hangup(): void;
|
|
78
80
|
/** Đăng xuất NextX trong khung softphone. */
|
package/dist/esm/phone.js
CHANGED
|
@@ -97,6 +97,10 @@ export class NextXPhone {
|
|
|
97
97
|
call(number, opts = {}) {
|
|
98
98
|
this.send({ type: "call", number: String(number), contactRef: opts.contactRef });
|
|
99
99
|
}
|
|
100
|
+
/** Nhấc cuộc đang đổ chuông (sự kiện `incoming`) — y như tổng đài viên bấm "Nghe" trong khung. */
|
|
101
|
+
answer() {
|
|
102
|
+
this.send({ type: "answer" });
|
|
103
|
+
}
|
|
100
104
|
/** Gác cuộc gọi đang diễn ra. */
|
|
101
105
|
hangup() {
|
|
102
106
|
this.send({ type: "hangup" });
|