@kavchat/node 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +93 -2
- package/dist/cjs/client.d.ts +39 -0
- package/dist/cjs/client.js +116 -0
- package/dist/cjs/errors.d.ts +21 -0
- package/dist/cjs/errors.js +37 -0
- package/dist/cjs/identity.d.ts +19 -0
- package/dist/cjs/identity.js +40 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +18 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types.d.ts +105 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/webhook.d.ts +14 -0
- package/dist/cjs/webhook.js +49 -0
- package/dist/esm/client.d.ts +39 -0
- package/dist/esm/client.js +112 -0
- package/dist/esm/errors.d.ts +21 -0
- package/dist/esm/errors.js +31 -0
- package/dist/esm/identity.d.ts +19 -0
- package/dist/esm/identity.js +36 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/types.d.ts +105 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/webhook.d.ts +14 -0
- package/dist/esm/webhook.js +45 -0
- package/package.json +57 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maxsave
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kavchat/node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Kavchat server SDK for Node.js 18+. TypeScript types included, ESM and CommonJS builds, no runtime dependencies (uses the global `fetch` and `node:crypto`).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add @kavchat/node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package name is a placeholder until it is published.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Kavchat } from '@kavchat/node';
|
|
17
|
+
// const { Kavchat } = require('@kavchat/node');
|
|
18
|
+
|
|
19
|
+
const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!, {
|
|
20
|
+
// baseUrl: 'https://api.kavchat.com',
|
|
21
|
+
// timeoutMs: 30_000,
|
|
22
|
+
// locale: 'en', // language of error messages
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Identify your users
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const token = kavchat.createUserToken({
|
|
30
|
+
userId: 'user_123', // required, 1 to 128 characters
|
|
31
|
+
phone: '+972501234567', // optional
|
|
32
|
+
name: 'Dana', // optional
|
|
33
|
+
email: 'dana@example.com', // optional
|
|
34
|
+
expiresIn: 7 * 24 * 3600, // seconds, default 7 days, at most 30 days
|
|
35
|
+
});
|
|
36
|
+
// Send the token to your page or app and pass it to the Kavchat widget or SDK.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`createUserToken(secretKey, options)` is also exported as a standalone function.
|
|
40
|
+
|
|
41
|
+
## Verify webhooks
|
|
42
|
+
|
|
43
|
+
Use the raw body. A body that was parsed and then serialized again will not match the signature.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import express from 'express';
|
|
47
|
+
import { WebhookVerificationError } from '@kavchat/node';
|
|
48
|
+
|
|
49
|
+
app.post('/kavchat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const event = kavchat.verifyWebhook(req.body, req.get('Kavchat-Signature'));
|
|
52
|
+
if (event.type === 'ticket.created') console.log('New ticket', event.data.number);
|
|
53
|
+
res.sendStatus(200);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err instanceof WebhookVerificationError) return res.status(400).send(err.code);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Options: `kavchat.verifyWebhook(body, header, { toleranceSeconds: 300 })`.
|
|
62
|
+
|
|
63
|
+
## Server API
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await kavchat.getWorkspace();
|
|
67
|
+
await kavchat.listTickets({ status: 'open', limit: 50, before: 120 });
|
|
68
|
+
await kavchat.getTicket(id);
|
|
69
|
+
await kavchat.setTicketStatus(id, 'closed');
|
|
70
|
+
await kavchat.getReports({ from: new Date('2026-01-01'), to: new Date() });
|
|
71
|
+
await kavchat.getSettings();
|
|
72
|
+
await kavchat.updateSettings({ audience: 'testers', testerPhones: ['+972501234567'] });
|
|
73
|
+
await kavchat.listReleases();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Errors are thrown as `KavchatApiError` with `status`, `code`, `message` and `details`. `status` is 0 when no response arrived (`code` is `timeout` or `network_error`).
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { KavchatApiError } from '@kavchat/node';
|
|
80
|
+
try {
|
|
81
|
+
await kavchat.getTicket('nope');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof KavchatApiError && err.code === 'ticket_not_found') { /* ... */ }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
yarn install
|
|
91
|
+
yarn test
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The tests cover the shared vectors in `../test-vectors`, the client against a local HTTP server, the CommonJS build, and an end-to-end check against the server's own `verifyIdentityToken` and `signWebhook` (run with `tsx` from the repository; skipped when the server sources are not there).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type UserTokenOptions } from './identity.js';
|
|
2
|
+
import { type VerifyWebhookOptions } from './webhook.js';
|
|
3
|
+
import type { ListTicketsParams, ReleaseSummary, Report, ReportParams, Settings, Ticket, TicketStatus, UpdateSettingsParams, WebhookEvent, Workspace } from './types.js';
|
|
4
|
+
export declare const DEFAULT_BASE_URL = "https://api.kavchat.com";
|
|
5
|
+
export declare const VERSION = "0.0.1";
|
|
6
|
+
export interface KavchatOptions {
|
|
7
|
+
/** Default https://api.kavchat.com */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Per request, in milliseconds. Default 30000. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** Language of error messages (Accept-Language), e.g. "en" or "he". */
|
|
12
|
+
locale?: string;
|
|
13
|
+
/** Custom fetch implementation. Defaults to the global fetch. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Kavchat server client. Keep the secret key on your backend only.
|
|
18
|
+
*
|
|
19
|
+
* const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);
|
|
20
|
+
*/
|
|
21
|
+
export declare class Kavchat {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(secretKey: string, options?: KavchatOptions);
|
|
24
|
+
/** Signs an identity token for one of your users. See createUserToken(). */
|
|
25
|
+
createUserToken(options: UserTokenOptions): string;
|
|
26
|
+
/** Verifies a webhook request and returns the event. See verifyWebhook(). */
|
|
27
|
+
verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): WebhookEvent;
|
|
28
|
+
getWorkspace(): Promise<Workspace>;
|
|
29
|
+
/** Newest first. Page with `before` = the number of the last ticket you have. */
|
|
30
|
+
listTickets(params?: ListTicketsParams): Promise<Ticket[]>;
|
|
31
|
+
getTicket(id: string): Promise<Ticket>;
|
|
32
|
+
setTicketStatus(id: string, status: TicketStatus): Promise<Ticket>;
|
|
33
|
+
/** Defaults to the last 30 days. */
|
|
34
|
+
getReports(params?: ReportParams): Promise<Report>;
|
|
35
|
+
getSettings(): Promise<Settings>;
|
|
36
|
+
/** Only the audience and the tester phones can be changed from the server. */
|
|
37
|
+
updateSettings(params: UpdateSettingsParams): Promise<Settings>;
|
|
38
|
+
listReleases(): Promise<ReleaseSummary[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Kavchat = exports.VERSION = exports.DEFAULT_BASE_URL = void 0;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const identity_js_1 = require("./identity.js");
|
|
6
|
+
const webhook_js_1 = require("./webhook.js");
|
|
7
|
+
exports.DEFAULT_BASE_URL = 'https://api.kavchat.com';
|
|
8
|
+
exports.VERSION = '0.0.1';
|
|
9
|
+
/**
|
|
10
|
+
* Kavchat server client. Keep the secret key on your backend only.
|
|
11
|
+
*
|
|
12
|
+
* const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);
|
|
13
|
+
*/
|
|
14
|
+
class Kavchat {
|
|
15
|
+
#secretKey;
|
|
16
|
+
#baseUrl;
|
|
17
|
+
#timeoutMs;
|
|
18
|
+
#locale;
|
|
19
|
+
#fetch;
|
|
20
|
+
constructor(secretKey, options = {}) {
|
|
21
|
+
if (typeof secretKey !== 'string' || !secretKey)
|
|
22
|
+
throw new TypeError('secretKey is required');
|
|
23
|
+
this.#secretKey = secretKey;
|
|
24
|
+
this.#baseUrl = (options.baseUrl ?? exports.DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
25
|
+
this.#timeoutMs = options.timeoutMs ?? 30_000;
|
|
26
|
+
this.#locale = options.locale;
|
|
27
|
+
const impl = options.fetch ?? globalThis.fetch;
|
|
28
|
+
if (typeof impl !== 'function')
|
|
29
|
+
throw new TypeError('No fetch implementation found (Node 18+ required)');
|
|
30
|
+
this.#fetch = impl;
|
|
31
|
+
}
|
|
32
|
+
/** Signs an identity token for one of your users. See createUserToken(). */
|
|
33
|
+
createUserToken(options) {
|
|
34
|
+
return (0, identity_js_1.createUserToken)(this.#secretKey, options);
|
|
35
|
+
}
|
|
36
|
+
/** Verifies a webhook request and returns the event. See verifyWebhook(). */
|
|
37
|
+
verifyWebhook(rawBody, signatureHeader, options) {
|
|
38
|
+
return (0, webhook_js_1.verifyWebhook)(this.#secretKey, rawBody, signatureHeader, options);
|
|
39
|
+
}
|
|
40
|
+
getWorkspace() {
|
|
41
|
+
return this.#request('GET', '/workspace');
|
|
42
|
+
}
|
|
43
|
+
/** Newest first. Page with `before` = the number of the last ticket you have. */
|
|
44
|
+
listTickets(params = {}) {
|
|
45
|
+
return this.#request('GET', '/tickets', { query: { ...params } });
|
|
46
|
+
}
|
|
47
|
+
getTicket(id) {
|
|
48
|
+
return this.#request('GET', `/tickets/${encodeURIComponent(id)}`);
|
|
49
|
+
}
|
|
50
|
+
setTicketStatus(id, status) {
|
|
51
|
+
return this.#request('PUT', `/tickets/${encodeURIComponent(id)}/status`, { body: { status } });
|
|
52
|
+
}
|
|
53
|
+
/** Defaults to the last 30 days. */
|
|
54
|
+
getReports(params = {}) {
|
|
55
|
+
const iso = (d) => (d instanceof Date ? d.toISOString() : d);
|
|
56
|
+
return this.#request('GET', '/reports', { query: { from: iso(params.from), to: iso(params.to) } });
|
|
57
|
+
}
|
|
58
|
+
getSettings() {
|
|
59
|
+
return this.#request('GET', '/settings');
|
|
60
|
+
}
|
|
61
|
+
/** Only the audience and the tester phones can be changed from the server. */
|
|
62
|
+
updateSettings(params) {
|
|
63
|
+
return this.#request('PUT', '/settings', { body: params });
|
|
64
|
+
}
|
|
65
|
+
listReleases() {
|
|
66
|
+
return this.#request('GET', '/releases');
|
|
67
|
+
}
|
|
68
|
+
async #request(method, path, opts = {}) {
|
|
69
|
+
const url = new URL(`${this.#baseUrl}/v1/server${path}`);
|
|
70
|
+
for (const [key, value] of Object.entries(opts.query ?? {})) {
|
|
71
|
+
if (value !== undefined && value !== null && value !== '')
|
|
72
|
+
url.searchParams.set(key, String(value));
|
|
73
|
+
}
|
|
74
|
+
const headers = {
|
|
75
|
+
Authorization: `Bearer ${this.#secretKey}`,
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
'User-Agent': `kavchat-node/${exports.VERSION}`,
|
|
78
|
+
};
|
|
79
|
+
if (this.#locale)
|
|
80
|
+
headers['Accept-Language'] = this.#locale;
|
|
81
|
+
if (opts.body !== undefined)
|
|
82
|
+
headers['Content-Type'] = 'application/json';
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await this.#fetch(url, {
|
|
86
|
+
method,
|
|
87
|
+
headers,
|
|
88
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
89
|
+
signal: AbortSignal.timeout(this.#timeoutMs),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const name = err?.name;
|
|
94
|
+
if (name === 'TimeoutError' || name === 'AbortError') {
|
|
95
|
+
throw new errors_js_1.KavchatApiError(0, 'timeout', `Request timed out after ${this.#timeoutMs} ms`);
|
|
96
|
+
}
|
|
97
|
+
throw new errors_js_1.KavchatApiError(0, 'network_error', `Could not reach Kavchat: ${err?.message ?? err}`);
|
|
98
|
+
}
|
|
99
|
+
const text = await res.text();
|
|
100
|
+
let json;
|
|
101
|
+
try {
|
|
102
|
+
json = text ? JSON.parse(text) : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
json = undefined;
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok || !json || json.success !== true) {
|
|
108
|
+
if (json && typeof json.code === 'string') {
|
|
109
|
+
throw new errors_js_1.KavchatApiError(res.status, json.code, String(json.message ?? json.code), json.details);
|
|
110
|
+
}
|
|
111
|
+
throw new errors_js_1.KavchatApiError(res.status, 'invalid_response', `Unexpected response from Kavchat (HTTP ${res.status})`);
|
|
112
|
+
}
|
|
113
|
+
return json.data;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.Kavchat = Kavchat;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Base class of every error thrown by this SDK. */
|
|
2
|
+
export declare class KavchatError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The Server API answered with an error, or could not be reached.
|
|
7
|
+
* `status` is the HTTP status, or 0 when no response arrived (`code` is then
|
|
8
|
+
* `timeout` or `network_error`).
|
|
9
|
+
*/
|
|
10
|
+
export declare class KavchatApiError extends KavchatError {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly details?: unknown;
|
|
14
|
+
constructor(status: number, code: string, message: string, details?: unknown);
|
|
15
|
+
}
|
|
16
|
+
export type WebhookErrorCode = 'malformed_header' | 'timestamp_out_of_range' | 'invalid_signature' | 'invalid_payload';
|
|
17
|
+
/** A webhook request failed verification. Answer it with HTTP 400. */
|
|
18
|
+
export declare class WebhookVerificationError extends KavchatError {
|
|
19
|
+
readonly code: WebhookErrorCode;
|
|
20
|
+
constructor(code: WebhookErrorCode, message: string);
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookVerificationError = exports.KavchatApiError = exports.KavchatError = void 0;
|
|
4
|
+
/** Base class of every error thrown by this SDK. */
|
|
5
|
+
class KavchatError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.KavchatError = KavchatError;
|
|
12
|
+
/**
|
|
13
|
+
* The Server API answered with an error, or could not be reached.
|
|
14
|
+
* `status` is the HTTP status, or 0 when no response arrived (`code` is then
|
|
15
|
+
* `timeout` or `network_error`).
|
|
16
|
+
*/
|
|
17
|
+
class KavchatApiError extends KavchatError {
|
|
18
|
+
status;
|
|
19
|
+
code;
|
|
20
|
+
details;
|
|
21
|
+
constructor(status, code, message, details) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.details = details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.KavchatApiError = KavchatApiError;
|
|
29
|
+
/** A webhook request failed verification. Answer it with HTTP 400. */
|
|
30
|
+
class WebhookVerificationError extends KavchatError {
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.WebhookVerificationError = WebhookVerificationError;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface UserTokenOptions {
|
|
2
|
+
/** Your own user id. Becomes the `sub` claim. 1 to 128 characters. */
|
|
3
|
+
userId: string;
|
|
4
|
+
phone?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
/** Lifetime in seconds. Default 7 days, at most 30 days. */
|
|
8
|
+
expiresIn?: number;
|
|
9
|
+
/** Issue time as unix seconds. Defaults to now; useful in tests. */
|
|
10
|
+
issuedAt?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_TOKEN_TTL_SECONDS: number;
|
|
13
|
+
export declare const MAX_TOKEN_TTL_SECONDS: number;
|
|
14
|
+
/**
|
|
15
|
+
* Signs an identity token (HS256 JWT) for one of your users.
|
|
16
|
+
* Pass it to the widget or the mobile SDK; Kavchat then tags sessions and
|
|
17
|
+
* tickets with `userId` and can show the bot only to testers.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createUserToken(secretKey: string, options: UserTokenOptions): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_TOKEN_TTL_SECONDS = exports.DEFAULT_TOKEN_TTL_SECONDS = void 0;
|
|
4
|
+
exports.createUserToken = createUserToken;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
exports.DEFAULT_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
7
|
+
exports.MAX_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
8
|
+
/** base64url of {"alg":"HS256","typ":"JWT"} */
|
|
9
|
+
const HEADER = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
10
|
+
/**
|
|
11
|
+
* Signs an identity token (HS256 JWT) for one of your users.
|
|
12
|
+
* Pass it to the widget or the mobile SDK; Kavchat then tags sessions and
|
|
13
|
+
* tickets with `userId` and can show the bot only to testers.
|
|
14
|
+
*/
|
|
15
|
+
function createUserToken(secretKey, options) {
|
|
16
|
+
if (typeof secretKey !== 'string' || !secretKey)
|
|
17
|
+
throw new TypeError('secretKey is required');
|
|
18
|
+
const { userId, phone, name, email } = options;
|
|
19
|
+
if (typeof userId !== 'string' || userId.length === 0 || userId.length > 128) {
|
|
20
|
+
throw new RangeError('userId must be a string of 1 to 128 characters');
|
|
21
|
+
}
|
|
22
|
+
const expiresIn = options.expiresIn ?? exports.DEFAULT_TOKEN_TTL_SECONDS;
|
|
23
|
+
if (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > exports.MAX_TOKEN_TTL_SECONDS) {
|
|
24
|
+
throw new RangeError(`expiresIn must be a whole number of seconds between 1 and ${exports.MAX_TOKEN_TTL_SECONDS}`);
|
|
25
|
+
}
|
|
26
|
+
const iat = options.issuedAt ?? Math.floor(Date.now() / 1000);
|
|
27
|
+
// Claim order is fixed (sub, phone, name, email, iat, exp) so every SDK signs the same bytes.
|
|
28
|
+
const claims = { sub: userId };
|
|
29
|
+
if (phone)
|
|
30
|
+
claims.phone = phone;
|
|
31
|
+
if (name)
|
|
32
|
+
claims.name = name;
|
|
33
|
+
if (email)
|
|
34
|
+
claims.email = email;
|
|
35
|
+
claims.iat = iat;
|
|
36
|
+
claims.exp = iat + expiresIn;
|
|
37
|
+
const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
|
|
38
|
+
const signature = (0, node_crypto_1.createHmac)('sha256', secretKey).update(`${HEADER}.${payload}`).digest('base64url');
|
|
39
|
+
return `${HEADER}.${payload}.${signature}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Kavchat, DEFAULT_BASE_URL, VERSION, type KavchatOptions } from './client.js';
|
|
2
|
+
export { createUserToken, DEFAULT_TOKEN_TTL_SECONDS, MAX_TOKEN_TTL_SECONDS, type UserTokenOptions, } from './identity.js';
|
|
3
|
+
export { verifyWebhook, SIGNATURE_HEADER, type VerifyWebhookOptions } from './webhook.js';
|
|
4
|
+
export { KavchatError, KavchatApiError, WebhookVerificationError, type WebhookErrorCode } from './errors.js';
|
|
5
|
+
export type * from './types.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookVerificationError = exports.KavchatApiError = exports.KavchatError = exports.SIGNATURE_HEADER = exports.verifyWebhook = exports.MAX_TOKEN_TTL_SECONDS = exports.DEFAULT_TOKEN_TTL_SECONDS = exports.createUserToken = exports.VERSION = exports.DEFAULT_BASE_URL = exports.Kavchat = void 0;
|
|
4
|
+
var client_js_1 = require("./client.js");
|
|
5
|
+
Object.defineProperty(exports, "Kavchat", { enumerable: true, get: function () { return client_js_1.Kavchat; } });
|
|
6
|
+
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return client_js_1.DEFAULT_BASE_URL; } });
|
|
7
|
+
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return client_js_1.VERSION; } });
|
|
8
|
+
var identity_js_1 = require("./identity.js");
|
|
9
|
+
Object.defineProperty(exports, "createUserToken", { enumerable: true, get: function () { return identity_js_1.createUserToken; } });
|
|
10
|
+
Object.defineProperty(exports, "DEFAULT_TOKEN_TTL_SECONDS", { enumerable: true, get: function () { return identity_js_1.DEFAULT_TOKEN_TTL_SECONDS; } });
|
|
11
|
+
Object.defineProperty(exports, "MAX_TOKEN_TTL_SECONDS", { enumerable: true, get: function () { return identity_js_1.MAX_TOKEN_TTL_SECONDS; } });
|
|
12
|
+
var webhook_js_1 = require("./webhook.js");
|
|
13
|
+
Object.defineProperty(exports, "verifyWebhook", { enumerable: true, get: function () { return webhook_js_1.verifyWebhook; } });
|
|
14
|
+
Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return webhook_js_1.SIGNATURE_HEADER; } });
|
|
15
|
+
var errors_js_1 = require("./errors.js");
|
|
16
|
+
Object.defineProperty(exports, "KavchatError", { enumerable: true, get: function () { return errors_js_1.KavchatError; } });
|
|
17
|
+
Object.defineProperty(exports, "KavchatApiError", { enumerable: true, get: function () { return errors_js_1.KavchatApiError; } });
|
|
18
|
+
Object.defineProperty(exports, "WebhookVerificationError", { enumerable: true, get: function () { return errors_js_1.WebhookVerificationError; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "commonjs" }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type TicketStatus = 'open' | 'closed';
|
|
2
|
+
export type Audience = 'off' | 'testers' | 'all';
|
|
3
|
+
export type Locale = 'he' | 'en' | 'ar' | 'ru' | 'es' | 'fr' | 'de' | 'pt' | 'it' | 'tr';
|
|
4
|
+
export interface TranscriptEntry {
|
|
5
|
+
from: 'bot' | 'user';
|
|
6
|
+
text: string;
|
|
7
|
+
stepId?: string;
|
|
8
|
+
at?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Ticket {
|
|
11
|
+
id: string;
|
|
12
|
+
number: number;
|
|
13
|
+
status: TicketStatus;
|
|
14
|
+
name: string | null;
|
|
15
|
+
email: string | null;
|
|
16
|
+
/** E.164 */
|
|
17
|
+
phone: string | null;
|
|
18
|
+
/** The `sub` claim of the identity token, when the user was identified. */
|
|
19
|
+
userId: string | null;
|
|
20
|
+
topicId: string | null;
|
|
21
|
+
topicName: string | null;
|
|
22
|
+
scriptId: string | null;
|
|
23
|
+
sessionId: string | null;
|
|
24
|
+
message: string;
|
|
25
|
+
transcript: TranscriptEntry[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ContactTopic {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Settings {
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
audience: Audience;
|
|
36
|
+
testerPhones: string[];
|
|
37
|
+
botName: string;
|
|
38
|
+
avatarText: string;
|
|
39
|
+
avatarImageUrl: string | null;
|
|
40
|
+
subtitle: string;
|
|
41
|
+
primaryColor: string;
|
|
42
|
+
position: 'start' | 'end';
|
|
43
|
+
contactTopics: ContactTopic[];
|
|
44
|
+
}
|
|
45
|
+
export interface Workspace {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
locale: Locale;
|
|
49
|
+
defaultCountry: string;
|
|
50
|
+
plan: string;
|
|
51
|
+
publicKey: string;
|
|
52
|
+
secretKeyHint: string;
|
|
53
|
+
webhookUrl: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface ReleaseSummary {
|
|
56
|
+
version: number;
|
|
57
|
+
note?: string;
|
|
58
|
+
restoredFrom?: number;
|
|
59
|
+
publishedAt: string;
|
|
60
|
+
scripts: {
|
|
61
|
+
id: string;
|
|
62
|
+
kind: string;
|
|
63
|
+
name: string;
|
|
64
|
+
}[];
|
|
65
|
+
}
|
|
66
|
+
export interface ReportTotals {
|
|
67
|
+
sessions: number;
|
|
68
|
+
open: number;
|
|
69
|
+
answered: number;
|
|
70
|
+
contact: number;
|
|
71
|
+
abandoned: number;
|
|
72
|
+
}
|
|
73
|
+
export interface UnmatchedQuestion {
|
|
74
|
+
groupKey: string;
|
|
75
|
+
sample: string;
|
|
76
|
+
count: number;
|
|
77
|
+
lastAt: string;
|
|
78
|
+
}
|
|
79
|
+
export interface Report {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
totals: ReportTotals;
|
|
83
|
+
byScript: Record<string, unknown>[];
|
|
84
|
+
dropOff: Record<string, unknown>[];
|
|
85
|
+
unmatched: UnmatchedQuestion[];
|
|
86
|
+
}
|
|
87
|
+
export interface WebhookEvent {
|
|
88
|
+
type: 'ticket.created';
|
|
89
|
+
data: Ticket;
|
|
90
|
+
}
|
|
91
|
+
export interface ListTicketsParams {
|
|
92
|
+
status?: TicketStatus;
|
|
93
|
+
/** 1 to 200, default 50 */
|
|
94
|
+
limit?: number;
|
|
95
|
+
/** Return tickets with a number lower than this one (paging). */
|
|
96
|
+
before?: number;
|
|
97
|
+
}
|
|
98
|
+
export interface ReportParams {
|
|
99
|
+
from?: Date | string;
|
|
100
|
+
to?: Date | string;
|
|
101
|
+
}
|
|
102
|
+
export interface UpdateSettingsParams {
|
|
103
|
+
audience?: Audience;
|
|
104
|
+
testerPhones?: string[];
|
|
105
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebhookEvent } from './types.js';
|
|
2
|
+
export interface VerifyWebhookOptions {
|
|
3
|
+
/** Maximum age (and clock skew) of the signature, in seconds. Default 300. */
|
|
4
|
+
toleranceSeconds?: number;
|
|
5
|
+
/** Current time as unix seconds. Defaults to now; useful in tests. */
|
|
6
|
+
now?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const SIGNATURE_HEADER = "Kavchat-Signature";
|
|
9
|
+
/**
|
|
10
|
+
* Checks the `Kavchat-Signature` header of a webhook request and returns the parsed event.
|
|
11
|
+
* `rawBody` must be the exact bytes received, before any JSON parsing.
|
|
12
|
+
* Throws WebhookVerificationError when the request is not authentic or too old.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyWebhook(secretKey: string, rawBody: string | Uint8Array, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): WebhookEvent;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SIGNATURE_HEADER = void 0;
|
|
4
|
+
exports.verifyWebhook = verifyWebhook;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
exports.SIGNATURE_HEADER = 'Kavchat-Signature';
|
|
8
|
+
/**
|
|
9
|
+
* Checks the `Kavchat-Signature` header of a webhook request and returns the parsed event.
|
|
10
|
+
* `rawBody` must be the exact bytes received, before any JSON parsing.
|
|
11
|
+
* Throws WebhookVerificationError when the request is not authentic or too old.
|
|
12
|
+
*/
|
|
13
|
+
function verifyWebhook(secretKey, rawBody, signatureHeader, options = {}) {
|
|
14
|
+
const tolerance = options.toleranceSeconds ?? 300;
|
|
15
|
+
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
16
|
+
let timestamp;
|
|
17
|
+
const signatures = [];
|
|
18
|
+
for (const part of String(signatureHeader ?? '').split(',')) {
|
|
19
|
+
const eq = part.indexOf('=');
|
|
20
|
+
if (eq < 0)
|
|
21
|
+
continue;
|
|
22
|
+
const key = part.slice(0, eq).trim();
|
|
23
|
+
const value = part.slice(eq + 1).trim();
|
|
24
|
+
if (key === 't')
|
|
25
|
+
timestamp = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
26
|
+
else if (key === 'v1' && value)
|
|
27
|
+
signatures.push(value);
|
|
28
|
+
}
|
|
29
|
+
if (timestamp === undefined || Number.isNaN(timestamp) || signatures.length === 0) {
|
|
30
|
+
throw new errors_js_1.WebhookVerificationError('malformed_header', 'Kavchat-Signature header is missing or malformed');
|
|
31
|
+
}
|
|
32
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
33
|
+
throw new errors_js_1.WebhookVerificationError('timestamp_out_of_range', 'Webhook timestamp is outside the tolerance');
|
|
34
|
+
}
|
|
35
|
+
const body = typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf8') : Buffer.from(rawBody);
|
|
36
|
+
const expected = Buffer.from((0, node_crypto_1.createHmac)('sha256', secretKey).update(`${timestamp}.`).update(body).digest('hex'), 'utf8');
|
|
37
|
+
const matches = signatures.some((sig) => {
|
|
38
|
+
const given = Buffer.from(sig, 'utf8');
|
|
39
|
+
return given.length === expected.length && (0, node_crypto_1.timingSafeEqual)(given, expected);
|
|
40
|
+
});
|
|
41
|
+
if (!matches)
|
|
42
|
+
throw new errors_js_1.WebhookVerificationError('invalid_signature', 'Webhook signature does not match');
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(body.toString('utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new errors_js_1.WebhookVerificationError('invalid_payload', 'Webhook body is not valid JSON');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type UserTokenOptions } from './identity.js';
|
|
2
|
+
import { type VerifyWebhookOptions } from './webhook.js';
|
|
3
|
+
import type { ListTicketsParams, ReleaseSummary, Report, ReportParams, Settings, Ticket, TicketStatus, UpdateSettingsParams, WebhookEvent, Workspace } from './types.js';
|
|
4
|
+
export declare const DEFAULT_BASE_URL = "https://api.kavchat.com";
|
|
5
|
+
export declare const VERSION = "0.0.1";
|
|
6
|
+
export interface KavchatOptions {
|
|
7
|
+
/** Default https://api.kavchat.com */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Per request, in milliseconds. Default 30000. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** Language of error messages (Accept-Language), e.g. "en" or "he". */
|
|
12
|
+
locale?: string;
|
|
13
|
+
/** Custom fetch implementation. Defaults to the global fetch. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Kavchat server client. Keep the secret key on your backend only.
|
|
18
|
+
*
|
|
19
|
+
* const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);
|
|
20
|
+
*/
|
|
21
|
+
export declare class Kavchat {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(secretKey: string, options?: KavchatOptions);
|
|
24
|
+
/** Signs an identity token for one of your users. See createUserToken(). */
|
|
25
|
+
createUserToken(options: UserTokenOptions): string;
|
|
26
|
+
/** Verifies a webhook request and returns the event. See verifyWebhook(). */
|
|
27
|
+
verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): WebhookEvent;
|
|
28
|
+
getWorkspace(): Promise<Workspace>;
|
|
29
|
+
/** Newest first. Page with `before` = the number of the last ticket you have. */
|
|
30
|
+
listTickets(params?: ListTicketsParams): Promise<Ticket[]>;
|
|
31
|
+
getTicket(id: string): Promise<Ticket>;
|
|
32
|
+
setTicketStatus(id: string, status: TicketStatus): Promise<Ticket>;
|
|
33
|
+
/** Defaults to the last 30 days. */
|
|
34
|
+
getReports(params?: ReportParams): Promise<Report>;
|
|
35
|
+
getSettings(): Promise<Settings>;
|
|
36
|
+
/** Only the audience and the tester phones can be changed from the server. */
|
|
37
|
+
updateSettings(params: UpdateSettingsParams): Promise<Settings>;
|
|
38
|
+
listReleases(): Promise<ReleaseSummary[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { KavchatApiError } from './errors.js';
|
|
2
|
+
import { createUserToken } from './identity.js';
|
|
3
|
+
import { verifyWebhook } from './webhook.js';
|
|
4
|
+
export const DEFAULT_BASE_URL = 'https://api.kavchat.com';
|
|
5
|
+
export const VERSION = '0.0.1';
|
|
6
|
+
/**
|
|
7
|
+
* Kavchat server client. Keep the secret key on your backend only.
|
|
8
|
+
*
|
|
9
|
+
* const kavchat = new Kavchat(process.env.KAVCHAT_SECRET_KEY!);
|
|
10
|
+
*/
|
|
11
|
+
export class Kavchat {
|
|
12
|
+
#secretKey;
|
|
13
|
+
#baseUrl;
|
|
14
|
+
#timeoutMs;
|
|
15
|
+
#locale;
|
|
16
|
+
#fetch;
|
|
17
|
+
constructor(secretKey, options = {}) {
|
|
18
|
+
if (typeof secretKey !== 'string' || !secretKey)
|
|
19
|
+
throw new TypeError('secretKey is required');
|
|
20
|
+
this.#secretKey = secretKey;
|
|
21
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
22
|
+
this.#timeoutMs = options.timeoutMs ?? 30_000;
|
|
23
|
+
this.#locale = options.locale;
|
|
24
|
+
const impl = options.fetch ?? globalThis.fetch;
|
|
25
|
+
if (typeof impl !== 'function')
|
|
26
|
+
throw new TypeError('No fetch implementation found (Node 18+ required)');
|
|
27
|
+
this.#fetch = impl;
|
|
28
|
+
}
|
|
29
|
+
/** Signs an identity token for one of your users. See createUserToken(). */
|
|
30
|
+
createUserToken(options) {
|
|
31
|
+
return createUserToken(this.#secretKey, options);
|
|
32
|
+
}
|
|
33
|
+
/** Verifies a webhook request and returns the event. See verifyWebhook(). */
|
|
34
|
+
verifyWebhook(rawBody, signatureHeader, options) {
|
|
35
|
+
return verifyWebhook(this.#secretKey, rawBody, signatureHeader, options);
|
|
36
|
+
}
|
|
37
|
+
getWorkspace() {
|
|
38
|
+
return this.#request('GET', '/workspace');
|
|
39
|
+
}
|
|
40
|
+
/** Newest first. Page with `before` = the number of the last ticket you have. */
|
|
41
|
+
listTickets(params = {}) {
|
|
42
|
+
return this.#request('GET', '/tickets', { query: { ...params } });
|
|
43
|
+
}
|
|
44
|
+
getTicket(id) {
|
|
45
|
+
return this.#request('GET', `/tickets/${encodeURIComponent(id)}`);
|
|
46
|
+
}
|
|
47
|
+
setTicketStatus(id, status) {
|
|
48
|
+
return this.#request('PUT', `/tickets/${encodeURIComponent(id)}/status`, { body: { status } });
|
|
49
|
+
}
|
|
50
|
+
/** Defaults to the last 30 days. */
|
|
51
|
+
getReports(params = {}) {
|
|
52
|
+
const iso = (d) => (d instanceof Date ? d.toISOString() : d);
|
|
53
|
+
return this.#request('GET', '/reports', { query: { from: iso(params.from), to: iso(params.to) } });
|
|
54
|
+
}
|
|
55
|
+
getSettings() {
|
|
56
|
+
return this.#request('GET', '/settings');
|
|
57
|
+
}
|
|
58
|
+
/** Only the audience and the tester phones can be changed from the server. */
|
|
59
|
+
updateSettings(params) {
|
|
60
|
+
return this.#request('PUT', '/settings', { body: params });
|
|
61
|
+
}
|
|
62
|
+
listReleases() {
|
|
63
|
+
return this.#request('GET', '/releases');
|
|
64
|
+
}
|
|
65
|
+
async #request(method, path, opts = {}) {
|
|
66
|
+
const url = new URL(`${this.#baseUrl}/v1/server${path}`);
|
|
67
|
+
for (const [key, value] of Object.entries(opts.query ?? {})) {
|
|
68
|
+
if (value !== undefined && value !== null && value !== '')
|
|
69
|
+
url.searchParams.set(key, String(value));
|
|
70
|
+
}
|
|
71
|
+
const headers = {
|
|
72
|
+
Authorization: `Bearer ${this.#secretKey}`,
|
|
73
|
+
Accept: 'application/json',
|
|
74
|
+
'User-Agent': `kavchat-node/${VERSION}`,
|
|
75
|
+
};
|
|
76
|
+
if (this.#locale)
|
|
77
|
+
headers['Accept-Language'] = this.#locale;
|
|
78
|
+
if (opts.body !== undefined)
|
|
79
|
+
headers['Content-Type'] = 'application/json';
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await this.#fetch(url, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
86
|
+
signal: AbortSignal.timeout(this.#timeoutMs),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const name = err?.name;
|
|
91
|
+
if (name === 'TimeoutError' || name === 'AbortError') {
|
|
92
|
+
throw new KavchatApiError(0, 'timeout', `Request timed out after ${this.#timeoutMs} ms`);
|
|
93
|
+
}
|
|
94
|
+
throw new KavchatApiError(0, 'network_error', `Could not reach Kavchat: ${err?.message ?? err}`);
|
|
95
|
+
}
|
|
96
|
+
const text = await res.text();
|
|
97
|
+
let json;
|
|
98
|
+
try {
|
|
99
|
+
json = text ? JSON.parse(text) : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
json = undefined;
|
|
103
|
+
}
|
|
104
|
+
if (!res.ok || !json || json.success !== true) {
|
|
105
|
+
if (json && typeof json.code === 'string') {
|
|
106
|
+
throw new KavchatApiError(res.status, json.code, String(json.message ?? json.code), json.details);
|
|
107
|
+
}
|
|
108
|
+
throw new KavchatApiError(res.status, 'invalid_response', `Unexpected response from Kavchat (HTTP ${res.status})`);
|
|
109
|
+
}
|
|
110
|
+
return json.data;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Base class of every error thrown by this SDK. */
|
|
2
|
+
export declare class KavchatError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The Server API answered with an error, or could not be reached.
|
|
7
|
+
* `status` is the HTTP status, or 0 when no response arrived (`code` is then
|
|
8
|
+
* `timeout` or `network_error`).
|
|
9
|
+
*/
|
|
10
|
+
export declare class KavchatApiError extends KavchatError {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly details?: unknown;
|
|
14
|
+
constructor(status: number, code: string, message: string, details?: unknown);
|
|
15
|
+
}
|
|
16
|
+
export type WebhookErrorCode = 'malformed_header' | 'timestamp_out_of_range' | 'invalid_signature' | 'invalid_payload';
|
|
17
|
+
/** A webhook request failed verification. Answer it with HTTP 400. */
|
|
18
|
+
export declare class WebhookVerificationError extends KavchatError {
|
|
19
|
+
readonly code: WebhookErrorCode;
|
|
20
|
+
constructor(code: WebhookErrorCode, message: string);
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Base class of every error thrown by this SDK. */
|
|
2
|
+
export class KavchatError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The Server API answered with an error, or could not be reached.
|
|
10
|
+
* `status` is the HTTP status, or 0 when no response arrived (`code` is then
|
|
11
|
+
* `timeout` or `network_error`).
|
|
12
|
+
*/
|
|
13
|
+
export class KavchatApiError extends KavchatError {
|
|
14
|
+
status;
|
|
15
|
+
code;
|
|
16
|
+
details;
|
|
17
|
+
constructor(status, code, message, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** A webhook request failed verification. Answer it with HTTP 400. */
|
|
25
|
+
export class WebhookVerificationError extends KavchatError {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface UserTokenOptions {
|
|
2
|
+
/** Your own user id. Becomes the `sub` claim. 1 to 128 characters. */
|
|
3
|
+
userId: string;
|
|
4
|
+
phone?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
/** Lifetime in seconds. Default 7 days, at most 30 days. */
|
|
8
|
+
expiresIn?: number;
|
|
9
|
+
/** Issue time as unix seconds. Defaults to now; useful in tests. */
|
|
10
|
+
issuedAt?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_TOKEN_TTL_SECONDS: number;
|
|
13
|
+
export declare const MAX_TOKEN_TTL_SECONDS: number;
|
|
14
|
+
/**
|
|
15
|
+
* Signs an identity token (HS256 JWT) for one of your users.
|
|
16
|
+
* Pass it to the widget or the mobile SDK; Kavchat then tags sessions and
|
|
17
|
+
* tickets with `userId` and can show the bot only to testers.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createUserToken(secretKey: string, options: UserTokenOptions): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
export const DEFAULT_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
3
|
+
export const MAX_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
4
|
+
/** base64url of {"alg":"HS256","typ":"JWT"} */
|
|
5
|
+
const HEADER = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
6
|
+
/**
|
|
7
|
+
* Signs an identity token (HS256 JWT) for one of your users.
|
|
8
|
+
* Pass it to the widget or the mobile SDK; Kavchat then tags sessions and
|
|
9
|
+
* tickets with `userId` and can show the bot only to testers.
|
|
10
|
+
*/
|
|
11
|
+
export function createUserToken(secretKey, options) {
|
|
12
|
+
if (typeof secretKey !== 'string' || !secretKey)
|
|
13
|
+
throw new TypeError('secretKey is required');
|
|
14
|
+
const { userId, phone, name, email } = options;
|
|
15
|
+
if (typeof userId !== 'string' || userId.length === 0 || userId.length > 128) {
|
|
16
|
+
throw new RangeError('userId must be a string of 1 to 128 characters');
|
|
17
|
+
}
|
|
18
|
+
const expiresIn = options.expiresIn ?? DEFAULT_TOKEN_TTL_SECONDS;
|
|
19
|
+
if (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > MAX_TOKEN_TTL_SECONDS) {
|
|
20
|
+
throw new RangeError(`expiresIn must be a whole number of seconds between 1 and ${MAX_TOKEN_TTL_SECONDS}`);
|
|
21
|
+
}
|
|
22
|
+
const iat = options.issuedAt ?? Math.floor(Date.now() / 1000);
|
|
23
|
+
// Claim order is fixed (sub, phone, name, email, iat, exp) so every SDK signs the same bytes.
|
|
24
|
+
const claims = { sub: userId };
|
|
25
|
+
if (phone)
|
|
26
|
+
claims.phone = phone;
|
|
27
|
+
if (name)
|
|
28
|
+
claims.name = name;
|
|
29
|
+
if (email)
|
|
30
|
+
claims.email = email;
|
|
31
|
+
claims.iat = iat;
|
|
32
|
+
claims.exp = iat + expiresIn;
|
|
33
|
+
const payload = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
|
|
34
|
+
const signature = createHmac('sha256', secretKey).update(`${HEADER}.${payload}`).digest('base64url');
|
|
35
|
+
return `${HEADER}.${payload}.${signature}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Kavchat, DEFAULT_BASE_URL, VERSION, type KavchatOptions } from './client.js';
|
|
2
|
+
export { createUserToken, DEFAULT_TOKEN_TTL_SECONDS, MAX_TOKEN_TTL_SECONDS, type UserTokenOptions, } from './identity.js';
|
|
3
|
+
export { verifyWebhook, SIGNATURE_HEADER, type VerifyWebhookOptions } from './webhook.js';
|
|
4
|
+
export { KavchatError, KavchatApiError, WebhookVerificationError, type WebhookErrorCode } from './errors.js';
|
|
5
|
+
export type * from './types.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Kavchat, DEFAULT_BASE_URL, VERSION } from './client.js';
|
|
2
|
+
export { createUserToken, DEFAULT_TOKEN_TTL_SECONDS, MAX_TOKEN_TTL_SECONDS, } from './identity.js';
|
|
3
|
+
export { verifyWebhook, SIGNATURE_HEADER } from './webhook.js';
|
|
4
|
+
export { KavchatError, KavchatApiError, WebhookVerificationError } from './errors.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type TicketStatus = 'open' | 'closed';
|
|
2
|
+
export type Audience = 'off' | 'testers' | 'all';
|
|
3
|
+
export type Locale = 'he' | 'en' | 'ar' | 'ru' | 'es' | 'fr' | 'de' | 'pt' | 'it' | 'tr';
|
|
4
|
+
export interface TranscriptEntry {
|
|
5
|
+
from: 'bot' | 'user';
|
|
6
|
+
text: string;
|
|
7
|
+
stepId?: string;
|
|
8
|
+
at?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Ticket {
|
|
11
|
+
id: string;
|
|
12
|
+
number: number;
|
|
13
|
+
status: TicketStatus;
|
|
14
|
+
name: string | null;
|
|
15
|
+
email: string | null;
|
|
16
|
+
/** E.164 */
|
|
17
|
+
phone: string | null;
|
|
18
|
+
/** The `sub` claim of the identity token, when the user was identified. */
|
|
19
|
+
userId: string | null;
|
|
20
|
+
topicId: string | null;
|
|
21
|
+
topicName: string | null;
|
|
22
|
+
scriptId: string | null;
|
|
23
|
+
sessionId: string | null;
|
|
24
|
+
message: string;
|
|
25
|
+
transcript: TranscriptEntry[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ContactTopic {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Settings {
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
audience: Audience;
|
|
36
|
+
testerPhones: string[];
|
|
37
|
+
botName: string;
|
|
38
|
+
avatarText: string;
|
|
39
|
+
avatarImageUrl: string | null;
|
|
40
|
+
subtitle: string;
|
|
41
|
+
primaryColor: string;
|
|
42
|
+
position: 'start' | 'end';
|
|
43
|
+
contactTopics: ContactTopic[];
|
|
44
|
+
}
|
|
45
|
+
export interface Workspace {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
locale: Locale;
|
|
49
|
+
defaultCountry: string;
|
|
50
|
+
plan: string;
|
|
51
|
+
publicKey: string;
|
|
52
|
+
secretKeyHint: string;
|
|
53
|
+
webhookUrl: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface ReleaseSummary {
|
|
56
|
+
version: number;
|
|
57
|
+
note?: string;
|
|
58
|
+
restoredFrom?: number;
|
|
59
|
+
publishedAt: string;
|
|
60
|
+
scripts: {
|
|
61
|
+
id: string;
|
|
62
|
+
kind: string;
|
|
63
|
+
name: string;
|
|
64
|
+
}[];
|
|
65
|
+
}
|
|
66
|
+
export interface ReportTotals {
|
|
67
|
+
sessions: number;
|
|
68
|
+
open: number;
|
|
69
|
+
answered: number;
|
|
70
|
+
contact: number;
|
|
71
|
+
abandoned: number;
|
|
72
|
+
}
|
|
73
|
+
export interface UnmatchedQuestion {
|
|
74
|
+
groupKey: string;
|
|
75
|
+
sample: string;
|
|
76
|
+
count: number;
|
|
77
|
+
lastAt: string;
|
|
78
|
+
}
|
|
79
|
+
export interface Report {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
totals: ReportTotals;
|
|
83
|
+
byScript: Record<string, unknown>[];
|
|
84
|
+
dropOff: Record<string, unknown>[];
|
|
85
|
+
unmatched: UnmatchedQuestion[];
|
|
86
|
+
}
|
|
87
|
+
export interface WebhookEvent {
|
|
88
|
+
type: 'ticket.created';
|
|
89
|
+
data: Ticket;
|
|
90
|
+
}
|
|
91
|
+
export interface ListTicketsParams {
|
|
92
|
+
status?: TicketStatus;
|
|
93
|
+
/** 1 to 200, default 50 */
|
|
94
|
+
limit?: number;
|
|
95
|
+
/** Return tickets with a number lower than this one (paging). */
|
|
96
|
+
before?: number;
|
|
97
|
+
}
|
|
98
|
+
export interface ReportParams {
|
|
99
|
+
from?: Date | string;
|
|
100
|
+
to?: Date | string;
|
|
101
|
+
}
|
|
102
|
+
export interface UpdateSettingsParams {
|
|
103
|
+
audience?: Audience;
|
|
104
|
+
testerPhones?: string[];
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WebhookEvent } from './types.js';
|
|
2
|
+
export interface VerifyWebhookOptions {
|
|
3
|
+
/** Maximum age (and clock skew) of the signature, in seconds. Default 300. */
|
|
4
|
+
toleranceSeconds?: number;
|
|
5
|
+
/** Current time as unix seconds. Defaults to now; useful in tests. */
|
|
6
|
+
now?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const SIGNATURE_HEADER = "Kavchat-Signature";
|
|
9
|
+
/**
|
|
10
|
+
* Checks the `Kavchat-Signature` header of a webhook request and returns the parsed event.
|
|
11
|
+
* `rawBody` must be the exact bytes received, before any JSON parsing.
|
|
12
|
+
* Throws WebhookVerificationError when the request is not authentic or too old.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyWebhook(secretKey: string, rawBody: string | Uint8Array, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): WebhookEvent;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { WebhookVerificationError } from './errors.js';
|
|
3
|
+
export const SIGNATURE_HEADER = 'Kavchat-Signature';
|
|
4
|
+
/**
|
|
5
|
+
* Checks the `Kavchat-Signature` header of a webhook request and returns the parsed event.
|
|
6
|
+
* `rawBody` must be the exact bytes received, before any JSON parsing.
|
|
7
|
+
* Throws WebhookVerificationError when the request is not authentic or too old.
|
|
8
|
+
*/
|
|
9
|
+
export function verifyWebhook(secretKey, rawBody, signatureHeader, options = {}) {
|
|
10
|
+
const tolerance = options.toleranceSeconds ?? 300;
|
|
11
|
+
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
12
|
+
let timestamp;
|
|
13
|
+
const signatures = [];
|
|
14
|
+
for (const part of String(signatureHeader ?? '').split(',')) {
|
|
15
|
+
const eq = part.indexOf('=');
|
|
16
|
+
if (eq < 0)
|
|
17
|
+
continue;
|
|
18
|
+
const key = part.slice(0, eq).trim();
|
|
19
|
+
const value = part.slice(eq + 1).trim();
|
|
20
|
+
if (key === 't')
|
|
21
|
+
timestamp = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
22
|
+
else if (key === 'v1' && value)
|
|
23
|
+
signatures.push(value);
|
|
24
|
+
}
|
|
25
|
+
if (timestamp === undefined || Number.isNaN(timestamp) || signatures.length === 0) {
|
|
26
|
+
throw new WebhookVerificationError('malformed_header', 'Kavchat-Signature header is missing or malformed');
|
|
27
|
+
}
|
|
28
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
29
|
+
throw new WebhookVerificationError('timestamp_out_of_range', 'Webhook timestamp is outside the tolerance');
|
|
30
|
+
}
|
|
31
|
+
const body = typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf8') : Buffer.from(rawBody);
|
|
32
|
+
const expected = Buffer.from(createHmac('sha256', secretKey).update(`${timestamp}.`).update(body).digest('hex'), 'utf8');
|
|
33
|
+
const matches = signatures.some((sig) => {
|
|
34
|
+
const given = Buffer.from(sig, 'utf8');
|
|
35
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
36
|
+
});
|
|
37
|
+
if (!matches)
|
|
38
|
+
throw new WebhookVerificationError('invalid_signature', 'Webhook signature does not match');
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(body.toString('utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new WebhookVerificationError('invalid_payload', 'Webhook body is not valid JSON');
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kavchat/node",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Kavchat server SDK for Node.js: identity tokens, webhook verification and the Server API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"module": "./dist/esm/index.js",
|
|
9
|
+
"types": "./dist/esm/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/esm/index.d.ts",
|
|
14
|
+
"default": "./dist/esm/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/cjs/index.d.ts",
|
|
18
|
+
"default": "./dist/cjs/index.js"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/write-cjs-package.mjs",
|
|
32
|
+
"test": "yarn build && node --test 'test/*.test.mjs' 'test/*.test.cjs'",
|
|
33
|
+
"prepublishOnly": "yarn build"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"chatbot",
|
|
37
|
+
"customer-support",
|
|
38
|
+
"jwt",
|
|
39
|
+
"kavchat",
|
|
40
|
+
"rtl",
|
|
41
|
+
"scripted-bot",
|
|
42
|
+
"support",
|
|
43
|
+
"support-chat",
|
|
44
|
+
"webhook"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.10.0",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://kavchat.com/docs",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"email": "support@kavchat.com"
|
|
53
|
+
},
|
|
54
|
+
"author": "maxsave <support@kavchat.com>",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"registry": "https://registry.npmjs.org/"
|
|
58
|
+
}
|
|
59
|
+
}
|