@letsbot/sdk 0.0.0-stage → 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 +251 -2
- package/dist/index.cjs +939 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15799 -0
- package/dist/index.d.ts +15799 -0
- package/dist/index.js +915 -0
- package/dist/index.js.map +1 -0
- package/openapi/openapi.yaml +15522 -0
- package/package.json +65 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { randomUUID, createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/client.ts
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var LetsBotApiError = class _LetsBotApiError extends Error {
|
|
7
|
+
name = "LetsBotApiError";
|
|
8
|
+
/** HTTP status. Can be 200/202 for a refusal carried in the body. */
|
|
9
|
+
status;
|
|
10
|
+
/** Parsed response body (raw text when it was not JSON, `undefined` when empty). */
|
|
11
|
+
body;
|
|
12
|
+
/**
|
|
13
|
+
* Support reference. The API does not send a request-id header; the only reference it returns
|
|
14
|
+
* is `reference` on a 500 (`API-XXXXXXXXXX`) — quote it to support. `undefined` otherwise.
|
|
15
|
+
*/
|
|
16
|
+
requestId;
|
|
17
|
+
/** Machine-readable code from the body (`error_code`, `code` or `error`), when there is one. */
|
|
18
|
+
code;
|
|
19
|
+
/** The operation that failed, e.g. `sendMessage`. */
|
|
20
|
+
operationId;
|
|
21
|
+
/** Seconds from a `Retry-After` header, when the response carried one. */
|
|
22
|
+
retryAfter;
|
|
23
|
+
/** Response headers. */
|
|
24
|
+
headers;
|
|
25
|
+
constructor(init) {
|
|
26
|
+
super(_LetsBotApiError.messageFrom(init.status, init.body, init.operationId));
|
|
27
|
+
this.status = init.status;
|
|
28
|
+
this.body = init.body;
|
|
29
|
+
this.headers = init.headers;
|
|
30
|
+
this.operationId = init.operationId;
|
|
31
|
+
this.retryAfter = init.retryAfter;
|
|
32
|
+
this.code = pickString(init.body, ["error_code", "code", "error"]);
|
|
33
|
+
this.requestId = pickString(init.body, ["reference"]) ?? init.headers.get("x-request-id") ?? void 0;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True for `409 idempotency_outcome_unknown`: an earlier request with this Idempotency-Key died
|
|
37
|
+
* before its result was known, so the message may or may not have been sent. Do not resend with
|
|
38
|
+
* the same key; check the message status, or send again with a NEW key if you accept a possible
|
|
39
|
+
* duplicate.
|
|
40
|
+
*/
|
|
41
|
+
get isOutcomeUnknown() {
|
|
42
|
+
return this.status === 409 && this.code === "idempotency_outcome_unknown";
|
|
43
|
+
}
|
|
44
|
+
static messageFrom(status, body, operationId) {
|
|
45
|
+
const text = pickString(body, ["Message", "message", "data", "error"]) ?? (typeof body === "string" ? body.slice(0, 200) : "");
|
|
46
|
+
return `${operationId} failed (HTTP ${status})${text ? `: ${text}` : ""}`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var LetsBotConnectionError = class extends Error {
|
|
50
|
+
name = "LetsBotConnectionError";
|
|
51
|
+
operationId;
|
|
52
|
+
constructor(operationId, cause) {
|
|
53
|
+
super(`${operationId} failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
54
|
+
this.operationId = operationId;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function pickString(body, keys) {
|
|
58
|
+
if (!body || typeof body !== "object") return void 0;
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
const value = body[key];
|
|
61
|
+
if (typeof value === "string" && value !== "") return value;
|
|
62
|
+
}
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function isLogicalFailure(body) {
|
|
66
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
67
|
+
const record = body;
|
|
68
|
+
if ("success" in record && record.success === false) return true;
|
|
69
|
+
return "Status" in record && [false, 0, "0", "false"].includes(record.Status);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/generated/operations.ts
|
|
73
|
+
var operations = {
|
|
74
|
+
addGroupParticipant: { method: "POST", path: "/group/add/participant", params: "body", send: false },
|
|
75
|
+
archiveChat: { method: "POST", path: "/chat/archive", params: "body", send: false },
|
|
76
|
+
blockUser: { method: "POST", path: "/user/block", params: "body", send: false },
|
|
77
|
+
checkPhone: { method: "POST", path: "/check/phone", params: "body", send: false },
|
|
78
|
+
createGroup: { method: "POST", path: "/group/create", params: "body", send: false },
|
|
79
|
+
createProduct: { method: "POST", path: "/product/create", params: "body", send: false },
|
|
80
|
+
deleteMessage: { method: "DELETE", path: "/message/delete", params: "query", send: false },
|
|
81
|
+
demoteGroupParticipant: { method: "POST", path: "/group/demote/participant", params: "body", send: false },
|
|
82
|
+
findSession: { method: "POST", path: "/session/find", params: "none", send: false },
|
|
83
|
+
forwardMessage: { method: "POST", path: "/message/send/forward/message", params: "body", send: true },
|
|
84
|
+
getChatDisplayPicture: { method: "GET", path: "/group/display/picture", params: "query", send: false },
|
|
85
|
+
getChatList: { method: "GET", path: "/chat/list", params: "query", send: false },
|
|
86
|
+
getCollections: { method: "POST", path: "/product/get/collections", params: "body", send: false },
|
|
87
|
+
getContact: { method: "GET", path: "/live_chat/contact", params: "query", send: false },
|
|
88
|
+
getContacts: { method: "GET", path: "/contacts", params: "query", send: false },
|
|
89
|
+
getConversation: { method: "GET", path: "/chat/conversation", params: "query", send: false },
|
|
90
|
+
getGroupConversation: { method: "GET", path: "/group/conversation", params: "query", send: false },
|
|
91
|
+
getGroupList: { method: "GET", path: "/group/list", params: "none", send: false },
|
|
92
|
+
getLabels: { method: "GET", path: "/live_chat/labels", params: "none", send: false },
|
|
93
|
+
getMessageByID: { method: "GET", path: "/message/id", params: "query", send: false },
|
|
94
|
+
getMessages: { method: "GET", path: "/message/fetch", params: "query", send: false },
|
|
95
|
+
getModerators: { method: "GET", path: "/live_chat/moderators", params: "none", send: false },
|
|
96
|
+
getOrder: { method: "POST", path: "/product/get/order", params: "body", send: false },
|
|
97
|
+
getProduct: { method: "GET", path: "/product/id", params: "query", send: false },
|
|
98
|
+
getProducts: { method: "POST", path: "/product", params: "body", send: false },
|
|
99
|
+
groupGetInviteInfo: { method: "GET", path: "/invite/get/info", params: "query", send: false },
|
|
100
|
+
groupMetadata: { method: "GET", path: "/group/meta/data", params: "query", send: false },
|
|
101
|
+
groupSettings: { method: "POST", path: "/group/settings", params: "body", send: false },
|
|
102
|
+
index: { method: "GET", path: "/messages", params: "query", send: false },
|
|
103
|
+
inviteCode: { method: "POST", path: "/invite/code", params: "body", send: false },
|
|
104
|
+
joinGroup: { method: "POST", path: "/group/join", params: "body", send: false },
|
|
105
|
+
muteChat: { method: "POST", path: "/chat/mute", params: "body", send: false },
|
|
106
|
+
pinChat: { method: "POST", path: "/chat/pin", params: "body", send: false },
|
|
107
|
+
promoteGroupParticipant: { method: "POST", path: "/group/promote/participant", params: "body", send: false },
|
|
108
|
+
qr: { method: "POST", path: "/qr", params: "none", send: false },
|
|
109
|
+
queryBroadcast: { method: "POST", path: "/instance/query/broadcast", params: "body", send: false },
|
|
110
|
+
readChat: { method: "POST", path: "/chat/read", params: "body", send: false },
|
|
111
|
+
removeGroupParticipant: { method: "POST", path: "/group/remove/participant", params: "body", send: false },
|
|
112
|
+
renameGroup: { method: "POST", path: "/group/rename", params: "body", send: false },
|
|
113
|
+
revokeInviteCode: { method: "POST", path: "/invite/revoke/code", params: "body", send: false },
|
|
114
|
+
sendAudio: { method: "POST", path: "/send/audio", params: "body", send: true },
|
|
115
|
+
sendButtons: { method: "POST", path: "/button", params: "body", send: true },
|
|
116
|
+
sendButtonsAlias: { method: "POST", path: "/send/buttons", params: "body", send: true },
|
|
117
|
+
sendButtonsTemplate: { method: "POST", path: "/button/template", params: "body", send: true },
|
|
118
|
+
sendButtonsTemplateWithDocument: { method: "POST", path: "/button/document/template", params: "body", send: true },
|
|
119
|
+
sendButtonsTemplateWithImage: { method: "POST", path: "/button/image/template", params: "body", send: true },
|
|
120
|
+
sendButtonsTemplateWithLocation: { method: "POST", path: "/button/location/template", params: "body", send: true },
|
|
121
|
+
sendButtonsTemplateWithVideo: { method: "POST", path: "/button/video/template", params: "body", send: true },
|
|
122
|
+
sendButtonsWithDocument: { method: "POST", path: "/button/document", params: "body", send: true },
|
|
123
|
+
sendButtonsWithImage: { method: "POST", path: "/button/image", params: "body", send: true },
|
|
124
|
+
sendButtonsWithLocation: { method: "POST", path: "/button/location", params: "body", send: true },
|
|
125
|
+
sendButtonsWithVideo: { method: "POST", path: "/button/video", params: "body", send: true },
|
|
126
|
+
sendCarousel: { method: "POST", path: "/carousel", params: "body", send: true },
|
|
127
|
+
sendContact: { method: "POST", path: "/send/contact", params: "body", send: true },
|
|
128
|
+
sendDisappearingMessage: { method: "POST", path: "/message/send/disappearing", params: "body", send: true },
|
|
129
|
+
sendFile: { method: "POST", path: "/send/file", params: "body", send: true },
|
|
130
|
+
sendGif: { method: "POST", path: "/send/gif", params: "body", send: true },
|
|
131
|
+
sendImage: { method: "POST", path: "/send/image", params: "body", send: true },
|
|
132
|
+
sendInviteLink: { method: "POST", path: "/invite/send/link", params: "body", send: true },
|
|
133
|
+
sendList: { method: "POST", path: "/list/message", params: "body", send: true },
|
|
134
|
+
sendLocation: { method: "POST", path: "/send/location", params: "body", send: true },
|
|
135
|
+
sendMention: { method: "POST", path: "/message/send/mention", params: "body", send: true },
|
|
136
|
+
sendMessage: { method: "POST", path: "/message/send", params: "body", send: true },
|
|
137
|
+
sendPTT: { method: "POST", path: "/send/ptt", params: "body", send: true },
|
|
138
|
+
sendReaction: { method: "POST", path: "/message/send/reaction", params: "body", send: true },
|
|
139
|
+
sendReply: { method: "POST", path: "/message/send/reply", params: "body", send: true },
|
|
140
|
+
sendSimpleTemplate: { method: "POST", path: "/template/simple", params: "body", send: true },
|
|
141
|
+
sendSticker: { method: "POST", path: "/message/send/sticker", params: "body", send: true },
|
|
142
|
+
sendTemplateWithDocument: { method: "POST", path: "/template/document", params: "body", send: true },
|
|
143
|
+
sendTemplateWithImage: { method: "POST", path: "/template/image", params: "body", send: true },
|
|
144
|
+
sendTemplateWithVideo: { method: "POST", path: "/template/video", params: "body", send: true },
|
|
145
|
+
sendVideo: { method: "POST", path: "/send/video", params: "body", send: true },
|
|
146
|
+
sendWhatsAppTemplate: { method: "POST", path: "/template/send", params: "body", send: true },
|
|
147
|
+
sessionStatus: { method: "POST", path: "/session/status", params: "none", send: false },
|
|
148
|
+
setChatDisplayPicture: { method: "POST", path: "/group/set/display/picture", params: "body", send: false },
|
|
149
|
+
setContactLabel: { method: "POST", path: "/live_chat/set-label", params: "body", send: false },
|
|
150
|
+
setContactModerator: { method: "POST", path: "/live_chat/set-moderator", params: "body", send: false },
|
|
151
|
+
setDisplayPicture: { method: "POST", path: "/user/set/display/picture", params: "body", send: false },
|
|
152
|
+
setGroupDescription: { method: "POST", path: "/group/set/description", params: "body", send: false },
|
|
153
|
+
setPresence: { method: "POST", path: "/instance/set/presence", params: "body", send: false },
|
|
154
|
+
setProfileName: { method: "POST", path: "/user/set/name", params: "body", send: false },
|
|
155
|
+
setRecording: { method: "POST", path: "/chat/recording", params: "body", send: false },
|
|
156
|
+
setSettings: { method: "POST", path: "/instance/set/settings", params: "body", send: false },
|
|
157
|
+
setStatus: { method: "POST", path: "/instance/set/status", params: "body", send: false },
|
|
158
|
+
setTyping: { method: "POST", path: "/chat/typing", params: "body", send: false },
|
|
159
|
+
setUserStatus: { method: "POST", path: "/user/set/status", params: "body", send: false },
|
|
160
|
+
shareCatalog: { method: "POST", path: "/share/catalog", params: "body", send: true },
|
|
161
|
+
status: { method: "GET", path: "/instance/status", params: "none", send: false },
|
|
162
|
+
unarchiveChat: { method: "POST", path: "/chat/unarchive", params: "body", send: false },
|
|
163
|
+
unblockUser: { method: "POST", path: "/user/unblock", params: "body", send: false },
|
|
164
|
+
unmuteChat: { method: "POST", path: "/chat/unmute", params: "body", send: false },
|
|
165
|
+
unpinChat: { method: "POST", path: "/chat/unpin", params: "body", send: false },
|
|
166
|
+
unreadChat: { method: "POST", path: "/chat/unread", params: "body", send: false },
|
|
167
|
+
userBusinessProfile: { method: "POST", path: "/user/business", params: "body", send: false },
|
|
168
|
+
userDisplayPicture: { method: "GET", path: "/user/display/picture", params: "query", send: false },
|
|
169
|
+
userPresence: { method: "POST", path: "/user/presence", params: "body", send: false },
|
|
170
|
+
userStatus: { method: "POST", path: "/user/status", params: "body", send: false }
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/rate-limit.ts
|
|
174
|
+
var TokenBucket = class {
|
|
175
|
+
constructor(capacity, intervalMs, now = Date.now, wait = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
176
|
+
this.capacity = capacity;
|
|
177
|
+
this.now = now;
|
|
178
|
+
this.wait = wait;
|
|
179
|
+
if (!(capacity > 0) || !(intervalMs > 0)) {
|
|
180
|
+
throw new RangeError("TokenBucket needs a positive capacity and interval");
|
|
181
|
+
}
|
|
182
|
+
this.tokens = capacity;
|
|
183
|
+
this.last = now();
|
|
184
|
+
this.refillPerMs = capacity / intervalMs;
|
|
185
|
+
}
|
|
186
|
+
capacity;
|
|
187
|
+
now;
|
|
188
|
+
wait;
|
|
189
|
+
tokens;
|
|
190
|
+
last;
|
|
191
|
+
refillPerMs;
|
|
192
|
+
/** Resolves once a token is available and taken. */
|
|
193
|
+
async take() {
|
|
194
|
+
for (; ; ) {
|
|
195
|
+
this.refill();
|
|
196
|
+
if (this.tokens >= 1) {
|
|
197
|
+
this.tokens -= 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await this.wait(Math.ceil((1 - this.tokens) / this.refillPerMs));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Tokens available right now (for tests and diagnostics). */
|
|
204
|
+
available() {
|
|
205
|
+
this.refill();
|
|
206
|
+
return this.tokens;
|
|
207
|
+
}
|
|
208
|
+
refill() {
|
|
209
|
+
const now = this.now();
|
|
210
|
+
const elapsed = now - this.last;
|
|
211
|
+
if (elapsed > 0) {
|
|
212
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerMs);
|
|
213
|
+
this.last = now;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/retry.ts
|
|
219
|
+
var DEFAULT_RETRY = {
|
|
220
|
+
maxRetries: 3,
|
|
221
|
+
baseDelayMs: 500,
|
|
222
|
+
maxDelayMs: 8e3,
|
|
223
|
+
maxRetryAfterMs: 6e4
|
|
224
|
+
};
|
|
225
|
+
function backoffDelay(attempt, options, random = Math.random) {
|
|
226
|
+
const ceiling = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** attempt);
|
|
227
|
+
return Math.floor(random() * ceiling);
|
|
228
|
+
}
|
|
229
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
230
|
+
if (value === null || value.trim() === "") return void 0;
|
|
231
|
+
const trimmed = value.trim();
|
|
232
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
233
|
+
const date = Date.parse(trimmed);
|
|
234
|
+
if (Number.isNaN(date)) return void 0;
|
|
235
|
+
return Math.max(0, Math.ceil((date - now) / 1e3));
|
|
236
|
+
}
|
|
237
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
238
|
+
|
|
239
|
+
// src/client.ts
|
|
240
|
+
var DEFAULT_BASE_URL = "https://letsbot.net/api/v1";
|
|
241
|
+
var SDK_VERSION = "0.2.0";
|
|
242
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
243
|
+
var RETRYABLE_5XX = /* @__PURE__ */ new Set([500, 502, 503, 504]);
|
|
244
|
+
var LetsBotClient = class {
|
|
245
|
+
apiKey;
|
|
246
|
+
baseUrl;
|
|
247
|
+
timeoutMs;
|
|
248
|
+
retry;
|
|
249
|
+
bucket;
|
|
250
|
+
throwOnLogicalFailure;
|
|
251
|
+
fetchImpl;
|
|
252
|
+
newKey;
|
|
253
|
+
userAgent;
|
|
254
|
+
sleep;
|
|
255
|
+
random;
|
|
256
|
+
constructor(options, keyLabel = "LetsBotClient: `apiKey` (the channel lb_api_key)") {
|
|
257
|
+
if (!options || typeof options.apiKey !== "string" || options.apiKey.trim() === "") {
|
|
258
|
+
throw new TypeError(`${keyLabel} is required`);
|
|
259
|
+
}
|
|
260
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
261
|
+
if (typeof fetchImpl !== "function") {
|
|
262
|
+
throw new TypeError("LetsBotClient: no fetch available. Use Node 18+ or pass `fetch`.");
|
|
263
|
+
}
|
|
264
|
+
this.apiKey = options.apiKey.trim();
|
|
265
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
266
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
267
|
+
this.retry = { ...DEFAULT_RETRY, ...options.retry ?? {} };
|
|
268
|
+
this.throwOnLogicalFailure = options.throwOnLogicalFailure ?? true;
|
|
269
|
+
this.fetchImpl = fetchImpl;
|
|
270
|
+
this.newKey = options.generateIdempotencyKey ?? (() => randomUUID());
|
|
271
|
+
this.userAgent = `letsbot-sdk-typescript/${SDK_VERSION}${options.userAgent ? ` ${options.userAgent}` : ""}`;
|
|
272
|
+
this.sleep = options.sleep ?? sleep;
|
|
273
|
+
this.random = options.random ?? Math.random;
|
|
274
|
+
const perMinute = options.rateLimit === false ? null : options.rateLimit?.requestsPerMinute ?? 1e3;
|
|
275
|
+
this.bucket = perMinute === null ? null : new TokenBucket(perMinute, 6e4, options.now ?? Date.now, this.sleep);
|
|
276
|
+
}
|
|
277
|
+
/** Call any operation by its operationId and get the body. */
|
|
278
|
+
async call(operationId, ...args) {
|
|
279
|
+
return (await this.callWithResponse(operationId, ...args)).data;
|
|
280
|
+
}
|
|
281
|
+
/** Call any operation and get the body plus status, headers, Idempotency-Key and replay flag. */
|
|
282
|
+
async callWithResponse(operationId, ...args) {
|
|
283
|
+
const [params, options = {}] = args;
|
|
284
|
+
const meta = operations[operationId];
|
|
285
|
+
if (!meta) {
|
|
286
|
+
throw new TypeError(`Unknown LetsBot operation: ${String(operationId)}`);
|
|
287
|
+
}
|
|
288
|
+
const url = new URL(this.baseUrl + meta.path);
|
|
289
|
+
const headers = this.baseHeaders(options);
|
|
290
|
+
let body;
|
|
291
|
+
if (meta.params === "query") {
|
|
292
|
+
appendQuery(url, params);
|
|
293
|
+
} else if (meta.params === "body") {
|
|
294
|
+
headers["Content-Type"] = "application/json";
|
|
295
|
+
body = JSON.stringify(params ?? {});
|
|
296
|
+
}
|
|
297
|
+
let idempotencyKey;
|
|
298
|
+
if (meta.send && options.idempotencyKey !== false) {
|
|
299
|
+
idempotencyKey = options.idempotencyKey ?? this.newKey();
|
|
300
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
301
|
+
}
|
|
302
|
+
const unsafeRetryAllowed = IDEMPOTENT_METHODS.has(meta.method) || meta.send && idempotencyKey !== void 0;
|
|
303
|
+
return this.perform(operationId, meta.method, url, headers, body, { idempotencyKey, unsafeRetryAllowed }, options);
|
|
304
|
+
}
|
|
305
|
+
/** Accept, auth, User-Agent and the per-call extra headers. */
|
|
306
|
+
baseHeaders(options) {
|
|
307
|
+
return {
|
|
308
|
+
Accept: "application/json",
|
|
309
|
+
...this.authHeaders(this.apiKey),
|
|
310
|
+
"User-Agent": this.userAgent,
|
|
311
|
+
...options.headers ?? {}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** How the key travels. The /api/v1 channel API takes a Bearer token. */
|
|
315
|
+
authHeaders(apiKey) {
|
|
316
|
+
return { Authorization: `Bearer ${apiKey}` };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* One request with the client's rate limit, timeout, retries and error mapping.
|
|
320
|
+
* `unsafeRetryAllowed`: whether a network error / 5xx may be retried (reads, keyed sends).
|
|
321
|
+
*/
|
|
322
|
+
async perform(operationId, method, url, headers, body, retry, options) {
|
|
323
|
+
const { idempotencyKey, unsafeRetryAllowed } = retry;
|
|
324
|
+
const maxRetries = options.maxRetries ?? this.retry.maxRetries;
|
|
325
|
+
for (let attempt = 0; ; attempt++) {
|
|
326
|
+
throwIfAborted(options.signal);
|
|
327
|
+
if (this.bucket) await this.bucket.take();
|
|
328
|
+
let response;
|
|
329
|
+
try {
|
|
330
|
+
response = await this.send(url, method, headers, body, options);
|
|
331
|
+
} catch (error2) {
|
|
332
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error2;
|
|
333
|
+
if (unsafeRetryAllowed && attempt < maxRetries) {
|
|
334
|
+
await this.wait(backoffDelay(attempt, this.retry, this.random), options.signal);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw new LetsBotConnectionError(operationId, error2);
|
|
338
|
+
}
|
|
339
|
+
const parsed = await readBody(response);
|
|
340
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
341
|
+
if (response.ok && !(this.throwOnLogicalFailure && isLogicalFailure(parsed))) {
|
|
342
|
+
return {
|
|
343
|
+
data: parsed,
|
|
344
|
+
status: response.status,
|
|
345
|
+
headers: response.headers,
|
|
346
|
+
idempotencyKey,
|
|
347
|
+
replayed: response.headers.get("idempotent-replayed") === "true",
|
|
348
|
+
attempts: attempt + 1
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const error = new LetsBotApiError({ status: response.status, body: parsed, headers: response.headers, operationId, retryAfter });
|
|
352
|
+
if (!response.ok && attempt < maxRetries && this.shouldRetry(error, unsafeRetryAllowed)) {
|
|
353
|
+
let delay = backoffDelay(attempt, this.retry, this.random);
|
|
354
|
+
if (retryAfter !== void 0) {
|
|
355
|
+
if (retryAfter * 1e3 > this.retry.maxRetryAfterMs) throw error;
|
|
356
|
+
delay = retryAfter * 1e3;
|
|
357
|
+
}
|
|
358
|
+
await this.wait(delay, options.signal);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
shouldRetry(error, unsafeRetryAllowed) {
|
|
365
|
+
if (error.status === 429) return true;
|
|
366
|
+
if (error.status === 409 && error.code === "idempotency_request_in_progress") return true;
|
|
367
|
+
return RETRYABLE_5XX.has(error.status) && unsafeRetryAllowed;
|
|
368
|
+
}
|
|
369
|
+
async send(url, method, headers, body, options) {
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
const timeout = setTimeout(() => controller.abort(new Error(`timed out after ${options.timeoutMs ?? this.timeoutMs} ms`)), options.timeoutMs ?? this.timeoutMs);
|
|
372
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
373
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
374
|
+
try {
|
|
375
|
+
return await this.fetchImpl(url.toString(), { method, headers, body, signal: controller.signal });
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timeout);
|
|
378
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async wait(ms, signal) {
|
|
382
|
+
await this.sleep(ms);
|
|
383
|
+
throwIfAborted(signal);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
function appendQuery(url, params) {
|
|
387
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
388
|
+
if (value === void 0 || value === null) continue;
|
|
389
|
+
url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function readBody(response) {
|
|
393
|
+
const text = await response.text();
|
|
394
|
+
if (text === "") return void 0;
|
|
395
|
+
try {
|
|
396
|
+
return JSON.parse(text);
|
|
397
|
+
} catch {
|
|
398
|
+
return text;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function throwIfAborted(signal) {
|
|
402
|
+
if (signal?.aborted) {
|
|
403
|
+
throw signal.reason ?? new Error("aborted");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/resources.ts
|
|
408
|
+
var resourceMap = {
|
|
409
|
+
messages: {
|
|
410
|
+
send: "sendMessage",
|
|
411
|
+
sendDisappearing: "sendDisappearingMessage",
|
|
412
|
+
forward: "forwardMessage",
|
|
413
|
+
sendSticker: "sendSticker",
|
|
414
|
+
react: "sendReaction",
|
|
415
|
+
reply: "sendReply",
|
|
416
|
+
mention: "sendMention",
|
|
417
|
+
sendList: "sendList",
|
|
418
|
+
fetch: "getMessages",
|
|
419
|
+
get: "getMessageByID",
|
|
420
|
+
delete: "deleteMessage"
|
|
421
|
+
},
|
|
422
|
+
media: {
|
|
423
|
+
sendFile: "sendFile",
|
|
424
|
+
sendImage: "sendImage",
|
|
425
|
+
sendAudio: "sendAudio",
|
|
426
|
+
sendVideo: "sendVideo",
|
|
427
|
+
sendGif: "sendGif",
|
|
428
|
+
sendLocation: "sendLocation",
|
|
429
|
+
sendVoiceNote: "sendPTT",
|
|
430
|
+
sendContact: "sendContact",
|
|
431
|
+
shareCatalog: "shareCatalog"
|
|
432
|
+
},
|
|
433
|
+
interactive: {
|
|
434
|
+
sendButtons: "sendButtons",
|
|
435
|
+
sendButtonsWithImage: "sendButtonsWithImage",
|
|
436
|
+
sendButtonsWithVideo: "sendButtonsWithVideo",
|
|
437
|
+
sendButtonsWithLocation: "sendButtonsWithLocation",
|
|
438
|
+
sendButtonsWithDocument: "sendButtonsWithDocument",
|
|
439
|
+
sendButtonsTemplate: "sendButtonsTemplate",
|
|
440
|
+
sendButtonsTemplateWithImage: "sendButtonsTemplateWithImage",
|
|
441
|
+
sendButtonsTemplateWithVideo: "sendButtonsTemplateWithVideo",
|
|
442
|
+
sendButtonsTemplateWithLocation: "sendButtonsTemplateWithLocation",
|
|
443
|
+
sendButtonsTemplateWithDocument: "sendButtonsTemplateWithDocument",
|
|
444
|
+
sendCarousel: "sendCarousel"
|
|
445
|
+
},
|
|
446
|
+
templates: {
|
|
447
|
+
send: "sendWhatsAppTemplate",
|
|
448
|
+
sendSimple: "sendSimpleTemplate",
|
|
449
|
+
sendWithImage: "sendTemplateWithImage",
|
|
450
|
+
sendWithDocument: "sendTemplateWithDocument",
|
|
451
|
+
sendWithVideo: "sendTemplateWithVideo"
|
|
452
|
+
},
|
|
453
|
+
chats: {
|
|
454
|
+
list: "getChatList",
|
|
455
|
+
conversation: "getConversation",
|
|
456
|
+
typing: "setTyping",
|
|
457
|
+
recording: "setRecording",
|
|
458
|
+
read: "readChat",
|
|
459
|
+
unread: "unreadChat",
|
|
460
|
+
archive: "archiveChat",
|
|
461
|
+
unarchive: "unarchiveChat",
|
|
462
|
+
mute: "muteChat",
|
|
463
|
+
unmute: "unmuteChat",
|
|
464
|
+
pin: "pinChat",
|
|
465
|
+
unpin: "unpinChat",
|
|
466
|
+
checkPhone: "checkPhone"
|
|
467
|
+
},
|
|
468
|
+
contacts: {
|
|
469
|
+
list: "getContacts",
|
|
470
|
+
get: "getContact",
|
|
471
|
+
listModerators: "getModerators",
|
|
472
|
+
assignModerator: "setContactModerator",
|
|
473
|
+
listLabels: "getLabels",
|
|
474
|
+
addLabel: "setContactLabel"
|
|
475
|
+
},
|
|
476
|
+
groups: {
|
|
477
|
+
create: "createGroup",
|
|
478
|
+
join: "joinGroup",
|
|
479
|
+
list: "getGroupList",
|
|
480
|
+
conversation: "getGroupConversation",
|
|
481
|
+
metadata: "groupMetadata",
|
|
482
|
+
settings: "groupSettings",
|
|
483
|
+
rename: "renameGroup",
|
|
484
|
+
setDescription: "setGroupDescription",
|
|
485
|
+
addParticipants: "addGroupParticipant",
|
|
486
|
+
removeParticipants: "removeGroupParticipant",
|
|
487
|
+
promoteParticipants: "promoteGroupParticipant",
|
|
488
|
+
demoteParticipants: "demoteGroupParticipant",
|
|
489
|
+
setPicture: "setChatDisplayPicture",
|
|
490
|
+
getPicture: "getChatDisplayPicture"
|
|
491
|
+
},
|
|
492
|
+
invites: {
|
|
493
|
+
getCode: "inviteCode",
|
|
494
|
+
revokeCode: "revokeInviteCode",
|
|
495
|
+
sendLink: "sendInviteLink",
|
|
496
|
+
getInfo: "groupGetInviteInfo"
|
|
497
|
+
},
|
|
498
|
+
user: {
|
|
499
|
+
setName: "setProfileName",
|
|
500
|
+
businessProfile: "userBusinessProfile",
|
|
501
|
+
block: "blockUser",
|
|
502
|
+
unblock: "unblockUser",
|
|
503
|
+
setPicture: "setDisplayPicture",
|
|
504
|
+
status: "userStatus",
|
|
505
|
+
setStatus: "setUserStatus",
|
|
506
|
+
presence: "userPresence",
|
|
507
|
+
picture: "userDisplayPicture"
|
|
508
|
+
},
|
|
509
|
+
instance: {
|
|
510
|
+
status: "status",
|
|
511
|
+
setStatus: "setStatus",
|
|
512
|
+
setPresence: "setPresence",
|
|
513
|
+
setSettings: "setSettings",
|
|
514
|
+
queryBroadcast: "queryBroadcast"
|
|
515
|
+
},
|
|
516
|
+
products: {
|
|
517
|
+
list: "getProducts",
|
|
518
|
+
get: "getProduct",
|
|
519
|
+
create: "createProduct",
|
|
520
|
+
collections: "getCollections",
|
|
521
|
+
getOrder: "getOrder"
|
|
522
|
+
},
|
|
523
|
+
session: {
|
|
524
|
+
qr: "qr",
|
|
525
|
+
find: "findSession",
|
|
526
|
+
status: "sessionStatus"
|
|
527
|
+
},
|
|
528
|
+
archive: {
|
|
529
|
+
list: "index"
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function bindResource(client, map) {
|
|
533
|
+
const bound = {};
|
|
534
|
+
for (const [name, operationId] of Object.entries(map)) {
|
|
535
|
+
bound[name] = (...args) => client.call(operationId, ...args);
|
|
536
|
+
}
|
|
537
|
+
return bound;
|
|
538
|
+
}
|
|
539
|
+
async function* iterateArchive(client, params, options) {
|
|
540
|
+
let cursor;
|
|
541
|
+
do {
|
|
542
|
+
const page = await client.call("index", { ...params, ...cursor ? { cursor } : {} }, options);
|
|
543
|
+
yield* page.data;
|
|
544
|
+
cursor = page.next_cursor ?? void 0;
|
|
545
|
+
} while (cursor);
|
|
546
|
+
}
|
|
547
|
+
var LEGACY_WEBHOOK_SECRET = "Letsbot";
|
|
548
|
+
function experimental_verifyWebhookSignature(rawBody, signature, secret = LEGACY_WEBHOOK_SECRET) {
|
|
549
|
+
if (typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature.trim())) return false;
|
|
550
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest();
|
|
551
|
+
const given = Buffer.from(signature.trim(), "hex");
|
|
552
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/generated/crm-operations.ts
|
|
556
|
+
var crmOperations = {
|
|
557
|
+
crmAddCompanyNote: { method: "POST", path: "/companies/{id}/notes", body: true },
|
|
558
|
+
crmAddContactNote: { method: "POST", path: "/contacts/{id}/notes", body: true },
|
|
559
|
+
crmAddDealNote: { method: "POST", path: "/deals/{id}/notes", body: true },
|
|
560
|
+
crmCompaniesSchema: { method: "GET", path: "/companies/schema", body: false },
|
|
561
|
+
crmCompleteTask: { method: "POST", path: "/tasks/{id}/complete", body: false },
|
|
562
|
+
crmContactsSchema: { method: "GET", path: "/contacts/schema", body: false },
|
|
563
|
+
crmCreateActivity: { method: "POST", path: "/activities", body: true },
|
|
564
|
+
crmCreateCompany: { method: "POST", path: "/companies", body: true },
|
|
565
|
+
crmCreateContact: { method: "POST", path: "/contacts", body: true },
|
|
566
|
+
crmCreateDeal: { method: "POST", path: "/deals", body: true },
|
|
567
|
+
crmCreateProduct: { method: "POST", path: "/products", body: true },
|
|
568
|
+
crmCreateQuote: { method: "POST", path: "/quotes", body: true },
|
|
569
|
+
crmCreateTask: { method: "POST", path: "/tasks", body: true },
|
|
570
|
+
crmDeleteCompany: { method: "DELETE", path: "/companies/{id}", body: false },
|
|
571
|
+
crmDeleteDeal: { method: "DELETE", path: "/deals/{id}", body: false },
|
|
572
|
+
crmDeleteProduct: { method: "DELETE", path: "/products/{id}", body: false },
|
|
573
|
+
crmDeleteQuote: { method: "DELETE", path: "/quotes/{id}", body: false },
|
|
574
|
+
crmDeleteTask: { method: "DELETE", path: "/tasks/{id}", body: false },
|
|
575
|
+
crmGetActivity: { method: "GET", path: "/activities/{id}", body: false },
|
|
576
|
+
crmGetCompany: { method: "GET", path: "/companies/{id}", body: false },
|
|
577
|
+
crmGetContact: { method: "GET", path: "/contacts/{id}", body: false },
|
|
578
|
+
crmGetDeal: { method: "GET", path: "/deals/{id}", body: false },
|
|
579
|
+
crmGetProduct: { method: "GET", path: "/products/{id}", body: false },
|
|
580
|
+
crmGetQuote: { method: "GET", path: "/quotes/{id}", body: false },
|
|
581
|
+
crmGetTask: { method: "GET", path: "/tasks/{id}", body: false },
|
|
582
|
+
crmHookSample: { method: "GET", path: "/hooks/sample", body: false },
|
|
583
|
+
crmLinkCompanyContact: { method: "POST", path: "/companies/{id}/contacts", body: true },
|
|
584
|
+
crmListActivities: { method: "GET", path: "/activities", body: false },
|
|
585
|
+
crmListCompanies: { method: "GET", path: "/companies", body: false },
|
|
586
|
+
crmListContacts: { method: "GET", path: "/contacts", body: false },
|
|
587
|
+
crmListDeals: { method: "GET", path: "/deals", body: false },
|
|
588
|
+
crmListHookEvents: { method: "GET", path: "/hooks/events", body: false },
|
|
589
|
+
crmListHooks: { method: "GET", path: "/hooks", body: false },
|
|
590
|
+
crmListLostReasons: { method: "GET", path: "/deals/lost-reasons", body: false },
|
|
591
|
+
crmListPipelines: { method: "GET", path: "/pipelines", body: false },
|
|
592
|
+
crmListProducts: { method: "GET", path: "/products", body: false },
|
|
593
|
+
crmListQuotes: { method: "GET", path: "/quotes", body: false },
|
|
594
|
+
crmListTasks: { method: "GET", path: "/tasks", body: false },
|
|
595
|
+
crmMarkDealLost: { method: "POST", path: "/deals/{id}/lost", body: true },
|
|
596
|
+
crmMarkDealWon: { method: "POST", path: "/deals/{id}/won", body: false },
|
|
597
|
+
crmMoveDealStage: { method: "POST", path: "/deals/{id}/stage", body: true },
|
|
598
|
+
crmProductsSchema: { method: "GET", path: "/products/schema", body: false },
|
|
599
|
+
crmQuotesSchema: { method: "GET", path: "/quotes/schema", body: false },
|
|
600
|
+
crmSearchCompanies: { method: "GET", path: "/companies/search", body: false },
|
|
601
|
+
crmSearchContacts: { method: "GET", path: "/contacts/search", body: false },
|
|
602
|
+
crmSearchDeals: { method: "GET", path: "/deals/search", body: false },
|
|
603
|
+
crmSubscribeHook: { method: "POST", path: "/hooks", body: true },
|
|
604
|
+
crmUnlinkCompanyContact: { method: "DELETE", path: "/companies/{id}/contacts/{contactId}", body: false },
|
|
605
|
+
crmUnsubscribeHook: { method: "DELETE", path: "/hooks/{id}", body: false },
|
|
606
|
+
crmUpdateCompany: { method: "PATCH", path: "/companies/{id}", body: true },
|
|
607
|
+
crmUpdateContact: { method: "PATCH", path: "/contacts/{id}", body: true },
|
|
608
|
+
crmUpdateDeal: { method: "PATCH", path: "/deals/{id}", body: true },
|
|
609
|
+
crmUpdateProduct: { method: "PATCH", path: "/products/{id}", body: true },
|
|
610
|
+
crmUpdateQuote: { method: "PATCH", path: "/quotes/{id}", body: true },
|
|
611
|
+
crmUpdateTask: { method: "PATCH", path: "/tasks/{id}", body: true }
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// src/crm.ts
|
|
615
|
+
var CRM_DEFAULT_RATE_LIMIT = 120;
|
|
616
|
+
var CRM_SIGNATURE_HEADER = "X-LetsBot-Signature";
|
|
617
|
+
var crmResourceMap = {
|
|
618
|
+
contacts: {
|
|
619
|
+
list: "crmListContacts",
|
|
620
|
+
schema: "crmContactsSchema",
|
|
621
|
+
search: "crmSearchContacts",
|
|
622
|
+
get: "crmGetContact",
|
|
623
|
+
create: "crmCreateContact",
|
|
624
|
+
update: "crmUpdateContact",
|
|
625
|
+
addNote: "crmAddContactNote"
|
|
626
|
+
},
|
|
627
|
+
companies: {
|
|
628
|
+
list: "crmListCompanies",
|
|
629
|
+
schema: "crmCompaniesSchema",
|
|
630
|
+
search: "crmSearchCompanies",
|
|
631
|
+
get: "crmGetCompany",
|
|
632
|
+
create: "crmCreateCompany",
|
|
633
|
+
update: "crmUpdateCompany",
|
|
634
|
+
delete: "crmDeleteCompany",
|
|
635
|
+
linkContact: "crmLinkCompanyContact",
|
|
636
|
+
unlinkContact: "crmUnlinkCompanyContact",
|
|
637
|
+
addNote: "crmAddCompanyNote"
|
|
638
|
+
},
|
|
639
|
+
deals: {
|
|
640
|
+
list: "crmListDeals",
|
|
641
|
+
pipelines: "crmListPipelines",
|
|
642
|
+
lostReasons: "crmListLostReasons",
|
|
643
|
+
search: "crmSearchDeals",
|
|
644
|
+
get: "crmGetDeal",
|
|
645
|
+
create: "crmCreateDeal",
|
|
646
|
+
update: "crmUpdateDeal",
|
|
647
|
+
delete: "crmDeleteDeal",
|
|
648
|
+
moveStage: "crmMoveDealStage",
|
|
649
|
+
markWon: "crmMarkDealWon",
|
|
650
|
+
markLost: "crmMarkDealLost",
|
|
651
|
+
addNote: "crmAddDealNote"
|
|
652
|
+
},
|
|
653
|
+
tasks: {
|
|
654
|
+
list: "crmListTasks",
|
|
655
|
+
get: "crmGetTask",
|
|
656
|
+
create: "crmCreateTask",
|
|
657
|
+
update: "crmUpdateTask",
|
|
658
|
+
delete: "crmDeleteTask",
|
|
659
|
+
complete: "crmCompleteTask"
|
|
660
|
+
},
|
|
661
|
+
activities: {
|
|
662
|
+
list: "crmListActivities",
|
|
663
|
+
get: "crmGetActivity",
|
|
664
|
+
create: "crmCreateActivity"
|
|
665
|
+
},
|
|
666
|
+
quotes: {
|
|
667
|
+
list: "crmListQuotes",
|
|
668
|
+
schema: "crmQuotesSchema",
|
|
669
|
+
get: "crmGetQuote",
|
|
670
|
+
create: "crmCreateQuote",
|
|
671
|
+
update: "crmUpdateQuote",
|
|
672
|
+
delete: "crmDeleteQuote"
|
|
673
|
+
},
|
|
674
|
+
products: {
|
|
675
|
+
list: "crmListProducts",
|
|
676
|
+
schema: "crmProductsSchema",
|
|
677
|
+
get: "crmGetProduct",
|
|
678
|
+
create: "crmCreateProduct",
|
|
679
|
+
update: "crmUpdateProduct",
|
|
680
|
+
delete: "crmDeleteProduct"
|
|
681
|
+
},
|
|
682
|
+
hooks: {
|
|
683
|
+
list: "crmListHooks",
|
|
684
|
+
events: "crmListHookEvents",
|
|
685
|
+
sample: "crmHookSample",
|
|
686
|
+
subscribe: "crmSubscribeHook",
|
|
687
|
+
unsubscribe: "crmUnsubscribeHook"
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
var UNSAFE = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
691
|
+
function crmBaseUrl(workspace) {
|
|
692
|
+
const raw = workspace.trim();
|
|
693
|
+
if (raw === "") throw new TypeError("LetsBotCrm: `workspace` is empty");
|
|
694
|
+
const host = (/^https?:\/\//i.test(raw) ? new URL(raw).host : raw.split("/")[0]).toLowerCase();
|
|
695
|
+
return `https://${host.includes(".") ? host : `${host}.letsbot.net`}/api/v1/crm`;
|
|
696
|
+
}
|
|
697
|
+
var CrmTransport = class extends LetsBotClient {
|
|
698
|
+
authHeaders(apiKey) {
|
|
699
|
+
return { "X-Zapier-Api-Key": apiKey };
|
|
700
|
+
}
|
|
701
|
+
request(operationId, method, url, json, options) {
|
|
702
|
+
const headers = this.baseHeaders(options);
|
|
703
|
+
let body;
|
|
704
|
+
if (json !== void 0) {
|
|
705
|
+
headers["Content-Type"] = "application/json";
|
|
706
|
+
body = JSON.stringify(json);
|
|
707
|
+
}
|
|
708
|
+
return this.perform(operationId, method, url, headers, body, { unsafeRetryAllowed: !UNSAFE.has(method) }, options);
|
|
709
|
+
}
|
|
710
|
+
get root() {
|
|
711
|
+
return this.baseUrl;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var LetsBotCrm = class {
|
|
715
|
+
contacts;
|
|
716
|
+
companies;
|
|
717
|
+
deals;
|
|
718
|
+
tasks;
|
|
719
|
+
activities;
|
|
720
|
+
quotes;
|
|
721
|
+
products;
|
|
722
|
+
hooks;
|
|
723
|
+
transport;
|
|
724
|
+
constructor(options) {
|
|
725
|
+
if (!options?.baseUrl && !options?.workspace) {
|
|
726
|
+
throw new TypeError("LetsBotCrm: pass `workspace` (e.g. mystore.letsbot.net) or `baseUrl`");
|
|
727
|
+
}
|
|
728
|
+
this.transport = new CrmTransport(
|
|
729
|
+
{
|
|
730
|
+
...options,
|
|
731
|
+
baseUrl: options.baseUrl ?? crmBaseUrl(options.workspace),
|
|
732
|
+
rateLimit: options.rateLimit === false ? false : { requestsPerMinute: options.rateLimit?.requestsPerMinute ?? CRM_DEFAULT_RATE_LIMIT }
|
|
733
|
+
},
|
|
734
|
+
"LetsBotCrm: `apiKey` (a workspace API key with the crm scope)"
|
|
735
|
+
);
|
|
736
|
+
const bind = (map) => {
|
|
737
|
+
const bound = {};
|
|
738
|
+
for (const [name, id] of Object.entries(map)) {
|
|
739
|
+
bound[name] = (params, options2) => this.call(id, ...[params, options2]);
|
|
740
|
+
}
|
|
741
|
+
return bound;
|
|
742
|
+
};
|
|
743
|
+
this.contacts = bind(crmResourceMap.contacts);
|
|
744
|
+
this.companies = bind(crmResourceMap.companies);
|
|
745
|
+
this.deals = bind(crmResourceMap.deals);
|
|
746
|
+
this.tasks = bind(crmResourceMap.tasks);
|
|
747
|
+
this.activities = bind(crmResourceMap.activities);
|
|
748
|
+
this.quotes = bind(crmResourceMap.quotes);
|
|
749
|
+
this.products = bind(crmResourceMap.products);
|
|
750
|
+
this.hooks = bind(crmResourceMap.hooks);
|
|
751
|
+
}
|
|
752
|
+
/** The resolved base URL, e.g. `https://mystore.letsbot.net/api/v1/crm`. */
|
|
753
|
+
get baseUrl() {
|
|
754
|
+
return this.transport.root;
|
|
755
|
+
}
|
|
756
|
+
/** Call any CRM operation by its operationId and get the body. */
|
|
757
|
+
async call(operationId, ...args) {
|
|
758
|
+
return (await this.callWithResponse(operationId, ...args)).data;
|
|
759
|
+
}
|
|
760
|
+
/** Call any CRM operation and get the body plus status and headers. */
|
|
761
|
+
async callWithResponse(operationId, ...args) {
|
|
762
|
+
const [params = {}, options = {}] = args;
|
|
763
|
+
const meta = crmOperations[operationId];
|
|
764
|
+
if (!meta) throw new TypeError(`Unknown LetsBot CRM operation: ${String(operationId)}`);
|
|
765
|
+
const rest = { ...params };
|
|
766
|
+
const path = meta.path.replace(/\{(\w+)\}/g, (_, name) => {
|
|
767
|
+
const value = rest[name];
|
|
768
|
+
if (value === void 0 || value === null || value === "") {
|
|
769
|
+
throw new TypeError(`${operationId}: \`${name}\` is required`);
|
|
770
|
+
}
|
|
771
|
+
delete rest[name];
|
|
772
|
+
return encodeURIComponent(String(value));
|
|
773
|
+
});
|
|
774
|
+
const url = new URL(this.transport.root + path);
|
|
775
|
+
if (!meta.body) {
|
|
776
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
777
|
+
if (value === void 0 || value === null) continue;
|
|
778
|
+
url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return this.transport.request(operationId, meta.method, url, meta.body ? rest : void 0, options);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Every row of a list, page by page (`meta.last_page`).
|
|
785
|
+
*
|
|
786
|
+
* ```ts
|
|
787
|
+
* for await (const deal of crm.iterate('crmListDeals', { status: 'won', sort: 'updated_at', dir: 'desc' })) { … }
|
|
788
|
+
* ```
|
|
789
|
+
*/
|
|
790
|
+
async *iterate(operationId, params = {}, options) {
|
|
791
|
+
for (let page = 1; ; page++) {
|
|
792
|
+
const body = await this.call(operationId, ...[{ per_page: 100, ...params, page }, options]);
|
|
793
|
+
yield* body.data;
|
|
794
|
+
if (body.data.length === 0 || page >= (body.meta?.last_page ?? page)) return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
function verifyCrmWebhookSignature(rawBody, header, secret, options = {}) {
|
|
799
|
+
if (typeof header !== "string" || !secret) return false;
|
|
800
|
+
const parts = /* @__PURE__ */ new Map();
|
|
801
|
+
for (const piece of header.split(",")) {
|
|
802
|
+
const index = piece.indexOf("=");
|
|
803
|
+
if (index > 0) parts.set(piece.slice(0, index).trim(), piece.slice(index + 1).trim());
|
|
804
|
+
}
|
|
805
|
+
const t = parts.get("t") ?? "";
|
|
806
|
+
const v1 = parts.get("v1") ?? "";
|
|
807
|
+
if (!/^\d+$/.test(t) || !/^[0-9a-f]{64}$/i.test(v1)) return false;
|
|
808
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
809
|
+
if (Math.abs(now - Number(t)) > (options.toleranceSeconds ?? 300)) return false;
|
|
810
|
+
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
|
|
811
|
+
const given = Buffer.from(v1, "hex");
|
|
812
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
813
|
+
}
|
|
814
|
+
var IDENTITY_MAX_SUB_LENGTH = 191;
|
|
815
|
+
var IDENTITY_MAX_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
816
|
+
var IDENTITY_DEFAULT_TTL = "24h";
|
|
817
|
+
var UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
818
|
+
function ttlSeconds(value) {
|
|
819
|
+
let seconds;
|
|
820
|
+
if (typeof value === "number") {
|
|
821
|
+
seconds = value;
|
|
822
|
+
} else if (typeof value === "string") {
|
|
823
|
+
const match = /^\s*(\d+)\s*([smhd]?)\s*$/.exec(value);
|
|
824
|
+
if (!match) throw new TypeError(`expiresIn must be seconds or a string like "24h", got "${value}"`);
|
|
825
|
+
seconds = Number(match[1]) * UNIT_SECONDS[match[2] || "s"];
|
|
826
|
+
} else {
|
|
827
|
+
throw new TypeError('expiresIn must be a number of seconds or a string like "24h"');
|
|
828
|
+
}
|
|
829
|
+
if (!Number.isInteger(seconds) || seconds <= 0) {
|
|
830
|
+
throw new RangeError("expiresIn must be a positive whole number of seconds");
|
|
831
|
+
}
|
|
832
|
+
if (seconds > IDENTITY_MAX_TTL_SECONDS) {
|
|
833
|
+
throw new RangeError(`expiresIn must be at most 7 days (${IDENTITY_MAX_TTL_SECONDS} seconds)`);
|
|
834
|
+
}
|
|
835
|
+
return seconds;
|
|
836
|
+
}
|
|
837
|
+
function unixNow(now) {
|
|
838
|
+
if (now === void 0) return Math.floor(Date.now() / 1e3);
|
|
839
|
+
const value = now instanceof Date ? Math.floor(now.getTime() / 1e3) : Math.floor(now);
|
|
840
|
+
if (!Number.isFinite(value) || value < 0) throw new TypeError("now must be a valid Date or Unix time in seconds");
|
|
841
|
+
return value;
|
|
842
|
+
}
|
|
843
|
+
function base64url(input) {
|
|
844
|
+
return Buffer.from(input).toString("base64url");
|
|
845
|
+
}
|
|
846
|
+
function optionalClaim(name, value) {
|
|
847
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
848
|
+
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
|
|
849
|
+
return value;
|
|
850
|
+
}
|
|
851
|
+
function createIdentityToken(options) {
|
|
852
|
+
if (options === null || typeof options !== "object") throw new TypeError("options are required");
|
|
853
|
+
const { userId, secret, expiresIn = IDENTITY_DEFAULT_TTL, now } = options;
|
|
854
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
855
|
+
throw new TypeError("secret must be a non-empty string (the app's Identity Secret)");
|
|
856
|
+
}
|
|
857
|
+
if (typeof userId === "number" && !Number.isFinite(userId)) throw new TypeError("userId must be a finite number or a string");
|
|
858
|
+
if (typeof userId !== "string" && typeof userId !== "number") throw new TypeError("userId must be a string or a number");
|
|
859
|
+
const sub = String(userId).trim();
|
|
860
|
+
if (sub.length === 0) throw new TypeError("userId must not be empty");
|
|
861
|
+
if ([...sub].length > IDENTITY_MAX_SUB_LENGTH) {
|
|
862
|
+
throw new RangeError(`userId must be at most ${IDENTITY_MAX_SUB_LENGTH} characters`);
|
|
863
|
+
}
|
|
864
|
+
const iat = unixNow(now);
|
|
865
|
+
const claims = { sub, iat, exp: iat + ttlSeconds(expiresIn) };
|
|
866
|
+
const name = optionalClaim("name", options.name);
|
|
867
|
+
const email = optionalClaim("email", options.email);
|
|
868
|
+
const phone = optionalClaim("phone", options.phone);
|
|
869
|
+
if (name !== void 0) claims.name = name;
|
|
870
|
+
if (email !== void 0) claims.email = email;
|
|
871
|
+
if (phone !== void 0) claims.phone = phone;
|
|
872
|
+
const signingInput = `${base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${base64url(JSON.stringify(claims))}`;
|
|
873
|
+
const signature = createHmac("sha256", secret).update(signingInput).digest("base64url");
|
|
874
|
+
return `${signingInput}.${signature}`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/index.ts
|
|
878
|
+
var LetsBot = class extends LetsBotClient {
|
|
879
|
+
messages;
|
|
880
|
+
media;
|
|
881
|
+
interactive;
|
|
882
|
+
templates;
|
|
883
|
+
chats;
|
|
884
|
+
contacts;
|
|
885
|
+
groups;
|
|
886
|
+
invites;
|
|
887
|
+
user;
|
|
888
|
+
instance;
|
|
889
|
+
products;
|
|
890
|
+
session;
|
|
891
|
+
archive;
|
|
892
|
+
constructor(options) {
|
|
893
|
+
super(options);
|
|
894
|
+
this.messages = bindResource(this, resourceMap.messages);
|
|
895
|
+
this.media = bindResource(this, resourceMap.media);
|
|
896
|
+
this.interactive = bindResource(this, resourceMap.interactive);
|
|
897
|
+
this.templates = bindResource(this, resourceMap.templates);
|
|
898
|
+
this.chats = bindResource(this, resourceMap.chats);
|
|
899
|
+
this.contacts = bindResource(this, resourceMap.contacts);
|
|
900
|
+
this.groups = bindResource(this, resourceMap.groups);
|
|
901
|
+
this.invites = bindResource(this, resourceMap.invites);
|
|
902
|
+
this.user = bindResource(this, resourceMap.user);
|
|
903
|
+
this.instance = bindResource(this, resourceMap.instance);
|
|
904
|
+
this.products = bindResource(this, resourceMap.products);
|
|
905
|
+
this.session = bindResource(this, resourceMap.session);
|
|
906
|
+
this.archive = {
|
|
907
|
+
...bindResource(this, resourceMap.archive),
|
|
908
|
+
iterate: (params, options2) => iterateArchive(this, params, options2)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
export { CRM_DEFAULT_RATE_LIMIT, CRM_SIGNATURE_HEADER, DEFAULT_BASE_URL, IDENTITY_DEFAULT_TTL, IDENTITY_MAX_SUB_LENGTH, IDENTITY_MAX_TTL_SECONDS, LEGACY_WEBHOOK_SECRET, LetsBot, LetsBotApiError, LetsBotClient, LetsBotConnectionError, LetsBotCrm, SDK_VERSION, TokenBucket, createIdentityToken, crmBaseUrl, crmOperations, crmResourceMap, experimental_verifyWebhookSignature, isLogicalFailure, operations, resourceMap, verifyCrmWebhookSignature };
|
|
914
|
+
//# sourceMappingURL=index.js.map
|
|
915
|
+
//# sourceMappingURL=index.js.map
|