@ixblix/sdk-js 0.2.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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/client.d.ts +132 -0
- package/dist/client.js +251 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto.d.ts +92 -0
- package/dist/crypto.js +195 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/keypair.d.ts +8 -0
- package/dist/keypair.js +44 -0
- package/dist/keypair.js.map +1 -0
- package/dist/types.d.ts +300 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/webhooks.d.ts +35 -0
- package/dist/webhooks.js +78 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ixblix Tecnologia Ltda
|
|
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
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# ixblix SDK
|
|
2
|
+
|
|
3
|
+
Official TypeScript SDK for integrating desk/CRM systems with **ixblix**.
|
|
4
|
+
ixblix lets customer-service platforms
|
|
5
|
+
transfer conversations from original channels (WhatsApp, Instagram, etc.) to a
|
|
6
|
+
white-labeled web/mobile experience — with **end-to-end encryption** so the ixblix
|
|
7
|
+
backend never sees message content.
|
|
8
|
+
|
|
9
|
+
This SDK is the **operator (desk/CRM) side** of the integration. It provides:
|
|
10
|
+
|
|
11
|
+
- A typed HTTP client for the ixblix REST API (conversations, messages, media,
|
|
12
|
+
keys, company onboarding, credits, webhook registration).
|
|
13
|
+
- End-to-end encryption helpers (RSA-OAEP + AES-256-GCM) so you can encrypt
|
|
14
|
+
messages to your customers and decrypt theirs.
|
|
15
|
+
- Webhook parsing/verification helpers (HMAC-SHA256) so you can receive
|
|
16
|
+
incoming messages and conversation events in real time without polling.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @ixblix/sdk-js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js >= 18.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import {
|
|
30
|
+
IxblixClient,
|
|
31
|
+
generateOperatorKeyPair,
|
|
32
|
+
loadOrCreateOperatorKey,
|
|
33
|
+
encryptToRecipient,
|
|
34
|
+
decryptEnvelope,
|
|
35
|
+
} from "@ixblix/sdk-js";
|
|
36
|
+
|
|
37
|
+
// 1. Configure the client with your company API key.
|
|
38
|
+
const ixblix = new IxblixClient({
|
|
39
|
+
baseUrl: "https://api.ixblix.app",
|
|
40
|
+
apiKey: process.env.IXBLIX_API_KEY,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Hold a persistent RSA keypair. The private key never leaves your side.
|
|
44
|
+
// Persist it once (e.g. loadOrCreateOperatorKey("./.keys")) and reuse it.
|
|
45
|
+
const operatorKey = generateOperatorKeyPair();
|
|
46
|
+
|
|
47
|
+
// 3. Create an overflow conversation for a contact.
|
|
48
|
+
const { conversation, deeplink } = await ixblix.createConversation({
|
|
49
|
+
contact: {
|
|
50
|
+
externalId: "whatsapp_5511999999999",
|
|
51
|
+
name: "John Doe",
|
|
52
|
+
phone: "+55 11 99999-9999",
|
|
53
|
+
},
|
|
54
|
+
channel: "whatsapp",
|
|
55
|
+
operatorPublicKey: operatorKey.publicKeySpki,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 4. Share `deeplink` with the contact. When they open it, their browser
|
|
59
|
+
// generates a keypair and registers its public key with ixblix.
|
|
60
|
+
|
|
61
|
+
// 5. Wait for the customer to join. Prefer a webhook (`CUSTOMER_JOINED`) so
|
|
62
|
+
// you don't poll. If you must poll, use getConversationKeys:
|
|
63
|
+
let keys = await ixblix.getConversationKeys(conversation.id);
|
|
64
|
+
while (keys.keyStatus !== "ACTIVE" || !keys.customerPublicKey) {
|
|
65
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
66
|
+
keys = await ixblix.getConversationKeys(conversation.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Encrypt and send a message to the customer.
|
|
70
|
+
const envelope = encryptToRecipient(
|
|
71
|
+
"Hello! How can we help?",
|
|
72
|
+
keys.customerPublicKey,
|
|
73
|
+
operatorKey.keyId,
|
|
74
|
+
operatorKey.publicKeySpki,
|
|
75
|
+
);
|
|
76
|
+
await ixblix.sendCompanyMessage(conversation.id, envelope);
|
|
77
|
+
|
|
78
|
+
// 7. Read incoming messages (decrypt with your private key). Prefer receiving
|
|
79
|
+
// a `MESSAGE_RECEIVED` webhook and fetching the message, rather than
|
|
80
|
+
// polling listMessages.
|
|
81
|
+
const messages = await ixblix.listMessages(conversation.id);
|
|
82
|
+
for (const message of messages) {
|
|
83
|
+
if (!message.contentEncrypted) continue;
|
|
84
|
+
const plaintext = decryptEnvelope(message, operatorKey.privateKey);
|
|
85
|
+
console.log(plaintext);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Company onboarding
|
|
90
|
+
|
|
91
|
+
Register a company (keyless), then activate it after payment to obtain the API
|
|
92
|
+
key:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const { company, payment } = await ixblix.registerCompany({
|
|
96
|
+
name: "Acme CRM",
|
|
97
|
+
slug: "acme-crm",
|
|
98
|
+
paymentProvider: "dummy",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// After the payment is confirmed:
|
|
102
|
+
const { apiKey } = await ixblix.activateCompany(
|
|
103
|
+
company.id,
|
|
104
|
+
payment.transactionId,
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## End-to-end encryption
|
|
109
|
+
|
|
110
|
+
ixblix uses a hybrid scheme:
|
|
111
|
+
|
|
112
|
+
- **RSA-OAEP** (2048-bit, SHA-256) to wrap a fresh per-message **AES-256-GCM**
|
|
113
|
+
key.
|
|
114
|
+
- Each message's AES key is wrapped **twice**: to the recipient
|
|
115
|
+
(`encryptedKey`) and to the sender's own public key (`selfEncryptedKey`), so
|
|
116
|
+
each side can read back its own sent messages.
|
|
117
|
+
|
|
118
|
+
The SDK exposes:
|
|
119
|
+
|
|
120
|
+
| Function | Purpose |
|
|
121
|
+
| ---------------------------------------------------------------------- | ------------------------------------------- |
|
|
122
|
+
| `generateOperatorKeyPair()` | Generate a fresh RSA keypair. |
|
|
123
|
+
| `loadOrCreateOperatorKey(dir)` | Load-or-create a persisted keypair on disk. |
|
|
124
|
+
| `encryptToRecipient(text, recipientPub, senderKeyId, senderPub)` | Encrypt an outgoing text message. |
|
|
125
|
+
| `decryptEnvelope(message, privateKey)` | Decrypt an incoming (or your own) message. |
|
|
126
|
+
| `encryptMediaToRecipient(bytes, recipientPub, senderKeyId, senderPub)` | Encrypt an outgoing media file. |
|
|
127
|
+
| `decryptMediaEnvelope(media, privateKey)` | Decrypt a media file. |
|
|
128
|
+
|
|
129
|
+
> **Security:** never send your private key to ixblix. Only the public key
|
|
130
|
+
> (`publicKeySpki`) is registered. Store the private key securely (the
|
|
131
|
+
> `loadOrCreateOperatorKey` helper writes it with mode `0600`).
|
|
132
|
+
|
|
133
|
+
## Webhooks
|
|
134
|
+
|
|
135
|
+
ixblix pushes events to your webhook URL so you can receive incoming messages and
|
|
136
|
+
conversation events in real time **without polling**. Configure your endpoint
|
|
137
|
+
and verify incoming payloads:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { IxblixClient, verifyWebhook, WEBHOOK_SIGNATURE_HEADER } from "@ixblix/sdk-js";
|
|
141
|
+
|
|
142
|
+
// 1. Register your webhook URL. The returned secret is shown only once.
|
|
143
|
+
const ixblix = new IxblixClient({
|
|
144
|
+
baseUrl: "https://api.ixblix.app",
|
|
145
|
+
apiKey: process.env.IXBLIX_API_KEY,
|
|
146
|
+
});
|
|
147
|
+
const { webhookSecret } = await ixblix.updateWebhook({
|
|
148
|
+
webhookUrl: "https://desk.example.com/ixblix/webhook",
|
|
149
|
+
});
|
|
150
|
+
// Store webhookSecret securely (e.g. process.env.IXBLIX_WEBHOOK_SECRET).
|
|
151
|
+
|
|
152
|
+
// 2. In your Express webhook handler, verify the HMAC signature:
|
|
153
|
+
app.post("/ixblix/webhook", (req, res) => {
|
|
154
|
+
const signature = req.header(WEBHOOK_SIGNATURE_HEADER);
|
|
155
|
+
const { event } = verifyWebhook(
|
|
156
|
+
req.body,
|
|
157
|
+
process.env.IXBLIX_WEBHOOK_SECRET,
|
|
158
|
+
signature,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (event.event === "MESSAGE_RECEIVED") {
|
|
162
|
+
// A contact sent a message. Fetch and decrypt it:
|
|
163
|
+
// const messages = await ixblix.listMessages(event.conversationId);
|
|
164
|
+
// const message = messages.find((m) => m.id === event.messageId);
|
|
165
|
+
// const plaintext = decryptEnvelope(message, operatorKey.privateKey);
|
|
166
|
+
} else if (event.event === "CUSTOMER_JOINED") {
|
|
167
|
+
// The customer registered its public key; you can now encrypt to them.
|
|
168
|
+
}
|
|
169
|
+
res.status(200).end();
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Webhook payloads are signed with HMAC-SHA256 using your webhook secret. Each
|
|
174
|
+
delivery also carries `X-Ixblix-Event-Id` (deduplicate on it — delivery is
|
|
175
|
+
at-least-once) and `X-Ixblix-Event`. See
|
|
176
|
+
[`docs/integrator/05-webhooks.md`](../docs/integrator/05-webhooks.md) for the
|
|
177
|
+
full event reference.
|
|
178
|
+
|
|
179
|
+
## Read receipts and presence
|
|
180
|
+
|
|
181
|
+
When your agents have displayed a customer-sent message, mark it as read so the
|
|
182
|
+
customer's chat app shows a read receipt:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await ixblix.markMessageReadByCompany(conversationId, messageId);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When the customer reads one of your messages, ixblix pushes a `MESSAGE_READ`
|
|
189
|
+
webhook to your endpoint (see the webhook handler above).
|
|
190
|
+
|
|
191
|
+
Report operator typing/recording so the customer's chat app can show an
|
|
192
|
+
indicator, or send `stopped` to clear it immediately:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
await ixblix.reportCompanyPresence(conversationId, "typing"); // or "recording"
|
|
196
|
+
await ixblix.reportCompanyPresence(conversationId, "stopped");
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ActivateCompanyResult, CompanyBalance, ContactInput, ConversationKeys, CreateConversationResult, CreditPurchase, Media, Message, MessageEnvelope, OriginalChannelMessageInput, PaymentProvidersResult, Plan, PurchaseCreditsResult, RegisterCompanyInput, RegisterCompanyResult } from "./types.js";
|
|
2
|
+
/** Options for constructing an {@link IxblixClient}. */
|
|
3
|
+
export interface IxblixClientOptions {
|
|
4
|
+
/** Base URL of the ixblix API, e.g. `https://api.ixblix.app`. */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** Company API key sent in the `X-API-Key` header. */
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/** Optional custom fetch implementation (e.g. for testing or proxies). */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
/** A media file to upload, with its plaintext bytes and metadata. */
|
|
12
|
+
export interface MediaUpload {
|
|
13
|
+
/** Plaintext file bytes. The SDK encrypts them before upload. */
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
fileName: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
}
|
|
18
|
+
/** A media file downloaded from ixblix, still encrypted. */
|
|
19
|
+
export interface MediaDownload {
|
|
20
|
+
/** Encrypted (ciphertext) bytes as stored by ixblix. */
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
media: Media;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Client for the ixblix API.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const ixblix = new IxblixClient({
|
|
29
|
+
* baseUrl: "https://api.ixblix.app",
|
|
30
|
+
* apiKey: process.env.IXBLIX_API_KEY,
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class IxblixClient {
|
|
35
|
+
private readonly baseUrl;
|
|
36
|
+
private readonly apiKey?;
|
|
37
|
+
private readonly fetchImpl;
|
|
38
|
+
constructor(options: IxblixClientOptions);
|
|
39
|
+
private buildHeaders;
|
|
40
|
+
private request;
|
|
41
|
+
/**
|
|
42
|
+
* Register a company (keyless). Returns payment instructions; call
|
|
43
|
+
* {@link activateCompany} once the payment is confirmed.
|
|
44
|
+
*/
|
|
45
|
+
registerCompany(input: RegisterCompanyInput): Promise<RegisterCompanyResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Activate a company after payment confirmation and obtain its API key.
|
|
48
|
+
*/
|
|
49
|
+
activateCompany(companyId: string, transactionId: string): Promise<ActivateCompanyResult>;
|
|
50
|
+
/** List available subscription plans. */
|
|
51
|
+
listPlans(): Promise<Plan[]>;
|
|
52
|
+
/** List available payment providers. */
|
|
53
|
+
listPaymentProviders(): Promise<PaymentProvidersResult>;
|
|
54
|
+
/** Fetch the authenticated company's balance and plan state. */
|
|
55
|
+
getBalance(): Promise<CompanyBalance>;
|
|
56
|
+
/** Purchase prepaid credits for the authenticated company. */
|
|
57
|
+
purchaseCredits(input: {
|
|
58
|
+
amountCents: number;
|
|
59
|
+
currency?: string;
|
|
60
|
+
provider: string;
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
}): Promise<PurchaseCreditsResult>;
|
|
63
|
+
/** List the authenticated company's credit purchases. */
|
|
64
|
+
listCreditPurchases(): Promise<CreditPurchase[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Set or update the webhook endpoint where ixblix delivers events (incoming
|
|
67
|
+
* messages, customer joined, conversation closed, presence metadata). When
|
|
68
|
+
* the URL changes (or `rotateSecret` is true) a fresh HMAC secret is
|
|
69
|
+
* generated and returned once — store it to verify webhook signatures.
|
|
70
|
+
*/
|
|
71
|
+
updateWebhook(input: {
|
|
72
|
+
webhookUrl?: string | null;
|
|
73
|
+
rotateSecret?: boolean;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
webhookUrl: string | null;
|
|
76
|
+
webhookSecret: string | null;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Create an overflow conversation for a contact. Supply the operator's RSA
|
|
80
|
+
* public key (base64 SPKI DER) so the customer can encrypt messages to it.
|
|
81
|
+
*/
|
|
82
|
+
createConversation(input: {
|
|
83
|
+
contact: ContactInput;
|
|
84
|
+
channel: string;
|
|
85
|
+
operatorPublicKey: string;
|
|
86
|
+
}): Promise<CreateConversationResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Fetch the current E2EE key state of a conversation so the operator can
|
|
89
|
+
* detect when the customer has joined (`keyStatus === "ACTIVE"`).
|
|
90
|
+
*/
|
|
91
|
+
getConversationKeys(conversationId: string): Promise<ConversationKeys>;
|
|
92
|
+
/**
|
|
93
|
+
* Send an encrypted message from the company to the contact. The message must
|
|
94
|
+
* already be encrypted to the customer's public key (see the crypto helpers).
|
|
95
|
+
*/
|
|
96
|
+
sendCompanyMessage(conversationId: string, envelope: MessageEnvelope, contentType?: string): Promise<Message>;
|
|
97
|
+
/** List all messages of a conversation (content is always ciphertext). */
|
|
98
|
+
listMessages(conversationId: string): Promise<Message[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Mark a contact-sent message as read by the operator. Emits a
|
|
101
|
+
* `message_read` Socket.io event to the customer's chat app so the customer
|
|
102
|
+
* sees the read receipt.
|
|
103
|
+
*/
|
|
104
|
+
markMessageReadByCompany(conversationId: string, messageId: string): Promise<{
|
|
105
|
+
messageId: string;
|
|
106
|
+
readAt: string;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Relay an operator presence event (typing, stopped typing, or recording)
|
|
110
|
+
* to the customer's chat app via Socket.io.
|
|
111
|
+
*/
|
|
112
|
+
reportCompanyPresence(conversationId: string, type: "typing" | "stopped" | "recording"): Promise<{
|
|
113
|
+
status: string;
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Relay a message received on the original channel (WhatsApp, Instagram,
|
|
117
|
+
* etc.) into ixblix. The message must be encrypted to the operator's public key.
|
|
118
|
+
*/
|
|
119
|
+
relayOriginalChannelMessage(input: OriginalChannelMessageInput): Promise<Message>;
|
|
120
|
+
/**
|
|
121
|
+
* Upload an encrypted media file from the company. `file.data` must be the
|
|
122
|
+
* ciphertext bytes produced by the media encryption helper.
|
|
123
|
+
*/
|
|
124
|
+
sendCompanyMedia(conversationId: string, file: MediaUpload, envelope: MessageEnvelope): Promise<Message>;
|
|
125
|
+
/** Fetch the metadata (envelope + file info) of a media file. */
|
|
126
|
+
getMedia(mediaId: string): Promise<Media>;
|
|
127
|
+
/**
|
|
128
|
+
* Download the raw (encrypted) bytes of a media file as the operator, along
|
|
129
|
+
* with its metadata so it can be decrypted.
|
|
130
|
+
*/
|
|
131
|
+
downloadCompanyMedia(mediaId: string): Promise<MediaDownload>;
|
|
132
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed HTTP client for the ixblix public REST API.
|
|
3
|
+
*
|
|
4
|
+
* All methods that operate on behalf of a company authenticate with the
|
|
5
|
+
* `X-API-Key` header. Company onboarding (register/activate) and public
|
|
6
|
+
* endpoints (plans, payment providers) do not require an API key.
|
|
7
|
+
*/
|
|
8
|
+
import { IxblixError } from "./errors.js";
|
|
9
|
+
/**
|
|
10
|
+
* Client for the ixblix API.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const ixblix = new IxblixClient({
|
|
14
|
+
* baseUrl: "https://api.ixblix.app",
|
|
15
|
+
* apiKey: process.env.IXBLIX_API_KEY,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class IxblixClient {
|
|
20
|
+
baseUrl;
|
|
21
|
+
apiKey;
|
|
22
|
+
fetchImpl;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
25
|
+
this.apiKey = options.apiKey;
|
|
26
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
27
|
+
}
|
|
28
|
+
buildHeaders(init) {
|
|
29
|
+
const headers = {};
|
|
30
|
+
const source = new Headers(init);
|
|
31
|
+
source.forEach((value, key) => {
|
|
32
|
+
headers[key] = value;
|
|
33
|
+
});
|
|
34
|
+
if (!headers["Content-Type"]) {
|
|
35
|
+
headers["Content-Type"] = "application/json";
|
|
36
|
+
}
|
|
37
|
+
if (this.apiKey) {
|
|
38
|
+
headers["X-API-Key"] = this.apiKey;
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
async request(path, init = {}) {
|
|
43
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
44
|
+
...init,
|
|
45
|
+
headers: this.buildHeaders(init.headers),
|
|
46
|
+
});
|
|
47
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
48
|
+
const isJson = contentType.includes("application/json");
|
|
49
|
+
const data = isJson ? (await response.json()) : null;
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const body = data;
|
|
52
|
+
throw new IxblixError(body?.error ?? `ixblix ${path} failed with status ${response.status}`, {
|
|
53
|
+
status: response.status,
|
|
54
|
+
code: body?.code,
|
|
55
|
+
details: body?.errors,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Company onboarding
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Register a company (keyless). Returns payment instructions; call
|
|
65
|
+
* {@link activateCompany} once the payment is confirmed.
|
|
66
|
+
*/
|
|
67
|
+
registerCompany(input) {
|
|
68
|
+
return this.request("/api/companies/register", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify(input),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Activate a company after payment confirmation and obtain its API key.
|
|
75
|
+
*/
|
|
76
|
+
activateCompany(companyId, transactionId) {
|
|
77
|
+
return this.request(`/api/companies/${companyId}/activate/${transactionId}`, { method: "POST" });
|
|
78
|
+
}
|
|
79
|
+
/** List available subscription plans. */
|
|
80
|
+
listPlans() {
|
|
81
|
+
return this.request("/api/plans");
|
|
82
|
+
}
|
|
83
|
+
/** List available payment providers. */
|
|
84
|
+
listPaymentProviders() {
|
|
85
|
+
return this.request("/api/payment/providers");
|
|
86
|
+
}
|
|
87
|
+
/** Fetch the authenticated company's balance and plan state. */
|
|
88
|
+
getBalance() {
|
|
89
|
+
return this.request("/api/companies/balance");
|
|
90
|
+
}
|
|
91
|
+
/** Purchase prepaid credits for the authenticated company. */
|
|
92
|
+
purchaseCredits(input) {
|
|
93
|
+
return this.request("/api/companies/credits/purchase", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify(input),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** List the authenticated company's credit purchases. */
|
|
99
|
+
listCreditPurchases() {
|
|
100
|
+
return this.request("/api/companies/credits/purchases");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Set or update the webhook endpoint where ixblix delivers events (incoming
|
|
104
|
+
* messages, customer joined, conversation closed, presence metadata). When
|
|
105
|
+
* the URL changes (or `rotateSecret` is true) a fresh HMAC secret is
|
|
106
|
+
* generated and returned once — store it to verify webhook signatures.
|
|
107
|
+
*/
|
|
108
|
+
updateWebhook(input) {
|
|
109
|
+
return this.request("/api/companies/webhook", {
|
|
110
|
+
method: "PUT",
|
|
111
|
+
body: JSON.stringify(input),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Conversations
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Create an overflow conversation for a contact. Supply the operator's RSA
|
|
119
|
+
* public key (base64 SPKI DER) so the customer can encrypt messages to it.
|
|
120
|
+
*/
|
|
121
|
+
createConversation(input) {
|
|
122
|
+
return this.request("/api/conversations", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: JSON.stringify(input),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fetch the current E2EE key state of a conversation so the operator can
|
|
129
|
+
* detect when the customer has joined (`keyStatus === "ACTIVE"`).
|
|
130
|
+
*/
|
|
131
|
+
getConversationKeys(conversationId) {
|
|
132
|
+
return this.request(`/api/conversations/${conversationId}/keys`);
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Messages
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/**
|
|
138
|
+
* Send an encrypted message from the company to the contact. The message must
|
|
139
|
+
* already be encrypted to the customer's public key (see the crypto helpers).
|
|
140
|
+
*/
|
|
141
|
+
sendCompanyMessage(conversationId, envelope, contentType = "text") {
|
|
142
|
+
return this.request("/api/messages/company", {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
conversationId,
|
|
146
|
+
content: envelope.content,
|
|
147
|
+
contentType,
|
|
148
|
+
iv: envelope.iv,
|
|
149
|
+
authTag: envelope.authTag,
|
|
150
|
+
encryptedKey: envelope.encryptedKey,
|
|
151
|
+
selfEncryptedKey: envelope.selfEncryptedKey,
|
|
152
|
+
keyId: envelope.keyId,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** List all messages of a conversation (content is always ciphertext). */
|
|
157
|
+
listMessages(conversationId) {
|
|
158
|
+
return this.request(`/api/messages/${conversationId}`);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Mark a contact-sent message as read by the operator. Emits a
|
|
162
|
+
* `message_read` Socket.io event to the customer's chat app so the customer
|
|
163
|
+
* sees the read receipt.
|
|
164
|
+
*/
|
|
165
|
+
markMessageReadByCompany(conversationId, messageId) {
|
|
166
|
+
return this.request("/api/messages/company/read", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify({ conversationId, messageId }),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Relay an operator presence event (typing, stopped typing, or recording)
|
|
173
|
+
* to the customer's chat app via Socket.io.
|
|
174
|
+
*/
|
|
175
|
+
reportCompanyPresence(conversationId, type) {
|
|
176
|
+
return this.request(`/api/conversations/${conversationId}/presence`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ type }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Relay a message received on the original channel (WhatsApp, Instagram,
|
|
183
|
+
* etc.) into ixblix. The message must be encrypted to the operator's public key.
|
|
184
|
+
*/
|
|
185
|
+
relayOriginalChannelMessage(input) {
|
|
186
|
+
return this.request("/api/webhooks/original-channel", {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: JSON.stringify(input),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Media
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Upload an encrypted media file from the company. `file.data` must be the
|
|
196
|
+
* ciphertext bytes produced by the media encryption helper.
|
|
197
|
+
*/
|
|
198
|
+
async sendCompanyMedia(conversationId, file, envelope) {
|
|
199
|
+
const form = new FormData();
|
|
200
|
+
form.append("conversationId", conversationId);
|
|
201
|
+
form.append("file", new Blob([file.data], { type: file.mimeType }), file.fileName);
|
|
202
|
+
form.append("iv", envelope.iv);
|
|
203
|
+
form.append("authTag", envelope.authTag);
|
|
204
|
+
form.append("encryptedKey", envelope.encryptedKey);
|
|
205
|
+
form.append("selfEncryptedKey", envelope.selfEncryptedKey);
|
|
206
|
+
form.append("keyId", envelope.keyId);
|
|
207
|
+
const headers = {};
|
|
208
|
+
if (this.apiKey) {
|
|
209
|
+
headers["X-API-Key"] = this.apiKey;
|
|
210
|
+
}
|
|
211
|
+
const response = await this.fetchImpl(`${this.baseUrl}/api/media/company`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers,
|
|
214
|
+
body: form,
|
|
215
|
+
});
|
|
216
|
+
const data = (await response.json());
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const body = data;
|
|
219
|
+
throw new IxblixError(body?.error ?? "ixblix media upload failed", {
|
|
220
|
+
status: response.status,
|
|
221
|
+
code: body?.code,
|
|
222
|
+
details: body?.errors,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return data;
|
|
226
|
+
}
|
|
227
|
+
/** Fetch the metadata (envelope + file info) of a media file. */
|
|
228
|
+
getMedia(mediaId) {
|
|
229
|
+
return this.request(`/api/media/${mediaId}`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Download the raw (encrypted) bytes of a media file as the operator, along
|
|
233
|
+
* with its metadata so it can be decrypted.
|
|
234
|
+
*/
|
|
235
|
+
async downloadCompanyMedia(mediaId) {
|
|
236
|
+
const media = await this.getMedia(mediaId);
|
|
237
|
+
const headers = {};
|
|
238
|
+
if (this.apiKey) {
|
|
239
|
+
headers["X-API-Key"] = this.apiKey;
|
|
240
|
+
}
|
|
241
|
+
const response = await this.fetchImpl(`${this.baseUrl}/api/media/${mediaId}/content`, { headers });
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
throw new IxblixError(`ixblix media download failed with status ${response.status}`, {
|
|
244
|
+
status: response.status,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
248
|
+
return { data: new Uint8Array(arrayBuffer), media };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|