whisp-js 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +296 -2
- package/dist/cjs/client.d.ts +164 -0
- package/dist/cjs/client.js +460 -0
- package/dist/cjs/constants.d.ts +43 -0
- package/dist/cjs/constants.js +59 -0
- package/dist/cjs/errors.d.ts +96 -0
- package/dist/cjs/errors.js +115 -0
- package/dist/cjs/index.d.ts +19 -0
- package/dist/cjs/index.js +39 -0
- package/dist/cjs/listener.d.ts +93 -0
- package/dist/cjs/listener.js +468 -0
- package/dist/cjs/media.d.ts +40 -0
- package/dist/cjs/media.js +181 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/refs.d.ts +4 -0
- package/dist/cjs/refs.js +38 -0
- package/dist/cjs/runtime.d.ts +24 -0
- package/dist/cjs/runtime.js +93 -0
- package/dist/cjs/sse.d.ts +32 -0
- package/dist/cjs/sse.js +105 -0
- package/dist/cjs/transport.d.ts +61 -0
- package/dist/cjs/transport.js +181 -0
- package/dist/cjs/types.d.ts +516 -0
- package/dist/cjs/types.js +14 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/webhook.d.ts +27 -0
- package/dist/cjs/webhook.js +128 -0
- package/dist/esm/client.d.ts +164 -0
- package/dist/esm/client.js +456 -0
- package/dist/esm/constants.d.ts +43 -0
- package/dist/esm/constants.js +56 -0
- package/dist/esm/errors.d.ts +96 -0
- package/dist/esm/errors.js +107 -0
- package/dist/esm/index.d.ts +19 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/listener.d.ts +93 -0
- package/dist/esm/listener.js +463 -0
- package/dist/esm/media.d.ts +40 -0
- package/dist/esm/media.js +175 -0
- package/dist/esm/refs.d.ts +4 -0
- package/dist/esm/refs.js +33 -0
- package/dist/esm/runtime.d.ts +24 -0
- package/dist/esm/runtime.js +84 -0
- package/dist/esm/sse.d.ts +32 -0
- package/dist/esm/sse.js +100 -0
- package/dist/esm/transport.d.ts +61 -0
- package/dist/esm/transport.js +172 -0
- package/dist/esm/types.d.ts +516 -0
- package/dist/esm/types.js +13 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/webhook.d.ts +27 -0
- package/dist/esm/webhook.js +123 -0
- package/package.json +37 -5
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Whisp = exports.MEDIA_WAIT_MAX_MS = exports.DEFAULT_MAX_RETRIES = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The `Whisp` client (phase2-design 12.2, 12.3).
|
|
6
|
+
*
|
|
7
|
+
* Nothing is called at construction. The token is read when a call needs it
|
|
8
|
+
* (`token`, else WHISP_API_TOKEN), and so is the base URL (`baseUrl`, else
|
|
9
|
+
* WHISP_API_URL, else the hosted API), so a value set after import still
|
|
10
|
+
* counts. Phones are checked at once: a typo throws here, not on the first
|
|
11
|
+
* send.
|
|
12
|
+
*
|
|
13
|
+
* An instance sends from its `phone`: one number, a pool (an array, picked
|
|
14
|
+
* server-side by `strategy`, `healthiest` unless set, sticky by default), or
|
|
15
|
+
* none, the whole project (its default number, else every number as a
|
|
16
|
+
* pool). `number(x)` returns a copy scoped to one number, which the reads
|
|
17
|
+
* that need one number require. `dailyCap` (phase3-design 12.4) goes with
|
|
18
|
+
* every send; the server counts and enforces it, never the SDK, since
|
|
19
|
+
* several app instances share one number.
|
|
20
|
+
*/
|
|
21
|
+
const constants_ts_1 = require("./constants.js");
|
|
22
|
+
const errors_ts_1 = require("./errors.js");
|
|
23
|
+
const listener_ts_1 = require("./listener.js");
|
|
24
|
+
const media_ts_1 = require("./media.js");
|
|
25
|
+
const refs_ts_1 = require("./refs.js");
|
|
26
|
+
const runtime_ts_1 = require("./runtime.js");
|
|
27
|
+
const transport_ts_1 = require("./transport.js");
|
|
28
|
+
const webhook_ts_1 = require("./webhook.js");
|
|
29
|
+
exports.DEFAULT_TIMEOUT_MS = 30_000;
|
|
30
|
+
exports.DEFAULT_MAX_RETRIES = 2;
|
|
31
|
+
/** `downloadMedia` waits for a file being prepared at most this long. */
|
|
32
|
+
exports.MEDIA_WAIT_MAX_MS = 60_000;
|
|
33
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
34
|
+
const invalidResponse = (what) => new errors_ts_1.WhispError({ code: 'INVALID_RESPONSE', message: `The API's answer to ${what} is not in the documented shape` });
|
|
35
|
+
/** The body as T when `check` holds, else INVALID_RESPONSE. The API only grows (4.1), so only what the SDK reads is checked. */
|
|
36
|
+
const expect = (body, what, check) => {
|
|
37
|
+
if (!isRecord(body) || !check(body))
|
|
38
|
+
throw invalidResponse(what);
|
|
39
|
+
return body;
|
|
40
|
+
};
|
|
41
|
+
const segment = (value) => encodeURIComponent(value);
|
|
42
|
+
const positiveInt = (value, fallback, name, min, max) => {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
46
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `${name} must be a whole number from ${min} to ${max}`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
/** A `dailyCap` as the API takes it (1 to 10,000, whole), or undefined when none was given. Checked before any call. */
|
|
51
|
+
const checkDailyCap = (value, name) => {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < constants_ts_1.DAILY_CAP_MIN || value > constants_ts_1.DAILY_CAP_MAX) {
|
|
55
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `${name} must be a whole number from ${constants_ts_1.DAILY_CAP_MIN} to 10,000`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
const checkBaseUrl = (value, source) => {
|
|
60
|
+
let protocol = '';
|
|
61
|
+
try {
|
|
62
|
+
protocol = new URL(value).protocol;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Reported below.
|
|
66
|
+
}
|
|
67
|
+
if (protocol !== 'https:' && protocol !== 'http:') {
|
|
68
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `${source} must be an http(s) URL`);
|
|
69
|
+
}
|
|
70
|
+
return value.replace(/\/+$/, '');
|
|
71
|
+
};
|
|
72
|
+
const normalizeFrom = (phone) => {
|
|
73
|
+
if (phone === undefined)
|
|
74
|
+
return null;
|
|
75
|
+
if (typeof phone === 'string')
|
|
76
|
+
return (0, refs_ts_1.requireNumberRef)(phone, 'phone');
|
|
77
|
+
if (!Array.isArray(phone))
|
|
78
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'phone must be a string or an array of strings');
|
|
79
|
+
if (phone.length === 0)
|
|
80
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'phone is an empty list; leave it out to use the whole project');
|
|
81
|
+
const refs = [...new Set(phone.map(ref => (0, refs_ts_1.requireNumberRef)(ref, 'phone')))];
|
|
82
|
+
if (refs.length > constants_ts_1.FROM_MAX)
|
|
83
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `A pool has at most ${constants_ts_1.FROM_MAX} numbers`);
|
|
84
|
+
return refs;
|
|
85
|
+
};
|
|
86
|
+
const waitMs = (wait) => wait === true ? constants_ts_1.WAIT_DEFAULT_MS : typeof wait === 'number' && Number.isFinite(wait) ? wait : 0;
|
|
87
|
+
const defaultReport = (error) => {
|
|
88
|
+
console.error(`[whisp-sdk] ${error.name}: ${error.message}`);
|
|
89
|
+
};
|
|
90
|
+
class Whisp {
|
|
91
|
+
#options;
|
|
92
|
+
#scope;
|
|
93
|
+
#timeoutMs;
|
|
94
|
+
#maxRetries;
|
|
95
|
+
#timers;
|
|
96
|
+
#listener;
|
|
97
|
+
/** Aborted by close(): retries waiting for their turn give up. Replaced, so later calls retry again. */
|
|
98
|
+
#retries = new AbortController();
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
const internal = options;
|
|
101
|
+
if ((0, runtime_ts_1.isBrowser)() && options.dangerouslyAllowBrowser !== true) {
|
|
102
|
+
throw (0, errors_ts_1.localError)('TOKEN_IN_BROWSER', 'The Whisp SDK runs on your server: in a browser, every visitor could copy the token and send from your numbers.');
|
|
103
|
+
}
|
|
104
|
+
if (options.strategy !== undefined && !constants_ts_1.SEND_STRATEGIES.includes(options.strategy)) {
|
|
105
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', "strategy must be 'healthiest', 'random' or 'round-robin'");
|
|
106
|
+
}
|
|
107
|
+
if (options.sticky !== undefined && typeof options.sticky !== 'boolean') {
|
|
108
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'sticky must be true or false');
|
|
109
|
+
}
|
|
110
|
+
checkDailyCap(options.dailyCap, 'dailyCap');
|
|
111
|
+
if (options.baseUrl !== undefined)
|
|
112
|
+
checkBaseUrl(options.baseUrl, 'baseUrl');
|
|
113
|
+
if (options.cursor !== undefined && (typeof options.cursor !== 'string' || options.cursor === '')) {
|
|
114
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'cursor must be a cursor string saved from whisp.cursor');
|
|
115
|
+
}
|
|
116
|
+
this.#options = { ...internal };
|
|
117
|
+
this.#scope = { from: normalizeFrom(options.phone) };
|
|
118
|
+
this.#timeoutMs = positiveInt(options.timeoutMs, exports.DEFAULT_TIMEOUT_MS, 'timeoutMs', 1, 600_000);
|
|
119
|
+
this.#maxRetries = positiveInt(options.maxRetries, exports.DEFAULT_MAX_RETRIES, 'maxRetries', 0, 10);
|
|
120
|
+
this.#timers = internal.timers ?? runtime_ts_1.realTimers;
|
|
121
|
+
const numbers = this.#scope.from === null ? null : [this.#scope.from].flat();
|
|
122
|
+
this.#listener = new listener_ts_1.Listener({
|
|
123
|
+
resolve: () => this.#target(),
|
|
124
|
+
numbers,
|
|
125
|
+
cursor: options.cursor ?? null,
|
|
126
|
+
connectTimeoutMs: this.#timeoutMs,
|
|
127
|
+
timers: this.#timers,
|
|
128
|
+
report: internal.report ?? defaultReport,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The cursor after the last event delivered to handlers (6.5): save it and
|
|
133
|
+
* pass it as `cursor` to resume after a restart. Not an event's `evt_` id,
|
|
134
|
+
* which is `event.event.id` in a handler. Before the first event it is
|
|
135
|
+
* where the stream started (its `ready`), so a save then resumes from
|
|
136
|
+
* there; null until the stream first opens, unless `cursor` was given
|
|
137
|
+
* (sdk-reconnect-without-cursor-loses-events).
|
|
138
|
+
*/
|
|
139
|
+
get cursor() {
|
|
140
|
+
return this.#listener.cursor;
|
|
141
|
+
}
|
|
142
|
+
/** A copy that sends from, reads and listens on this one number (a phone or a `num_` id). */
|
|
143
|
+
number(phoneOrId) {
|
|
144
|
+
const ref = (0, refs_ts_1.requireNumberRef)(phoneOrId, 'number()');
|
|
145
|
+
return new Whisp({ ...this.#options, phone: ref });
|
|
146
|
+
}
|
|
147
|
+
// Connection ---------------------------------------------------------------------------------
|
|
148
|
+
#fetch() {
|
|
149
|
+
const custom = this.#options.fetch;
|
|
150
|
+
// Looked up per call, so a fetch installed after import is used; called unbound, or window.fetch throws.
|
|
151
|
+
return custom ? (input, init) => custom(input, init) : (input, init) => globalThis.fetch(input, init);
|
|
152
|
+
}
|
|
153
|
+
#target() {
|
|
154
|
+
const token = this.#options.token?.trim() || (0, runtime_ts_1.readEnv)(constants_ts_1.API_TOKEN_ENV);
|
|
155
|
+
if (!token) {
|
|
156
|
+
throw (0, errors_ts_1.localError)('TOKEN_MISSING', `No token: pass { token } or set ${constants_ts_1.API_TOKEN_ENV} (the project's API & SDK tab)`);
|
|
157
|
+
}
|
|
158
|
+
if (!constants_ts_1.TOKEN_PATTERN.test(token)) {
|
|
159
|
+
throw (0, errors_ts_1.localError)('TOKEN_INVALID', "The token is not a Whisp project token (wsp_live_…). Copy it again from the project's API & SDK tab.");
|
|
160
|
+
}
|
|
161
|
+
const baseUrl = this.#options.baseUrl ?? (0, runtime_ts_1.readEnv)(constants_ts_1.API_URL_ENV) ?? constants_ts_1.DEFAULT_BASE_URL;
|
|
162
|
+
return {
|
|
163
|
+
token,
|
|
164
|
+
baseUrl: checkBaseUrl(baseUrl, this.#options.baseUrl !== undefined ? 'baseUrl' : constants_ts_1.API_URL_ENV),
|
|
165
|
+
fetch: this.#fetch(),
|
|
166
|
+
userAgent: (0, runtime_ts_1.userAgent)(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
#transport() {
|
|
170
|
+
return {
|
|
171
|
+
...this.#target(),
|
|
172
|
+
timeoutMs: this.#timeoutMs,
|
|
173
|
+
maxRetries: this.#maxRetries,
|
|
174
|
+
timers: this.#timers,
|
|
175
|
+
retrySignal: this.#retries.signal,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async #call(call) {
|
|
179
|
+
return (await (0, transport_ts_1.request)(this.#transport(), call)).body;
|
|
180
|
+
}
|
|
181
|
+
#single(method) {
|
|
182
|
+
const { from } = this.#scope;
|
|
183
|
+
const single = typeof from === 'string' ? from : from?.length === 1 ? from[0] : undefined;
|
|
184
|
+
if (single === undefined) {
|
|
185
|
+
throw (0, errors_ts_1.localError)('NUMBER_REQUIRED', `${method}() reads one number's data: call it on whisp.number('<phone or num_ id>')`);
|
|
186
|
+
}
|
|
187
|
+
return single;
|
|
188
|
+
}
|
|
189
|
+
// Project -------------------------------------------------------------------------------------
|
|
190
|
+
/**
|
|
191
|
+
* Checks the token and the instance's numbers (GET /project, then GET
|
|
192
|
+
* /numbers): throws NUMBER_NOT_IN_PROJECT listing every phone or id that
|
|
193
|
+
* isn't the token's project's. Optional: the first send or listen reports
|
|
194
|
+
* the same.
|
|
195
|
+
*/
|
|
196
|
+
async verify() {
|
|
197
|
+
const info = expect(await this.#call({ method: 'GET', path: '/project' }), 'GET /project', body => isRecord(body.project) && isRecord(body.token));
|
|
198
|
+
const numbers = await this.numbers();
|
|
199
|
+
const { from } = this.#scope;
|
|
200
|
+
const missing = (from === null ? [] : [from].flat()).filter(ref => !numbers.some(number => number.id === ref || number.phone === ref));
|
|
201
|
+
if (missing.length > 0) {
|
|
202
|
+
throw new errors_ts_1.WhispError({
|
|
203
|
+
code: 'NUMBER_NOT_IN_PROJECT',
|
|
204
|
+
message: `${missing.join(', ')} ${missing.length === 1 ? 'is not a number' : 'are not numbers'} of this project`,
|
|
205
|
+
params: { number: missing.join(',') },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { ...info, numbers };
|
|
209
|
+
}
|
|
210
|
+
// Sending -------------------------------------------------------------------------------------
|
|
211
|
+
/** Sends text (`POST /messages`) from the instance's number, pool or project. Resolves the message (`pending`, or its status after `wait`). */
|
|
212
|
+
async send(to, text, options = {}) {
|
|
213
|
+
// Async, so a bad option rejects like every other failure instead of throwing from the call.
|
|
214
|
+
return this.#send({ to, type: 'text', text, ...this.#common(options) }, options);
|
|
215
|
+
}
|
|
216
|
+
sendImage(to, media, options = {}) {
|
|
217
|
+
return this.#sendFile('image', to, media, options, options.caption);
|
|
218
|
+
}
|
|
219
|
+
sendVideo(to, media, options = {}) {
|
|
220
|
+
return this.#sendFile('video', to, media, options, options.caption);
|
|
221
|
+
}
|
|
222
|
+
sendDocument(to, media, options = {}) {
|
|
223
|
+
return this.#sendFile('document', to, media, options, options.caption);
|
|
224
|
+
}
|
|
225
|
+
sendVoice(to, media, options = {}) {
|
|
226
|
+
return this.#sendFile('voice', to, media, options, undefined);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* `POST /messages` with a body you build (5.6). The instance's `from`,
|
|
230
|
+
* `strategy`, `sticky` and `dailyCap` fill in what the body leaves out.
|
|
231
|
+
*/
|
|
232
|
+
sendMessage(body, options = {}) {
|
|
233
|
+
const defaults = { ...this.#pool(), ...this.#cap(undefined) };
|
|
234
|
+
const filled = { ...body };
|
|
235
|
+
for (const [key, value] of Object.entries(defaults))
|
|
236
|
+
if (filled[key] === undefined)
|
|
237
|
+
filled[key] = value;
|
|
238
|
+
return this.#send(filled, { ...options, wait: body.wait });
|
|
239
|
+
}
|
|
240
|
+
/** `from`, `strategy` and `sticky` for this instance; strategy and sticky only where a pool picks. */
|
|
241
|
+
#pool() {
|
|
242
|
+
const { from } = this.#scope;
|
|
243
|
+
const out = {};
|
|
244
|
+
if (from !== null)
|
|
245
|
+
out.from = from;
|
|
246
|
+
if (from === null || Array.isArray(from)) {
|
|
247
|
+
if (this.#options.strategy !== undefined)
|
|
248
|
+
out.strategy = this.#options.strategy;
|
|
249
|
+
if (this.#options.sticky !== undefined)
|
|
250
|
+
out.sticky = this.#options.sticky;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** `dailyCap` for one send: the call's, else the instance's; none when neither set one. */
|
|
255
|
+
#cap(perCall) {
|
|
256
|
+
const cap = checkDailyCap(perCall, 'dailyCap') ?? this.#options.dailyCap;
|
|
257
|
+
return cap === undefined ? {} : { dailyCap: cap };
|
|
258
|
+
}
|
|
259
|
+
#common(options) {
|
|
260
|
+
const out = { ...this.#pool(), ...this.#cap(options.dailyCap) };
|
|
261
|
+
if (options.replyTo !== undefined)
|
|
262
|
+
out.replyTo = options.replyTo;
|
|
263
|
+
if (options.reference !== undefined)
|
|
264
|
+
out.reference = options.reference;
|
|
265
|
+
if (options.wait !== undefined)
|
|
266
|
+
out.wait = options.wait;
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
async #send(body, options) {
|
|
270
|
+
const idempotencyKey = options.idempotencyKey ?? (0, runtime_ts_1.randomId)();
|
|
271
|
+
// The server holds a `wait` send open up to 20 s; the attempt's timeout leaves room for it.
|
|
272
|
+
const wait = waitMs(options.wait);
|
|
273
|
+
const answer = await this.#call({
|
|
274
|
+
method: 'POST',
|
|
275
|
+
path: '/messages',
|
|
276
|
+
body,
|
|
277
|
+
idempotencyKey,
|
|
278
|
+
...(wait > 0 ? { timeoutMs: Math.max(this.#timeoutMs, wait + 10_000) } : {}),
|
|
279
|
+
});
|
|
280
|
+
return expect(answer, 'POST /messages', message => typeof message.id === 'string');
|
|
281
|
+
}
|
|
282
|
+
async #sendFile(kind, to, media, options, caption) {
|
|
283
|
+
// The options first: a bad one fails before any file is uploaded.
|
|
284
|
+
const common = this.#common(options);
|
|
285
|
+
let input;
|
|
286
|
+
if (isRecord(media) && typeof media.url === 'string') {
|
|
287
|
+
input = { url: media.url };
|
|
288
|
+
if (typeof media.fileName === 'string')
|
|
289
|
+
input.fileName = media.fileName;
|
|
290
|
+
if (typeof media.mime === 'string')
|
|
291
|
+
input.mime = media.mime;
|
|
292
|
+
}
|
|
293
|
+
else if (isRecord(media) && 'file' in media && media.file !== undefined && media.file !== null) {
|
|
294
|
+
input = { uploadId: await this.#upload(kind, media) };
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
throw (0, errors_ts_1.localError)('MEDIA_INVALID', `send${kind[0]?.toUpperCase()}${kind.slice(1)}() takes { url } or { file }`);
|
|
298
|
+
}
|
|
299
|
+
const body = { to, type: kind, media: input, ...common };
|
|
300
|
+
if (caption !== undefined)
|
|
301
|
+
body.caption = caption;
|
|
302
|
+
return this.#send(body, options);
|
|
303
|
+
}
|
|
304
|
+
/** `POST /uploads`, then the PUT to storage; resolves the upload id a send takes. */
|
|
305
|
+
async #upload(kind, media) {
|
|
306
|
+
const file = await (0, media_ts_1.prepareFile)(kind, media);
|
|
307
|
+
const upload = expect(await this.#call({
|
|
308
|
+
method: 'POST',
|
|
309
|
+
path: '/uploads',
|
|
310
|
+
body: { kind, mime: file.mime, size: file.size, name: file.name },
|
|
311
|
+
}), 'POST /uploads', body => typeof body.uploadId === 'string' && typeof body.url === 'string' && isRecord(body.headers));
|
|
312
|
+
await (0, media_ts_1.putUpload)({
|
|
313
|
+
fetch: this.#fetch(),
|
|
314
|
+
timers: this.#timers,
|
|
315
|
+
timeoutMs: this.#timeoutMs,
|
|
316
|
+
maxRetries: this.#maxRetries,
|
|
317
|
+
retrySignal: this.#retries.signal,
|
|
318
|
+
}, upload.url, upload.headers, file);
|
|
319
|
+
return upload.uploadId;
|
|
320
|
+
}
|
|
321
|
+
// Reads ---------------------------------------------------------------------------------------
|
|
322
|
+
/** Every number of the project, any status. */
|
|
323
|
+
async numbers() {
|
|
324
|
+
const list = expect(await this.#call({ method: 'GET', path: '/numbers' }), 'GET /numbers', body => Array.isArray(body.numbers));
|
|
325
|
+
return list.numbers;
|
|
326
|
+
}
|
|
327
|
+
/** One message of the project's numbers; 404 MESSAGE_NOT_FOUND otherwise. */
|
|
328
|
+
async message(messageId) {
|
|
329
|
+
return expect(await this.#call({ method: 'GET', path: `/messages/${segment(messageId)}` }), 'GET /messages/:messageId', body => typeof body.id === 'string');
|
|
330
|
+
}
|
|
331
|
+
/** What earlier sends with this `reference` made, newest first, at most 50. */
|
|
332
|
+
async messages(query) {
|
|
333
|
+
const list = expect(await this.#call({ method: 'GET', path: '/messages', query: { reference: query.reference } }), 'GET /messages', body => Array.isArray(body.messages));
|
|
334
|
+
return list.messages;
|
|
335
|
+
}
|
|
336
|
+
/** Is this phone on WhatsApp, asked through the instance's number. */
|
|
337
|
+
async check(phone) {
|
|
338
|
+
const number = this.#single('check');
|
|
339
|
+
return expect(await this.#call({ method: 'GET', path: `/numbers/${segment(number)}/check/${segment(phone)}` }), 'GET /numbers/:number/check/:phone', body => typeof body.exists === 'boolean');
|
|
340
|
+
}
|
|
341
|
+
/** The number's chats, newest activity first. */
|
|
342
|
+
chats(query = {}) {
|
|
343
|
+
const number = this.#single('chats');
|
|
344
|
+
const fetchPage = async (cursor) => {
|
|
345
|
+
const list = expect(await this.#call({
|
|
346
|
+
method: 'GET',
|
|
347
|
+
path: `/numbers/${segment(number)}/chats`,
|
|
348
|
+
query: { ...query, ...(cursor !== undefined ? { cursor } : {}) },
|
|
349
|
+
}), 'GET /numbers/:number/chats', body => Array.isArray(body.chats));
|
|
350
|
+
return { items: list.chats, nextCursor: list.nextCursor ?? null };
|
|
351
|
+
};
|
|
352
|
+
return paginate(fetchPage);
|
|
353
|
+
}
|
|
354
|
+
/** One chat: a phone, a `cht_` id or a group JID (a merged-away id resolves to the chat it joined). */
|
|
355
|
+
async chat(chat) {
|
|
356
|
+
const number = this.#single('chat');
|
|
357
|
+
return expect(await this.#call({ method: 'GET', path: `/numbers/${segment(number)}/chats/${segment(chat)}` }), 'GET /numbers/:number/chats/:chat', body => typeof body.id === 'string');
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* A chat's messages, newest first, history included. Each next page is
|
|
361
|
+
* older (`nextCursor` goes in `before`); after an `after` request, each next
|
|
362
|
+
* page is newer (`nextCursor` goes in `after`).
|
|
363
|
+
*/
|
|
364
|
+
chatMessages(chat, query = {}) {
|
|
365
|
+
const number = this.#single('chatMessages');
|
|
366
|
+
const direction = query.after !== undefined && query.before === undefined ? 'after' : 'before';
|
|
367
|
+
const fetchPage = async (cursor) => {
|
|
368
|
+
const list = expect(await this.#call({
|
|
369
|
+
method: 'GET',
|
|
370
|
+
path: `/numbers/${segment(number)}/chats/${segment(chat)}/messages`,
|
|
371
|
+
query: { ...query, ...(cursor !== undefined ? { [direction]: cursor } : {}) },
|
|
372
|
+
}), 'GET /numbers/:number/chats/:chat/messages', body => Array.isArray(body.messages));
|
|
373
|
+
return { items: list.messages, nextCursor: list.nextCursor ?? null };
|
|
374
|
+
};
|
|
375
|
+
return paginate(fetchPage);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A message's file as a fetch `Response` (12.3). Asks the API for a
|
|
379
|
+
* 5-minute link with the token, then fetches the link with no token and no
|
|
380
|
+
* redirects. A file still being prepared (409 MEDIA_NOT_READY with
|
|
381
|
+
* Retry-After) is waited for, up to 60 s.
|
|
382
|
+
*/
|
|
383
|
+
async downloadMedia(message, options = {}) {
|
|
384
|
+
const id = typeof message === 'string' ? message : message?.id;
|
|
385
|
+
if (typeof id !== 'string' || id === '')
|
|
386
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'downloadMedia() takes a message or its id');
|
|
387
|
+
const started = this.#timers.now();
|
|
388
|
+
let link;
|
|
389
|
+
for (;;) {
|
|
390
|
+
try {
|
|
391
|
+
link = expect(await this.#call({
|
|
392
|
+
method: 'GET',
|
|
393
|
+
path: `/messages/${segment(id)}/media`,
|
|
394
|
+
query: { redirect: '0', variant: options.variant },
|
|
395
|
+
}), 'GET /messages/:messageId/media', body => typeof body.url === 'string');
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
const pending = error instanceof errors_ts_1.WhispError && error.code === 'MEDIA_NOT_READY' && error.retryAfter !== null;
|
|
400
|
+
const waitFor = pending ? Math.max(1, error.retryAfter) * 1_000 : 0;
|
|
401
|
+
if (!pending || this.#timers.now() - started + waitFor > exports.MEDIA_WAIT_MAX_MS)
|
|
402
|
+
throw error;
|
|
403
|
+
await (0, runtime_ts_1.sleep)(this.#timers, waitFor, this.#retries.signal);
|
|
404
|
+
if (this.#retries.signal.aborted)
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
let response;
|
|
409
|
+
try {
|
|
410
|
+
// A new request: no Authorization, and a redirect is an error rather than a hop.
|
|
411
|
+
response = await this.#fetch()(link.url, { method: 'GET', redirect: 'error' });
|
|
412
|
+
}
|
|
413
|
+
catch (cause) {
|
|
414
|
+
throw new errors_ts_1.WhispError({ code: 'MEDIA_DOWNLOAD_FAILED', message: 'The file link could not be fetched', cause });
|
|
415
|
+
}
|
|
416
|
+
if (!response.ok) {
|
|
417
|
+
await response.body?.cancel().catch(() => { });
|
|
418
|
+
throw new errors_ts_1.WhispError({
|
|
419
|
+
code: 'MEDIA_DOWNLOAD_FAILED',
|
|
420
|
+
status: response.status,
|
|
421
|
+
message: `The file link answered ${response.status}`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return response;
|
|
425
|
+
}
|
|
426
|
+
on(event, handler) {
|
|
427
|
+
if (typeof handler !== 'function')
|
|
428
|
+
throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `on('${String(event)}') needs a function`);
|
|
429
|
+
this.#listener.add(event, handler);
|
|
430
|
+
return () => this.#listener.remove(event, handler);
|
|
431
|
+
}
|
|
432
|
+
off(event, handler) {
|
|
433
|
+
this.#listener.remove(event, handler);
|
|
434
|
+
}
|
|
435
|
+
/** Ends the stream, forgets every handler, and stops retries still waiting for their turn. */
|
|
436
|
+
close() {
|
|
437
|
+
this.#listener.close();
|
|
438
|
+
this.#retries.abort();
|
|
439
|
+
this.#retries = new AbortController();
|
|
440
|
+
}
|
|
441
|
+
/** The standalone `verifyWebhook`, for convenience. */
|
|
442
|
+
verifyWebhook(input) {
|
|
443
|
+
return (0, webhook_ts_1.verifyWebhook)(input);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
exports.Whisp = Whisp;
|
|
447
|
+
/** A first page fetched at once, awaitable, and iterable across every page. */
|
|
448
|
+
const paginate = (fetchPage) => {
|
|
449
|
+
const first = fetchPage(undefined);
|
|
450
|
+
const iterate = async function* () {
|
|
451
|
+
let page = await first;
|
|
452
|
+
for (;;) {
|
|
453
|
+
yield* page.items;
|
|
454
|
+
if (page.nextCursor === null || page.items.length === 0)
|
|
455
|
+
return;
|
|
456
|
+
page = await fetchPage(page.nextCursor);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
return Object.assign(first, { [Symbol.asyncIterator]: iterate });
|
|
460
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The npm name (16.2 decision 1: `@whisphq/sdk` if the `@whisp` scope isn't the owner's). */
|
|
2
|
+
export declare const SDK_PACKAGE_NAME = "whisp-js";
|
|
3
|
+
/** Where the token is read from when none is passed, on the first call. */
|
|
4
|
+
export declare const API_TOKEN_ENV = "WHISP_API_TOKEN";
|
|
5
|
+
/** Where a different base URL is read from (agent stacks, local tests). */
|
|
6
|
+
export declare const API_URL_ENV = "WHISP_API_URL";
|
|
7
|
+
export declare const DEFAULT_BASE_URL = "https://api.whisp.lue.my";
|
|
8
|
+
/** `whisp-sdk/<version> (<runtime>/<version>)`. */
|
|
9
|
+
export declare const USER_AGENT_PREFIX = "whisp-sdk";
|
|
10
|
+
/** The webhook signature header, lowercase as HTTP header names compare. */
|
|
11
|
+
export declare const SIGNATURE_HEADER = "x-whisp-signature";
|
|
12
|
+
/** Every project token starts with it. */
|
|
13
|
+
export declare const TOKEN_PREFIX = "wsp_live_";
|
|
14
|
+
/** Every webhook signing secret starts with it. */
|
|
15
|
+
export declare const WEBHOOK_SECRET_PREFIX = "wsp_whs_";
|
|
16
|
+
/** An event cursor (the SSE `id:`) starts with it; the rest is opaque. */
|
|
17
|
+
export declare const EVENT_CURSOR_PREFIX = "c_";
|
|
18
|
+
/** How old a webhook signature's timestamp may be, by default (seconds). */
|
|
19
|
+
export declare const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
20
|
+
/** A project token's whole shape: the prefix, 32 base62 and a 6-character checksum (core's `TOKEN_PATTERN`). */
|
|
21
|
+
export declare const TOKEN_PATTERN: RegExp;
|
|
22
|
+
/** A webhook signing secret's whole shape (core's `WEBHOOK_SECRET_PATTERN`). */
|
|
23
|
+
export declare const WEBHOOK_SECRET_PATTERN: RegExp;
|
|
24
|
+
/** A number id: `num_` and 8 characters of core's base58 id alphabet (no 0, O, I or l). */
|
|
25
|
+
export declare const NUMBER_ID_PATTERN: RegExp;
|
|
26
|
+
/** At most this many numbers in one pool (`from`, PUBLIC_LIMITS.fromMax). */
|
|
27
|
+
export declare const FROM_MAX = 20;
|
|
28
|
+
/** `wait: true` waits this long on the server (PUBLIC_LIMITS.waitDefaultMs). */
|
|
29
|
+
export declare const WAIT_DEFAULT_MS = 15000;
|
|
30
|
+
/** How a pool picks its number (core SEND_STRATEGIES); the first, `healthiest`, is the server's default. */
|
|
31
|
+
export declare const SEND_STRATEGIES: readonly ["healthiest", "random", "round-robin"];
|
|
32
|
+
/** A send's own `dailyCap`: a whole number in this range (core SEND_DAILY_CAP). */
|
|
33
|
+
export declare const DAILY_CAP_MIN = 1;
|
|
34
|
+
export declare const DAILY_CAP_MAX = 10000;
|
|
35
|
+
/** The upload cap of each kind (core's `uploadMaxBytes`); a stream is never read past it. */
|
|
36
|
+
export declare const UPLOAD_MAX_BYTES: {
|
|
37
|
+
readonly image: number;
|
|
38
|
+
readonly video: number;
|
|
39
|
+
readonly voice: number;
|
|
40
|
+
readonly document: number;
|
|
41
|
+
};
|
|
42
|
+
/** PLAN's twelve, in the order the API lists them. */
|
|
43
|
+
export declare const PUBLIC_EVENT_TYPES: readonly ["message.received", "message.sent", "message.status", "message.reaction", "message.deleted", "chat.updated", "presence.updated", "group.updated", "group.participants", "number.status", "number.risk", "call.received"];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUBLIC_EVENT_TYPES = exports.UPLOAD_MAX_BYTES = exports.DAILY_CAP_MAX = exports.DAILY_CAP_MIN = exports.SEND_STRATEGIES = exports.WAIT_DEFAULT_MS = exports.FROM_MAX = exports.NUMBER_ID_PATTERN = exports.WEBHOOK_SECRET_PATTERN = exports.TOKEN_PATTERN = exports.SIGNATURE_TOLERANCE_SECONDS = exports.EVENT_CURSOR_PREFIX = exports.WEBHOOK_SECRET_PREFIX = exports.TOKEN_PREFIX = exports.SIGNATURE_HEADER = exports.USER_AGENT_PREFIX = exports.DEFAULT_BASE_URL = exports.API_URL_ENV = exports.API_TOKEN_ENV = exports.SDK_PACKAGE_NAME = void 0;
|
|
4
|
+
/** The npm name (16.2 decision 1: `@whisphq/sdk` if the `@whisp` scope isn't the owner's). */
|
|
5
|
+
exports.SDK_PACKAGE_NAME = 'whisp-js';
|
|
6
|
+
/** Where the token is read from when none is passed, on the first call. */
|
|
7
|
+
exports.API_TOKEN_ENV = 'WHISP_API_TOKEN';
|
|
8
|
+
/** Where a different base URL is read from (agent stacks, local tests). */
|
|
9
|
+
exports.API_URL_ENV = 'WHISP_API_URL';
|
|
10
|
+
exports.DEFAULT_BASE_URL = 'https://api.whisp.lue.my';
|
|
11
|
+
/** `whisp-sdk/<version> (<runtime>/<version>)`. */
|
|
12
|
+
exports.USER_AGENT_PREFIX = 'whisp-sdk';
|
|
13
|
+
/** The webhook signature header, lowercase as HTTP header names compare. */
|
|
14
|
+
exports.SIGNATURE_HEADER = 'x-whisp-signature';
|
|
15
|
+
/** Every project token starts with it. */
|
|
16
|
+
exports.TOKEN_PREFIX = 'wsp_live_';
|
|
17
|
+
/** Every webhook signing secret starts with it. */
|
|
18
|
+
exports.WEBHOOK_SECRET_PREFIX = 'wsp_whs_';
|
|
19
|
+
/** An event cursor (the SSE `id:`) starts with it; the rest is opaque. */
|
|
20
|
+
exports.EVENT_CURSOR_PREFIX = 'c_';
|
|
21
|
+
/** How old a webhook signature's timestamp may be, by default (seconds). */
|
|
22
|
+
exports.SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
23
|
+
/** A project token's whole shape: the prefix, 32 base62 and a 6-character checksum (core's `TOKEN_PATTERN`). */
|
|
24
|
+
exports.TOKEN_PATTERN = /^wsp_live_[0-9A-Za-z]{38}$/;
|
|
25
|
+
/** A webhook signing secret's whole shape (core's `WEBHOOK_SECRET_PATTERN`). */
|
|
26
|
+
exports.WEBHOOK_SECRET_PATTERN = /^wsp_whs_[0-9A-Za-z]{38}$/;
|
|
27
|
+
/** A number id: `num_` and 8 characters of core's base58 id alphabet (no 0, O, I or l). */
|
|
28
|
+
exports.NUMBER_ID_PATTERN = /^num_[1-9A-HJ-NP-Za-km-z]{8}$/;
|
|
29
|
+
/** At most this many numbers in one pool (`from`, PUBLIC_LIMITS.fromMax). */
|
|
30
|
+
exports.FROM_MAX = 20;
|
|
31
|
+
/** `wait: true` waits this long on the server (PUBLIC_LIMITS.waitDefaultMs). */
|
|
32
|
+
exports.WAIT_DEFAULT_MS = 15_000;
|
|
33
|
+
/** How a pool picks its number (core SEND_STRATEGIES); the first, `healthiest`, is the server's default. */
|
|
34
|
+
exports.SEND_STRATEGIES = ['healthiest', 'random', 'round-robin'];
|
|
35
|
+
/** A send's own `dailyCap`: a whole number in this range (core SEND_DAILY_CAP). */
|
|
36
|
+
exports.DAILY_CAP_MIN = 1;
|
|
37
|
+
exports.DAILY_CAP_MAX = 10_000;
|
|
38
|
+
/** The upload cap of each kind (core's `uploadMaxBytes`); a stream is never read past it. */
|
|
39
|
+
exports.UPLOAD_MAX_BYTES = {
|
|
40
|
+
image: 16 * 1024 * 1024,
|
|
41
|
+
video: 64 * 1024 * 1024,
|
|
42
|
+
voice: 16 * 1024 * 1024,
|
|
43
|
+
document: 100 * 1024 * 1024,
|
|
44
|
+
};
|
|
45
|
+
/** PLAN's twelve, in the order the API lists them. */
|
|
46
|
+
exports.PUBLIC_EVENT_TYPES = [
|
|
47
|
+
'message.received',
|
|
48
|
+
'message.sent',
|
|
49
|
+
'message.status',
|
|
50
|
+
'message.reaction',
|
|
51
|
+
'message.deleted',
|
|
52
|
+
'chat.updated',
|
|
53
|
+
'presence.updated',
|
|
54
|
+
'group.updated',
|
|
55
|
+
'group.participants',
|
|
56
|
+
'number.status',
|
|
57
|
+
'number.risk',
|
|
58
|
+
'call.received',
|
|
59
|
+
];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's errors (phase2-design 12.3, 12.5). Every one is a `WhispError`,
|
|
3
|
+
* so one `instanceof` check catches them all and `code` says what happened:
|
|
4
|
+
*
|
|
5
|
+
* - `WhispError`: the API answered with an error (`status` is the HTTP
|
|
6
|
+
* status, `code` the API's stable code), or a local check failed before
|
|
7
|
+
* anything was sent (`status` is null).
|
|
8
|
+
* - `WhispConnectionError`: no HTTP answer after every retry (network error
|
|
9
|
+
* or timeout).
|
|
10
|
+
* - `WhispSignatureError`: `verifyWebhook` refused a request.
|
|
11
|
+
*
|
|
12
|
+
* Messages are English and safe to log: they never contain the token or a
|
|
13
|
+
* webhook secret.
|
|
14
|
+
*/
|
|
15
|
+
/** Codes the SDK makes itself. Everything else comes from the API as it sent it. */
|
|
16
|
+
export declare const SDK_ERROR_CODES: {
|
|
17
|
+
/** No `token` was passed and WHISP_API_TOKEN is not set. Nothing was sent. */
|
|
18
|
+
readonly TOKEN_MISSING: 'TOKEN_MISSING';
|
|
19
|
+
/** The token isn't shaped like a project token (`wsp_live_…`). Nothing was sent. The API uses the same code. */
|
|
20
|
+
readonly TOKEN_INVALID: 'TOKEN_INVALID';
|
|
21
|
+
/** The SDK was created in a browser without `dangerouslyAllowBrowser`. The API uses the same code. */
|
|
22
|
+
readonly TOKEN_IN_BROWSER: 'TOKEN_IN_BROWSER';
|
|
23
|
+
/** A `phone` (or `number(...)`) that is neither a phone in international form nor a `num_` id. */
|
|
24
|
+
readonly PHONE_INVALID: 'PHONE_INVALID';
|
|
25
|
+
/** A read that needs one number (`chats`, `chat`, `chatMessages`, `check`) on a pool or whole-project instance. */
|
|
26
|
+
readonly NUMBER_REQUIRED: 'NUMBER_REQUIRED';
|
|
27
|
+
/** An option or argument the SDK can tell is wrong without asking the API (`baseUrl`, `timeoutMs`, ...). */
|
|
28
|
+
readonly OPTIONS_INVALID: 'OPTIONS_INVALID';
|
|
29
|
+
/** A file to send that can't be uploaded as given (no bytes, no type, too big). Nothing was sent. */
|
|
30
|
+
readonly MEDIA_INVALID: 'MEDIA_INVALID';
|
|
31
|
+
/** The presigned upload (the PUT to storage) failed. */
|
|
32
|
+
readonly UPLOAD_FAILED: 'UPLOAD_FAILED';
|
|
33
|
+
/** The signed file link answered an error (`downloadMedia`). */
|
|
34
|
+
readonly MEDIA_DOWNLOAD_FAILED: 'MEDIA_DOWNLOAD_FAILED';
|
|
35
|
+
/** A 2xx whose body wasn't what the API documents, or a redirect the SDK refused to follow. */
|
|
36
|
+
readonly INVALID_RESPONSE: 'INVALID_RESPONSE';
|
|
37
|
+
/** No HTTP answer: DNS, connection refused or reset, TLS. */
|
|
38
|
+
readonly NETWORK_ERROR: 'NETWORK_ERROR';
|
|
39
|
+
/** An attempt ran past `timeoutMs`, or the event stream sent nothing for 45 s. */
|
|
40
|
+
readonly TIMEOUT: 'TIMEOUT';
|
|
41
|
+
/** `verifyWebhook` got no secret (an unset env variable, usually). */
|
|
42
|
+
readonly SECRET_MISSING: 'SECRET_MISSING';
|
|
43
|
+
/** `verifyWebhook` got a secret that isn't a webhook signing secret (`wsp_whs_…`). */
|
|
44
|
+
readonly SECRET_INVALID: 'SECRET_INVALID';
|
|
45
|
+
/** `verifyWebhook` refused the request; `reason` says why. */
|
|
46
|
+
readonly SIGNATURE_INVALID: 'SIGNATURE_INVALID';
|
|
47
|
+
/** This runtime has no Web Crypto (`crypto.subtle`), which `verifyWebhook` needs. */
|
|
48
|
+
readonly CRYPTO_UNAVAILABLE: 'CRYPTO_UNAVAILABLE';
|
|
49
|
+
};
|
|
50
|
+
export type SdkErrorCode = (typeof SDK_ERROR_CODES)[keyof typeof SDK_ERROR_CODES];
|
|
51
|
+
export type ErrorParams = Record<string, string | number>;
|
|
52
|
+
export interface WhispErrorInit {
|
|
53
|
+
code: string;
|
|
54
|
+
message: string;
|
|
55
|
+
/** The HTTP status when the API answered; null for local checks and connection failures. */
|
|
56
|
+
status?: number | null;
|
|
57
|
+
params?: ErrorParams | null;
|
|
58
|
+
/** The API's `X-Request-Id`, for support. */
|
|
59
|
+
requestId?: string | null;
|
|
60
|
+
/** Seconds from `Retry-After`, when the API sent one. */
|
|
61
|
+
retryAfter?: number | null;
|
|
62
|
+
cause?: unknown;
|
|
63
|
+
}
|
|
64
|
+
export declare class WhispError extends Error {
|
|
65
|
+
readonly name: string;
|
|
66
|
+
/** Stable: `NUMBER_NOT_IN_PROJECT`, `RATE_LIMITED`, `TOKEN_MISSING`, ... */
|
|
67
|
+
readonly code: string;
|
|
68
|
+
/** The HTTP status when the API answered; null when nothing was answered. */
|
|
69
|
+
readonly status: number | null;
|
|
70
|
+
/** The values the code names (`number`, `total`, `reason`, ...), or null. */
|
|
71
|
+
readonly params: ErrorParams | null;
|
|
72
|
+
readonly requestId: string | null;
|
|
73
|
+
/** Seconds the API asked to wait (`Retry-After`), or null. */
|
|
74
|
+
readonly retryAfter: number | null;
|
|
75
|
+
constructor(init: WhispErrorInit);
|
|
76
|
+
}
|
|
77
|
+
/** No HTTP answer after every retry. `cause` is the runtime's own error. */
|
|
78
|
+
export declare class WhispConnectionError extends WhispError {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
constructor(code: 'NETWORK_ERROR' | 'TIMEOUT', message: string, cause?: unknown);
|
|
81
|
+
}
|
|
82
|
+
/** Why `verifyWebhook` refused a request (12.5). */
|
|
83
|
+
export type SignatureFailureReason = 'missing' | 'malformed' | 'timestamp' | 'mismatch';
|
|
84
|
+
export declare class WhispSignatureError extends WhispError {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly reason: SignatureFailureReason;
|
|
87
|
+
constructor(reason: SignatureFailureReason, message?: string);
|
|
88
|
+
}
|
|
89
|
+
/** `{ code, message, params? }` out of an API body (5.12), or null when it isn't one. */
|
|
90
|
+
export declare const readApiError: (body: unknown) => {
|
|
91
|
+
code: string;
|
|
92
|
+
message: string;
|
|
93
|
+
params: ErrorParams | null;
|
|
94
|
+
} | null;
|
|
95
|
+
/** A local check failed; nothing was sent. */
|
|
96
|
+
export declare const localError: (code: SdkErrorCode, message: string, params?: ErrorParams) => WhispError;
|