@riseonly/sdk 0.1.1-next.0 → 0.1.2

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/README.md CHANGED
@@ -15,14 +15,13 @@ import { RiseonlyBot } from '@riseonly/sdk';
15
15
 
16
16
  const bot = new RiseonlyBot(process.env.BOT_TOKEN, { polling: true });
17
17
 
18
- bot.on('message', async (message) => {
19
- if (message.text === '/start') {
20
- await bot.sendMessage({
21
- chat_id: message.chat.id,
22
- text: 'welcome to riseonly',
23
- });
24
- }
18
+ bot.command('start', async (message) => {
19
+ await bot.reply(message, 'Welcome to Riseonly');
25
20
  });
21
+
22
+ bot.hears(/hello/i, (message) => bot.reply(message, 'Hi!'));
23
+ bot.action(/^track:/, (query) => bot.answer(query, 'Loading track…'));
24
+ bot.catch((error) => console.error('Bot handler failed', error));
26
25
  ```
27
26
 
28
27
  ## Bot API client
@@ -37,7 +36,8 @@ const api = new ApiClient(process.env.BOT_TOKEN, {
37
36
  });
38
37
 
39
38
  const me = await api.getMe();
40
- await api.sendMessage({ chat_id: me.id, text: 'hello' });
39
+ console.log(`Connected as @${me.username}`);
40
+ await api.sendMessage({ chat_id: 'chat-id', text: 'hello' });
41
41
  ```
42
42
 
43
43
  ## Webhooks
@@ -65,7 +65,44 @@ await bot.startWebhook({
65
65
  });
66
66
  ```
67
67
 
68
- ### Express / Fastify adapter
68
+ Webhook handlers return a retryable HTTP 500 when your application handler fails. Updates are acknowledged only after handlers finish successfully.
69
+
70
+ ## Configure from code
71
+
72
+ Keep bot commands and delivery configuration in source control instead of repeating setup by hand:
73
+
74
+ ```js
75
+ await bot.setup({
76
+ commands: [
77
+ { command: 'start', description: 'Start the bot' },
78
+ { command: 'help', description: 'Show help' },
79
+ ],
80
+ webhook: false,
81
+ });
82
+
83
+ await bot.launch();
84
+ ```
85
+
86
+ Use `webhook: { url, secret_token }` to configure webhook delivery instead. `bot.stop()` gracefully stops polling and the built-in webhook server.
87
+
88
+ ## Media metadata
89
+
90
+ Remote HTTPS media can include metadata used by Riseonly clients:
91
+
92
+ ```js
93
+ await bot.sendAudio({
94
+ url: 'https://cdn.example.com/song.mp3',
95
+ file_name: 'Artist - Song.mp3',
96
+ mime_type: 'audio/mpeg',
97
+ thumbnail_url: 'https://cdn.example.com/cover.jpg',
98
+ duration: 180,
99
+ }, {
100
+ chat_id: 'chat-id',
101
+ caption: 'Artist — Song',
102
+ });
103
+ ```
104
+
105
+ ## Express / Fastify webhook adapter
69
106
 
70
107
  ```js
71
108
  import express from 'express';
package/dist/index.cjs CHANGED
@@ -94,6 +94,22 @@ var RiseonlyTimeoutError = class extends RiseonlyNetworkError {
94
94
  this.name = "RiseonlyTimeoutError";
95
95
  }
96
96
  };
97
+ var RiseonlyAbortError = class extends RiseonlyNetworkError {
98
+ constructor(message = "Request was aborted", cause) {
99
+ super(message, cause, 499);
100
+ this.name = "RiseonlyAbortError";
101
+ }
102
+ };
103
+ var RiseonlyResponseError = class extends RiseonlyNetworkError {
104
+ status;
105
+ body;
106
+ constructor(message, status, body) {
107
+ super(message, void 0, status);
108
+ this.name = "RiseonlyResponseError";
109
+ this.status = status;
110
+ this.body = body;
111
+ }
112
+ };
97
113
 
98
114
  // src/client.ts
99
115
  var ApiClient = class {
@@ -108,8 +124,15 @@ var ApiClient = class {
108
124
  }
109
125
  this.token = token.trim();
110
126
  this.baseUrl = (options.baseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
111
- this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
127
+ const fetchImpl = options.fetch ?? globalThis.fetch;
128
+ if (typeof fetchImpl !== "function") {
129
+ throw new Error("global fetch is unavailable; provide ClientOptions.fetch");
130
+ }
131
+ this.fetchImpl = options.fetch ? fetchImpl : fetchImpl.bind(globalThis);
112
132
  this.requestTimeoutMs = options.requestTimeoutMs ?? 6e4;
133
+ if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
134
+ throw new Error("requestTimeoutMs must be a positive number");
135
+ }
113
136
  this.useCompatibleEndpoint = options.useCompatibleEndpoint ?? false;
114
137
  }
115
138
  /**
@@ -117,15 +140,21 @@ var ApiClient = class {
117
140
  */
118
141
  async getCapabilities(signal) {
119
142
  const url = `${this.baseUrl}${CANONICAL_API_PREFIX}/capabilities`;
120
- const response = await this.fetchImpl(url, {
143
+ const { response, body } = await this.fetchJson(url, {
121
144
  method: "GET",
122
- headers: { Accept: "application/json" },
123
- signal
124
- });
145
+ headers: { Accept: "application/json" }
146
+ }, { signal });
125
147
  if (!response.ok) {
126
- throw new RiseonlyNetworkError(`capabilities request failed with status ${response.status}`);
148
+ throw new RiseonlyResponseError(
149
+ `capabilities request failed with status ${response.status}`,
150
+ response.status,
151
+ serializeResponseBody(body)
152
+ );
127
153
  }
128
- return response.json();
154
+ if (!isRecord(body) || !Array.isArray(body.methods)) {
155
+ throw new RiseonlyResponseError("capabilities response is invalid", response.status);
156
+ }
157
+ return body;
129
158
  }
130
159
  /**
131
160
  * Calls a Bot API method and returns the typed result.
@@ -147,37 +176,36 @@ var ApiClient = class {
147
176
  if (options.requestId) {
148
177
  headers[IDEMPOTENCY_HEADER] = options.requestId;
149
178
  }
150
- const controller = new AbortController();
151
- const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
152
- const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
153
- let response;
154
- try {
155
- response = await this.fetchImpl(url, {
179
+ const { response, body } = await this.fetchJson(
180
+ url,
181
+ {
156
182
  method: "POST",
157
183
  headers,
158
- body: JSON.stringify(payload),
159
- signal
160
- });
161
- } catch (error) {
162
- if (error instanceof Error && error.name === "AbortError") {
163
- throw new RiseonlyTimeoutError();
164
- }
165
- throw new RiseonlyNetworkError("network request failed", error);
166
- } finally {
167
- clearTimeout(timeout);
168
- }
169
- const body = await response.json();
170
- if (!body.ok) {
184
+ body: JSON.stringify(payload)
185
+ },
186
+ options
187
+ );
188
+ if (isApiErrorResponse(body)) {
171
189
  throw RiseonlyError.fromApiResponse(body);
172
190
  }
191
+ if (!response.ok) {
192
+ throw new RiseonlyResponseError(
193
+ `Bot API request failed with status ${response.status}`,
194
+ response.status,
195
+ serializeResponseBody(body)
196
+ );
197
+ }
198
+ if (!isRecord(body) || body.ok !== true || !("result" in body)) {
199
+ throw new RiseonlyResponseError("Bot API response envelope is invalid", response.status);
200
+ }
173
201
  return body;
174
202
  }
175
203
  async getMe(options) {
176
204
  return this.callMethod("getMe", {}, options);
177
205
  }
178
206
  async sendMessage(options) {
179
- const { requestId, ...payload } = options;
180
- return this.callMethod("sendMessage", payload, { requestId });
207
+ const { request, payload } = splitRequestOptions(options);
208
+ return this.callMethod("sendMessage", payload, request);
181
209
  }
182
210
  async sendPhoto(photo, options) {
183
211
  return this.sendMedia("sendPhoto", "photo", photo, options);
@@ -198,72 +226,75 @@ var ApiClient = class {
198
226
  return this.sendMedia("sendAnimation", "animation", animation, options);
199
227
  }
200
228
  async editMessageText(options) {
201
- const { requestId, ...payload } = options;
202
- return this.callMethod("editMessageText", payload, { requestId });
229
+ const { request, payload } = splitRequestOptions(options);
230
+ return this.callMethod("editMessageText", payload, request);
203
231
  }
204
232
  async deleteMessage(options) {
205
- const { requestId, ...payload } = options;
206
- return this.callMethod("deleteMessage", payload, { requestId });
233
+ const { request, payload } = splitRequestOptions(options);
234
+ return this.callMethod("deleteMessage", payload, request);
207
235
  }
208
236
  async sendChatAction(options) {
209
- const { requestId, ...payload } = options;
210
- return this.callMethod("sendChatAction", payload, { requestId });
237
+ const { request, payload } = splitRequestOptions(options);
238
+ return this.callMethod("sendChatAction", payload, request);
211
239
  }
212
240
  async getUpdates(options = {}) {
213
- const { requestId, ...payload } = options;
214
- return this.callMethod("getUpdates", payload, { requestId });
241
+ const { request, payload } = splitRequestOptions(options);
242
+ const serverTimeoutMs = (options.timeout ?? 0) * 1e3 + 5e3;
243
+ request.timeoutMs ??= Math.max(this.requestTimeoutMs, serverTimeoutMs);
244
+ return this.callMethod("getUpdates", payload, request);
215
245
  }
216
246
  async setWebhook(options) {
217
- const { requestId, ...payload } = options;
218
- return this.callMethod("setWebhook", payload, { requestId });
247
+ const { request, payload } = splitRequestOptions(options);
248
+ return this.callMethod("setWebhook", payload, request);
219
249
  }
220
250
  async deleteWebhook(options = {}) {
221
- const { requestId, ...payload } = options;
222
- return this.callMethod("deleteWebhook", payload, { requestId });
251
+ const { request, payload } = splitRequestOptions(options);
252
+ return this.callMethod("deleteWebhook", payload, request);
223
253
  }
224
254
  async getWebhookInfo(options) {
225
255
  return this.callMethod("getWebhookInfo", {}, options);
226
256
  }
227
257
  async setMyCommands(options) {
228
- const { requestId, ...payload } = options;
229
- return this.callMethod("setMyCommands", payload, { requestId });
258
+ const { request, payload } = splitRequestOptions(options);
259
+ return this.callMethod("setMyCommands", payload, request);
230
260
  }
231
261
  async getMyCommands(options = {}) {
232
- const { requestId, ...payload } = options;
233
- return this.callMethod("getMyCommands", payload, { requestId });
262
+ const { request, payload } = splitRequestOptions(options);
263
+ return this.callMethod("getMyCommands", payload, request);
234
264
  }
235
265
  async deleteMyCommands(options = {}) {
236
- const { requestId, ...payload } = options;
237
- return this.callMethod("deleteMyCommands", payload, { requestId });
266
+ const { request, payload } = splitRequestOptions(options);
267
+ return this.callMethod("deleteMyCommands", payload, request);
238
268
  }
239
269
  async answerCallbackQuery(options) {
240
- const { requestId, ...payload } = options;
241
- return this.callMethod("answerCallbackQuery", payload, { requestId });
270
+ const { request, payload } = splitRequestOptions(options);
271
+ return this.callMethod("answerCallbackQuery", payload, request);
242
272
  }
243
273
  async getChat(options) {
244
- const { requestId, ...payload } = options;
245
- return this.callMethod("getChat", payload, { requestId });
274
+ const { request, payload } = splitRequestOptions(options);
275
+ return this.callMethod("getChat", payload, request);
246
276
  }
247
277
  async sendMedia(method, field, media, options) {
248
- const { requestId, ...payload } = options;
278
+ const { request, payload } = splitRequestOptions(options);
249
279
  return this.callMethod(
250
280
  method,
251
281
  {
252
282
  ...payload,
253
283
  [field]: this.normalizeMedia(media)
254
284
  },
255
- { requestId }
285
+ request
256
286
  );
257
287
  }
258
288
  normalizeMedia(media) {
259
289
  if (typeof media === "string") {
260
290
  return media;
261
291
  }
262
- if (media.file_id) {
263
- return media.file_id;
264
- }
265
- if (media.url) {
266
- return media.url;
292
+ if (media.file_id || media.url) {
293
+ return Object.fromEntries(
294
+ Object.entries(media).filter(
295
+ (entry) => typeof entry[1] === "string" || typeof entry[1] === "number"
296
+ )
297
+ );
267
298
  }
268
299
  throw new Error("media requires file_id or url");
269
300
  }
@@ -273,9 +304,77 @@ var ApiClient = class {
273
304
  }
274
305
  return `${this.baseUrl}${CANONICAL_API_PREFIX}/${method}`;
275
306
  }
307
+ async fetchJson(url, init, options = {}) {
308
+ const controller = new AbortController();
309
+ let timedOut = false;
310
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
311
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
312
+ throw new Error("timeoutMs must be a positive number");
313
+ }
314
+ const abortFromCaller = () => controller.abort(options.signal?.reason);
315
+ if (options.signal?.aborted) {
316
+ throw new RiseonlyAbortError("Request was aborted", options.signal.reason);
317
+ }
318
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
319
+ const timeout = setTimeout(() => {
320
+ timedOut = true;
321
+ controller.abort();
322
+ }, timeoutMs);
323
+ let response;
324
+ try {
325
+ response = await this.fetchImpl(url, { ...init, signal: controller.signal });
326
+ } catch (error) {
327
+ if (controller.signal.aborted) {
328
+ if (timedOut) {
329
+ throw new RiseonlyTimeoutError();
330
+ }
331
+ throw new RiseonlyAbortError("Request was aborted", error);
332
+ }
333
+ throw new RiseonlyNetworkError("network request failed", error);
334
+ } finally {
335
+ clearTimeout(timeout);
336
+ options.signal?.removeEventListener("abort", abortFromCaller);
337
+ }
338
+ let text;
339
+ try {
340
+ text = await response.text();
341
+ } catch (error) {
342
+ throw new RiseonlyNetworkError("unable to read response body", error, response.status);
343
+ }
344
+ if (!text) {
345
+ throw new RiseonlyResponseError("Bot API returned an empty response", response.status);
346
+ }
347
+ try {
348
+ return { response, body: JSON.parse(text) };
349
+ } catch {
350
+ throw new RiseonlyResponseError(
351
+ "Bot API returned a non-JSON response",
352
+ response.status,
353
+ text.slice(0, 4096)
354
+ );
355
+ }
356
+ }
276
357
  };
277
-
278
- // src/webhook.ts
358
+ function splitRequestOptions(options) {
359
+ const { requestId, signal, timeoutMs, ...payload } = options;
360
+ return {
361
+ request: { requestId, signal, timeoutMs },
362
+ payload
363
+ };
364
+ }
365
+ function isRecord(value) {
366
+ return typeof value === "object" && value !== null && !Array.isArray(value);
367
+ }
368
+ function isApiErrorResponse(value) {
369
+ return isRecord(value) && value.ok === false && typeof value.error_code === "number" && typeof value.description === "string";
370
+ }
371
+ function serializeResponseBody(body) {
372
+ try {
373
+ return JSON.stringify(body).slice(0, 4096);
374
+ } catch {
375
+ return void 0;
376
+ }
377
+ }
279
378
  function parseWebhookUpdate(body) {
280
379
  if (!body || typeof body !== "object") {
281
380
  throw new Error("webhook body must be an object");
@@ -290,28 +389,49 @@ function verifyWebhookSecret(headers, secretToken) {
290
389
  if (!secretToken) {
291
390
  return true;
292
391
  }
293
- const headerValue = headers[WEBHOOK_SECRET_HEADER] ?? headers[WEBHOOK_SECRET_HEADER.toLowerCase()];
392
+ const headerEntry = Object.entries(headers).find(
393
+ ([name]) => name.toLowerCase() === WEBHOOK_SECRET_HEADER
394
+ );
395
+ const headerValue = headerEntry?.[1];
294
396
  if (!headerValue) {
295
397
  return false;
296
398
  }
297
399
  const received = Array.isArray(headerValue) ? headerValue[0] : headerValue;
298
- return received === secretToken;
400
+ if (typeof received !== "string") {
401
+ return false;
402
+ }
403
+ const actual = Buffer.from(received);
404
+ const expected = Buffer.from(secretToken);
405
+ return actual.length === expected.length && crypto$1.timingSafeEqual(actual, expected);
299
406
  }
300
407
  function createWebhookHandler(options) {
301
408
  return async (request) => {
409
+ if (!verifyWebhookSecret(request.headers, options.secretToken)) {
410
+ return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
411
+ }
412
+ let update;
413
+ try {
414
+ update = parseWebhookUpdate(request.body);
415
+ } catch (error) {
416
+ await notifyWebhookError(options.onError, error);
417
+ return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
418
+ }
302
419
  try {
303
- if (!verifyWebhookSecret(request.headers, options.secretToken)) {
304
- return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
305
- }
306
- const update = parseWebhookUpdate(request.body);
307
420
  await options.onUpdate(update);
308
421
  return { status: 200, body: { ok: true } };
309
422
  } catch (error) {
310
- await options.onError?.(error);
311
- return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
423
+ await notifyWebhookError(options.onError, error);
424
+ return { status: 500, body: { ok: false, description: "webhook handler failed" } };
312
425
  }
313
426
  };
314
427
  }
428
+ async function notifyWebhookError(handler, error) {
429
+ try {
430
+ await handler?.(error);
431
+ } catch {
432
+ return;
433
+ }
434
+ }
315
435
 
316
436
  // src/bot.ts
317
437
  var RiseonlyBot = class extends ApiClient {
@@ -327,6 +447,7 @@ var RiseonlyBot = class extends ApiClient {
327
447
  webhookOptions;
328
448
  polling = false;
329
449
  pollingAbort;
450
+ pollingTask;
330
451
  offset = 0;
331
452
  webhookServer;
332
453
  constructor(token, options = {}) {
@@ -368,6 +489,67 @@ var RiseonlyBot = class extends ApiClient {
368
489
  });
369
490
  return this.on(event, wrapper);
370
491
  }
492
+ command(commands, handler) {
493
+ const accepted = new Set(
494
+ (Array.isArray(commands) ? commands : [commands]).map((command) => command.replace(/^\//, "").toLowerCase()).filter(Boolean)
495
+ );
496
+ return this.on("message", async (message, update) => {
497
+ const text = message.text?.trim();
498
+ if (!text?.startsWith("/")) {
499
+ return;
500
+ }
501
+ const token = text.split(/\s+/, 1)[0].slice(1).split("@", 1)[0].toLowerCase();
502
+ if (accepted.has(token)) {
503
+ await handler(message, update);
504
+ }
505
+ });
506
+ }
507
+ hears(trigger, handler) {
508
+ return this.on("message", async (message, update) => {
509
+ if (message.text === void 0) {
510
+ return;
511
+ }
512
+ if (typeof trigger === "string") {
513
+ if (message.text === trigger) {
514
+ await handler(message, null, update);
515
+ }
516
+ return;
517
+ }
518
+ trigger.lastIndex = 0;
519
+ const match = trigger.exec(message.text);
520
+ if (match) {
521
+ await handler(message, match, update);
522
+ }
523
+ });
524
+ }
525
+ action(trigger, handler) {
526
+ return this.on("callback_query", async (query, update) => {
527
+ if (query.data === void 0) {
528
+ return;
529
+ }
530
+ const matches = typeof trigger === "string" ? query.data === trigger : testPattern(trigger, query.data);
531
+ if (matches) {
532
+ await handler(query, update);
533
+ }
534
+ });
535
+ }
536
+ catch(handler) {
537
+ return this.on("error", handler);
538
+ }
539
+ async setup(options) {
540
+ if (options.commands) {
541
+ await this.setMyCommands({
542
+ commands: options.commands,
543
+ scope: options.commandScope,
544
+ language_code: options.languageCode
545
+ });
546
+ }
547
+ if (options.webhook === false) {
548
+ await this.deleteWebhook();
549
+ } else if (options.webhook) {
550
+ await this.setWebhook(options.webhook);
551
+ }
552
+ }
371
553
  /**
372
554
  * Starts long polling for bot updates.
373
555
  */
@@ -378,7 +560,7 @@ var RiseonlyBot = class extends ApiClient {
378
560
  this.polling = true;
379
561
  this.pollingOptions = { ...this.pollingOptions, ...options, autoStart: true };
380
562
  this.pollingAbort = new AbortController();
381
- void this.pollLoop();
563
+ this.pollingTask = this.pollLoop();
382
564
  }
383
565
  /**
384
566
  * Stops long polling.
@@ -386,7 +568,15 @@ var RiseonlyBot = class extends ApiClient {
386
568
  async stopPolling() {
387
569
  this.polling = false;
388
570
  this.pollingAbort?.abort();
571
+ await this.pollingTask;
389
572
  this.pollingAbort = void 0;
573
+ this.pollingTask = void 0;
574
+ }
575
+ async launch(options = {}) {
576
+ await this.startPolling(options);
577
+ }
578
+ async stop() {
579
+ await Promise.all([this.stopPolling(), this.stopWebhook()]);
390
580
  }
391
581
  /**
392
582
  * Starts a built-in HTTP server that receives webhook updates.
@@ -397,6 +587,12 @@ var RiseonlyBot = class extends ApiClient {
397
587
  const path = config.path ?? "/";
398
588
  const host = config.host ?? "0.0.0.0";
399
589
  const port = config.port ?? 3e3;
590
+ if (!path.startsWith("/")) {
591
+ throw new Error("webhook path must start with /");
592
+ }
593
+ if (config.maxBodyBytes !== void 0 && config.maxBodyBytes <= 0) {
594
+ throw new Error("maxBodyBytes must be positive");
595
+ }
400
596
  const handler = createWebhookHandler({
401
597
  secretToken: config.secretToken,
402
598
  onUpdate: (update) => this.processUpdate(update),
@@ -411,15 +607,16 @@ var RiseonlyBot = class extends ApiClient {
411
607
  return;
412
608
  }
413
609
  try {
414
- const body = await readJsonBody(req);
610
+ const body = await readJsonBody(req, config.maxBodyBytes);
415
611
  const result = await handler({
416
612
  headers: normalizeHeaders(req.headers),
417
613
  body
418
614
  });
419
615
  respond(res, result.status, result.body);
420
616
  } catch (error) {
421
- await this.emit("error", error);
422
- respond(res, 500, { ok: false });
617
+ await this.emitError(error);
618
+ const status = error instanceof WebhookPayloadTooLargeError ? 413 : 500;
619
+ respond(res, status, { ok: false });
423
620
  }
424
621
  });
425
622
  await new Promise((resolve, reject) => {
@@ -491,8 +688,8 @@ var RiseonlyBot = class extends ApiClient {
491
688
  if (update.update_id < this.offset) {
492
689
  continue;
493
690
  }
494
- this.offset = Math.max(this.offset, update.update_id + 1);
495
691
  await this.processUpdate(update);
692
+ this.offset = Math.max(this.offset, update.update_id + 1);
496
693
  processed += 1;
497
694
  }
498
695
  if (processed === 0) {
@@ -502,7 +699,10 @@ var RiseonlyBot = class extends ApiClient {
502
699
  if (!this.polling || isAbortError(error)) {
503
700
  return;
504
701
  }
505
- await this.emit("polling_error", error);
702
+ try {
703
+ await this.emit("polling_error", error);
704
+ } catch {
705
+ }
506
706
  if (error instanceof RiseonlyError && error.retryAfter) {
507
707
  await sleep(error.retryAfter * 1e3);
508
708
  } else {
@@ -512,11 +712,27 @@ var RiseonlyBot = class extends ApiClient {
512
712
  }
513
713
  }
514
714
  async emit(event, ...args) {
715
+ let firstError;
515
716
  for (const listener of [...this.listeners[event]]) {
516
717
  try {
517
718
  await listener(...args);
518
719
  } catch (error) {
519
- await this.emit("error", error);
720
+ firstError ??= error;
721
+ if (event !== "error") {
722
+ await this.emitError(error);
723
+ }
724
+ }
725
+ }
726
+ if (firstError !== void 0) {
727
+ throw firstError;
728
+ }
729
+ }
730
+ async emitError(error) {
731
+ for (const listener of [...this.listeners.error]) {
732
+ try {
733
+ await listener(error);
734
+ } catch {
735
+ continue;
520
736
  }
521
737
  }
522
738
  }
@@ -540,7 +756,11 @@ function sleep(ms) {
540
756
  return new Promise((resolve) => setTimeout(resolve, ms));
541
757
  }
542
758
  function isAbortError(error) {
543
- return error instanceof Error && error.name === "AbortError";
759
+ return error instanceof Error && (error.name === "AbortError" || error.name === "RiseonlyAbortError");
760
+ }
761
+ function testPattern(pattern, value) {
762
+ pattern.lastIndex = 0;
763
+ return pattern.test(value);
544
764
  }
545
765
  function normalizeHeaders(headers) {
546
766
  const normalized = {};
@@ -549,10 +769,19 @@ function normalizeHeaders(headers) {
549
769
  }
550
770
  return normalized;
551
771
  }
552
- async function readJsonBody(req) {
772
+ var WebhookPayloadTooLargeError = class extends Error {
773
+ };
774
+ async function readJsonBody(req, maxBodyBytes = 1024 * 1024) {
553
775
  const chunks = [];
776
+ let received = 0;
554
777
  for await (const chunk of req) {
555
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
778
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
779
+ received += buffer.length;
780
+ if (received > maxBodyBytes) {
781
+ req.resume();
782
+ throw new WebhookPayloadTooLargeError("webhook payload is too large");
783
+ }
784
+ chunks.push(buffer);
556
785
  }
557
786
  if (chunks.length === 0) {
558
787
  return {};
@@ -595,8 +824,12 @@ function verifyInitData(botToken, encoded) {
595
824
  if (left.length !== right.length || !crypto$1.timingSafeEqual(left, right)) {
596
825
  throw new Error("init data signature is invalid");
597
826
  }
598
- const expiresAt = fields.expires_at ? Number(fields.expires_at) : void 0;
599
- if (expiresAt !== void 0 && Number.isFinite(expiresAt) && expiresAt < Date.now()) {
827
+ const authDate = Number(fields.auth_date);
828
+ const expiresAt = Number(fields.expires_at);
829
+ if (!Number.isSafeInteger(authDate) || !Number.isSafeInteger(expiresAt) || expiresAt <= authDate) {
830
+ throw new Error("init data timestamps are invalid");
831
+ }
832
+ if (expiresAt < Date.now()) {
600
833
  throw new Error("init data has expired");
601
834
  }
602
835
  return fields;
@@ -619,9 +852,11 @@ exports.CHAT_ACTIONS = CHAT_ACTIONS;
619
852
  exports.COMMAND_SCOPES = COMMAND_SCOPES;
620
853
  exports.DEFAULT_API_BASE = DEFAULT_API_BASE;
621
854
  exports.IDEMPOTENCY_HEADER = IDEMPOTENCY_HEADER;
855
+ exports.RiseonlyAbortError = RiseonlyAbortError;
622
856
  exports.RiseonlyBot = RiseonlyBot;
623
857
  exports.RiseonlyError = RiseonlyError;
624
858
  exports.RiseonlyNetworkError = RiseonlyNetworkError;
859
+ exports.RiseonlyResponseError = RiseonlyResponseError;
625
860
  exports.RiseonlyTimeoutError = RiseonlyTimeoutError;
626
861
  exports.SUPPORTED_METHODS = SUPPORTED_METHODS;
627
862
  exports.UPDATE_TYPES = UPDATE_TYPES;