@riseonly/sdk 0.1.1 → 0.1.3

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createServer } from 'http';
2
- import { createHmac, timingSafeEqual } from 'crypto';
2
+ import { timingSafeEqual, createHmac } from 'crypto';
3
3
 
4
4
  // src/bot.ts
5
5
 
@@ -27,6 +27,12 @@ var SUPPORTED_METHODS = [
27
27
  "setMyCommands",
28
28
  "getMyCommands",
29
29
  "deleteMyCommands",
30
+ "setMyName",
31
+ "getMyName",
32
+ "setMyDescription",
33
+ "getMyDescription",
34
+ "setMyProfilePhoto",
35
+ "removeMyProfilePhoto",
30
36
  "answerCallbackQuery",
31
37
  "getChat"
32
38
  ];
@@ -92,6 +98,22 @@ var RiseonlyTimeoutError = class extends RiseonlyNetworkError {
92
98
  this.name = "RiseonlyTimeoutError";
93
99
  }
94
100
  };
101
+ var RiseonlyAbortError = class extends RiseonlyNetworkError {
102
+ constructor(message = "Request was aborted", cause) {
103
+ super(message, cause, 499);
104
+ this.name = "RiseonlyAbortError";
105
+ }
106
+ };
107
+ var RiseonlyResponseError = class extends RiseonlyNetworkError {
108
+ status;
109
+ body;
110
+ constructor(message, status, body) {
111
+ super(message, void 0, status);
112
+ this.name = "RiseonlyResponseError";
113
+ this.status = status;
114
+ this.body = body;
115
+ }
116
+ };
95
117
 
96
118
  // src/client.ts
97
119
  var ApiClient = class {
@@ -106,8 +128,15 @@ var ApiClient = class {
106
128
  }
107
129
  this.token = token.trim();
108
130
  this.baseUrl = (options.baseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
109
- this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
131
+ const fetchImpl = options.fetch ?? globalThis.fetch;
132
+ if (typeof fetchImpl !== "function") {
133
+ throw new Error("global fetch is unavailable; provide ClientOptions.fetch");
134
+ }
135
+ this.fetchImpl = options.fetch ? fetchImpl : fetchImpl.bind(globalThis);
110
136
  this.requestTimeoutMs = options.requestTimeoutMs ?? 6e4;
137
+ if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
138
+ throw new Error("requestTimeoutMs must be a positive number");
139
+ }
111
140
  this.useCompatibleEndpoint = options.useCompatibleEndpoint ?? false;
112
141
  }
113
142
  /**
@@ -115,15 +144,21 @@ var ApiClient = class {
115
144
  */
116
145
  async getCapabilities(signal) {
117
146
  const url = `${this.baseUrl}${CANONICAL_API_PREFIX}/capabilities`;
118
- const response = await this.fetchImpl(url, {
147
+ const { response, body } = await this.fetchJson(url, {
119
148
  method: "GET",
120
- headers: { Accept: "application/json" },
121
- signal
122
- });
149
+ headers: { Accept: "application/json" }
150
+ }, { signal });
123
151
  if (!response.ok) {
124
- throw new RiseonlyNetworkError(`capabilities request failed with status ${response.status}`);
152
+ throw new RiseonlyResponseError(
153
+ `capabilities request failed with status ${response.status}`,
154
+ response.status,
155
+ serializeResponseBody(body)
156
+ );
157
+ }
158
+ if (!isRecord(body) || !Array.isArray(body.methods)) {
159
+ throw new RiseonlyResponseError("capabilities response is invalid", response.status);
125
160
  }
126
- return response.json();
161
+ return body;
127
162
  }
128
163
  /**
129
164
  * Calls a Bot API method and returns the typed result.
@@ -145,37 +180,36 @@ var ApiClient = class {
145
180
  if (options.requestId) {
146
181
  headers[IDEMPOTENCY_HEADER] = options.requestId;
147
182
  }
148
- const controller = new AbortController();
149
- const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
150
- const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
151
- let response;
152
- try {
153
- response = await this.fetchImpl(url, {
183
+ const { response, body } = await this.fetchJson(
184
+ url,
185
+ {
154
186
  method: "POST",
155
187
  headers,
156
- body: JSON.stringify(payload),
157
- signal
158
- });
159
- } catch (error) {
160
- if (error instanceof Error && error.name === "AbortError") {
161
- throw new RiseonlyTimeoutError();
162
- }
163
- throw new RiseonlyNetworkError("network request failed", error);
164
- } finally {
165
- clearTimeout(timeout);
166
- }
167
- const body = await response.json();
168
- if (!body.ok) {
188
+ body: JSON.stringify(payload)
189
+ },
190
+ options
191
+ );
192
+ if (isApiErrorResponse(body)) {
169
193
  throw RiseonlyError.fromApiResponse(body);
170
194
  }
195
+ if (!response.ok) {
196
+ throw new RiseonlyResponseError(
197
+ `Bot API request failed with status ${response.status}`,
198
+ response.status,
199
+ serializeResponseBody(body)
200
+ );
201
+ }
202
+ if (!isRecord(body) || body.ok !== true || !("result" in body)) {
203
+ throw new RiseonlyResponseError("Bot API response envelope is invalid", response.status);
204
+ }
171
205
  return body;
172
206
  }
173
207
  async getMe(options) {
174
208
  return this.callMethod("getMe", {}, options);
175
209
  }
176
210
  async sendMessage(options) {
177
- const { requestId, ...payload } = options;
178
- return this.callMethod("sendMessage", payload, { requestId });
211
+ const { request, payload } = splitRequestOptions(options);
212
+ return this.callMethod("sendMessage", payload, request);
179
213
  }
180
214
  async sendPhoto(photo, options) {
181
215
  return this.sendMedia("sendPhoto", "photo", photo, options);
@@ -196,72 +230,96 @@ var ApiClient = class {
196
230
  return this.sendMedia("sendAnimation", "animation", animation, options);
197
231
  }
198
232
  async editMessageText(options) {
199
- const { requestId, ...payload } = options;
200
- return this.callMethod("editMessageText", payload, { requestId });
233
+ const { request, payload } = splitRequestOptions(options);
234
+ return this.callMethod("editMessageText", payload, request);
201
235
  }
202
236
  async deleteMessage(options) {
203
- const { requestId, ...payload } = options;
204
- return this.callMethod("deleteMessage", payload, { requestId });
237
+ const { request, payload } = splitRequestOptions(options);
238
+ return this.callMethod("deleteMessage", payload, request);
205
239
  }
206
240
  async sendChatAction(options) {
207
- const { requestId, ...payload } = options;
208
- return this.callMethod("sendChatAction", payload, { requestId });
241
+ const { request, payload } = splitRequestOptions(options);
242
+ return this.callMethod("sendChatAction", payload, request);
209
243
  }
210
244
  async getUpdates(options = {}) {
211
- const { requestId, ...payload } = options;
212
- return this.callMethod("getUpdates", payload, { requestId });
245
+ const { request, payload } = splitRequestOptions(options);
246
+ const serverTimeoutMs = (options.timeout ?? 0) * 1e3 + 5e3;
247
+ request.timeoutMs ??= Math.max(this.requestTimeoutMs, serverTimeoutMs);
248
+ return this.callMethod("getUpdates", payload, request);
213
249
  }
214
250
  async setWebhook(options) {
215
- const { requestId, ...payload } = options;
216
- return this.callMethod("setWebhook", payload, { requestId });
251
+ const { request, payload } = splitRequestOptions(options);
252
+ return this.callMethod("setWebhook", payload, request);
217
253
  }
218
254
  async deleteWebhook(options = {}) {
219
- const { requestId, ...payload } = options;
220
- return this.callMethod("deleteWebhook", payload, { requestId });
255
+ const { request, payload } = splitRequestOptions(options);
256
+ return this.callMethod("deleteWebhook", payload, request);
221
257
  }
222
258
  async getWebhookInfo(options) {
223
259
  return this.callMethod("getWebhookInfo", {}, options);
224
260
  }
225
261
  async setMyCommands(options) {
226
- const { requestId, ...payload } = options;
227
- return this.callMethod("setMyCommands", payload, { requestId });
262
+ const { request, payload } = splitRequestOptions(options);
263
+ return this.callMethod("setMyCommands", payload, request);
228
264
  }
229
265
  async getMyCommands(options = {}) {
230
- const { requestId, ...payload } = options;
231
- return this.callMethod("getMyCommands", payload, { requestId });
266
+ const { request, payload } = splitRequestOptions(options);
267
+ return this.callMethod("getMyCommands", payload, request);
232
268
  }
233
269
  async deleteMyCommands(options = {}) {
234
- const { requestId, ...payload } = options;
235
- return this.callMethod("deleteMyCommands", payload, { requestId });
270
+ const { request, payload } = splitRequestOptions(options);
271
+ return this.callMethod("deleteMyCommands", payload, request);
272
+ }
273
+ async setMyName(options) {
274
+ const { request, payload } = splitRequestOptions(options);
275
+ return this.callMethod("setMyName", payload, request);
276
+ }
277
+ async getMyName(options = {}) {
278
+ return this.callMethod("getMyName", {}, options);
279
+ }
280
+ async setMyDescription(options) {
281
+ const { request, payload } = splitRequestOptions(options);
282
+ return this.callMethod("setMyDescription", payload, request);
283
+ }
284
+ async getMyDescription(options = {}) {
285
+ return this.callMethod("getMyDescription", {}, options);
286
+ }
287
+ async setMyProfilePhoto(options) {
288
+ const { request, payload } = splitRequestOptions(options);
289
+ return this.callMethod("setMyProfilePhoto", payload, request);
290
+ }
291
+ async removeMyProfilePhoto(options = {}) {
292
+ return this.callMethod("removeMyProfilePhoto", {}, options);
236
293
  }
237
294
  async answerCallbackQuery(options) {
238
- const { requestId, ...payload } = options;
239
- return this.callMethod("answerCallbackQuery", payload, { requestId });
295
+ const { request, payload } = splitRequestOptions(options);
296
+ return this.callMethod("answerCallbackQuery", payload, request);
240
297
  }
241
298
  async getChat(options) {
242
- const { requestId, ...payload } = options;
243
- return this.callMethod("getChat", payload, { requestId });
299
+ const { request, payload } = splitRequestOptions(options);
300
+ return this.callMethod("getChat", payload, request);
244
301
  }
245
302
  async sendMedia(method, field, media, options) {
246
- const { requestId, ...payload } = options;
303
+ const { request, payload } = splitRequestOptions(options);
247
304
  return this.callMethod(
248
305
  method,
249
306
  {
250
307
  ...payload,
251
308
  [field]: this.normalizeMedia(media)
252
309
  },
253
- { requestId }
310
+ request
254
311
  );
255
312
  }
256
313
  normalizeMedia(media) {
257
314
  if (typeof media === "string") {
258
315
  return media;
259
316
  }
260
- if (media.file_id) {
261
- return media.file_id;
262
- }
263
- if (media.url) {
264
- return media.url;
317
+ if (media.file_id || media.url) {
318
+ return Object.fromEntries(
319
+ Object.entries(media).filter(
320
+ (entry) => typeof entry[1] === "string" || typeof entry[1] === "number"
321
+ )
322
+ );
265
323
  }
266
324
  throw new Error("media requires file_id or url");
267
325
  }
@@ -271,9 +329,77 @@ var ApiClient = class {
271
329
  }
272
330
  return `${this.baseUrl}${CANONICAL_API_PREFIX}/${method}`;
273
331
  }
332
+ async fetchJson(url, init, options = {}) {
333
+ const controller = new AbortController();
334
+ let timedOut = false;
335
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
336
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
337
+ throw new Error("timeoutMs must be a positive number");
338
+ }
339
+ const abortFromCaller = () => controller.abort(options.signal?.reason);
340
+ if (options.signal?.aborted) {
341
+ throw new RiseonlyAbortError("Request was aborted", options.signal.reason);
342
+ }
343
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
344
+ const timeout = setTimeout(() => {
345
+ timedOut = true;
346
+ controller.abort();
347
+ }, timeoutMs);
348
+ let response;
349
+ try {
350
+ response = await this.fetchImpl(url, { ...init, signal: controller.signal });
351
+ } catch (error) {
352
+ if (controller.signal.aborted) {
353
+ if (timedOut) {
354
+ throw new RiseonlyTimeoutError();
355
+ }
356
+ throw new RiseonlyAbortError("Request was aborted", error);
357
+ }
358
+ throw new RiseonlyNetworkError("network request failed", error);
359
+ } finally {
360
+ clearTimeout(timeout);
361
+ options.signal?.removeEventListener("abort", abortFromCaller);
362
+ }
363
+ let text;
364
+ try {
365
+ text = await response.text();
366
+ } catch (error) {
367
+ throw new RiseonlyNetworkError("unable to read response body", error, response.status);
368
+ }
369
+ if (!text) {
370
+ throw new RiseonlyResponseError("Bot API returned an empty response", response.status);
371
+ }
372
+ try {
373
+ return { response, body: JSON.parse(text) };
374
+ } catch {
375
+ throw new RiseonlyResponseError(
376
+ "Bot API returned a non-JSON response",
377
+ response.status,
378
+ text.slice(0, 4096)
379
+ );
380
+ }
381
+ }
274
382
  };
275
-
276
- // src/webhook.ts
383
+ function splitRequestOptions(options) {
384
+ const { requestId, signal, timeoutMs, ...payload } = options;
385
+ return {
386
+ request: { requestId, signal, timeoutMs },
387
+ payload
388
+ };
389
+ }
390
+ function isRecord(value) {
391
+ return typeof value === "object" && value !== null && !Array.isArray(value);
392
+ }
393
+ function isApiErrorResponse(value) {
394
+ return isRecord(value) && value.ok === false && typeof value.error_code === "number" && typeof value.description === "string";
395
+ }
396
+ function serializeResponseBody(body) {
397
+ try {
398
+ return JSON.stringify(body).slice(0, 4096);
399
+ } catch {
400
+ return void 0;
401
+ }
402
+ }
277
403
  function parseWebhookUpdate(body) {
278
404
  if (!body || typeof body !== "object") {
279
405
  throw new Error("webhook body must be an object");
@@ -288,28 +414,49 @@ function verifyWebhookSecret(headers, secretToken) {
288
414
  if (!secretToken) {
289
415
  return true;
290
416
  }
291
- const headerValue = headers[WEBHOOK_SECRET_HEADER] ?? headers[WEBHOOK_SECRET_HEADER.toLowerCase()];
417
+ const headerEntry = Object.entries(headers).find(
418
+ ([name]) => name.toLowerCase() === WEBHOOK_SECRET_HEADER
419
+ );
420
+ const headerValue = headerEntry?.[1];
292
421
  if (!headerValue) {
293
422
  return false;
294
423
  }
295
424
  const received = Array.isArray(headerValue) ? headerValue[0] : headerValue;
296
- return received === secretToken;
425
+ if (typeof received !== "string") {
426
+ return false;
427
+ }
428
+ const actual = Buffer.from(received);
429
+ const expected = Buffer.from(secretToken);
430
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
297
431
  }
298
432
  function createWebhookHandler(options) {
299
433
  return async (request) => {
434
+ if (!verifyWebhookSecret(request.headers, options.secretToken)) {
435
+ return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
436
+ }
437
+ let update;
438
+ try {
439
+ update = parseWebhookUpdate(request.body);
440
+ } catch (error) {
441
+ await notifyWebhookError(options.onError, error);
442
+ return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
443
+ }
300
444
  try {
301
- if (!verifyWebhookSecret(request.headers, options.secretToken)) {
302
- return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
303
- }
304
- const update = parseWebhookUpdate(request.body);
305
445
  await options.onUpdate(update);
306
446
  return { status: 200, body: { ok: true } };
307
447
  } catch (error) {
308
- await options.onError?.(error);
309
- return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
448
+ await notifyWebhookError(options.onError, error);
449
+ return { status: 500, body: { ok: false, description: "webhook handler failed" } };
310
450
  }
311
451
  };
312
452
  }
453
+ async function notifyWebhookError(handler, error) {
454
+ try {
455
+ await handler?.(error);
456
+ } catch {
457
+ return;
458
+ }
459
+ }
313
460
 
314
461
  // src/bot.ts
315
462
  var RiseonlyBot = class extends ApiClient {
@@ -325,6 +472,7 @@ var RiseonlyBot = class extends ApiClient {
325
472
  webhookOptions;
326
473
  polling = false;
327
474
  pollingAbort;
475
+ pollingTask;
328
476
  offset = 0;
329
477
  webhookServer;
330
478
  constructor(token, options = {}) {
@@ -366,6 +514,76 @@ var RiseonlyBot = class extends ApiClient {
366
514
  });
367
515
  return this.on(event, wrapper);
368
516
  }
517
+ command(commands, handler) {
518
+ const accepted = new Set(
519
+ (Array.isArray(commands) ? commands : [commands]).map((command) => command.replace(/^\//, "").toLowerCase()).filter(Boolean)
520
+ );
521
+ return this.on("message", async (message, update) => {
522
+ const text = message.text?.trim();
523
+ if (!text?.startsWith("/")) {
524
+ return;
525
+ }
526
+ const token = text.split(/\s+/, 1)[0].slice(1).split("@", 1)[0].toLowerCase();
527
+ if (accepted.has(token)) {
528
+ await handler(message, update);
529
+ }
530
+ });
531
+ }
532
+ hears(trigger, handler) {
533
+ return this.on("message", async (message, update) => {
534
+ if (message.text === void 0) {
535
+ return;
536
+ }
537
+ if (typeof trigger === "string") {
538
+ if (message.text === trigger) {
539
+ await handler(message, null, update);
540
+ }
541
+ return;
542
+ }
543
+ trigger.lastIndex = 0;
544
+ const match = trigger.exec(message.text);
545
+ if (match) {
546
+ await handler(message, match, update);
547
+ }
548
+ });
549
+ }
550
+ action(trigger, handler) {
551
+ return this.on("callback_query", async (query, update) => {
552
+ if (query.data === void 0) {
553
+ return;
554
+ }
555
+ const matches = typeof trigger === "string" ? query.data === trigger : testPattern(trigger, query.data);
556
+ if (matches) {
557
+ await handler(query, update);
558
+ }
559
+ });
560
+ }
561
+ catch(handler) {
562
+ return this.on("error", handler);
563
+ }
564
+ async setup(options) {
565
+ if (options.name !== void 0) {
566
+ await this.setMyName({ name: options.name });
567
+ }
568
+ if (options.description !== void 0) {
569
+ await this.setMyDescription({ description: options.description });
570
+ }
571
+ if (options.profilePhoto !== void 0) {
572
+ await this.setMyProfilePhoto({ photo: options.profilePhoto });
573
+ }
574
+ if (options.commands) {
575
+ await this.setMyCommands({
576
+ commands: options.commands,
577
+ scope: options.commandScope,
578
+ language_code: options.languageCode
579
+ });
580
+ }
581
+ if (options.webhook === false) {
582
+ await this.deleteWebhook();
583
+ } else if (options.webhook) {
584
+ await this.setWebhook(options.webhook);
585
+ }
586
+ }
369
587
  /**
370
588
  * Starts long polling for bot updates.
371
589
  */
@@ -376,7 +594,7 @@ var RiseonlyBot = class extends ApiClient {
376
594
  this.polling = true;
377
595
  this.pollingOptions = { ...this.pollingOptions, ...options, autoStart: true };
378
596
  this.pollingAbort = new AbortController();
379
- void this.pollLoop();
597
+ this.pollingTask = this.pollLoop();
380
598
  }
381
599
  /**
382
600
  * Stops long polling.
@@ -384,7 +602,15 @@ var RiseonlyBot = class extends ApiClient {
384
602
  async stopPolling() {
385
603
  this.polling = false;
386
604
  this.pollingAbort?.abort();
605
+ await this.pollingTask;
387
606
  this.pollingAbort = void 0;
607
+ this.pollingTask = void 0;
608
+ }
609
+ async launch(options = {}) {
610
+ await this.startPolling(options);
611
+ }
612
+ async stop() {
613
+ await Promise.all([this.stopPolling(), this.stopWebhook()]);
388
614
  }
389
615
  /**
390
616
  * Starts a built-in HTTP server that receives webhook updates.
@@ -395,6 +621,12 @@ var RiseonlyBot = class extends ApiClient {
395
621
  const path = config.path ?? "/";
396
622
  const host = config.host ?? "0.0.0.0";
397
623
  const port = config.port ?? 3e3;
624
+ if (!path.startsWith("/")) {
625
+ throw new Error("webhook path must start with /");
626
+ }
627
+ if (config.maxBodyBytes !== void 0 && config.maxBodyBytes <= 0) {
628
+ throw new Error("maxBodyBytes must be positive");
629
+ }
398
630
  const handler = createWebhookHandler({
399
631
  secretToken: config.secretToken,
400
632
  onUpdate: (update) => this.processUpdate(update),
@@ -409,15 +641,16 @@ var RiseonlyBot = class extends ApiClient {
409
641
  return;
410
642
  }
411
643
  try {
412
- const body = await readJsonBody(req);
644
+ const body = await readJsonBody(req, config.maxBodyBytes);
413
645
  const result = await handler({
414
646
  headers: normalizeHeaders(req.headers),
415
647
  body
416
648
  });
417
649
  respond(res, result.status, result.body);
418
650
  } catch (error) {
419
- await this.emit("error", error);
420
- respond(res, 500, { ok: false });
651
+ await this.emitError(error);
652
+ const status = error instanceof WebhookPayloadTooLargeError ? 413 : 500;
653
+ respond(res, status, { ok: false });
421
654
  }
422
655
  });
423
656
  await new Promise((resolve, reject) => {
@@ -489,8 +722,8 @@ var RiseonlyBot = class extends ApiClient {
489
722
  if (update.update_id < this.offset) {
490
723
  continue;
491
724
  }
492
- this.offset = Math.max(this.offset, update.update_id + 1);
493
725
  await this.processUpdate(update);
726
+ this.offset = Math.max(this.offset, update.update_id + 1);
494
727
  processed += 1;
495
728
  }
496
729
  if (processed === 0) {
@@ -500,7 +733,10 @@ var RiseonlyBot = class extends ApiClient {
500
733
  if (!this.polling || isAbortError(error)) {
501
734
  return;
502
735
  }
503
- await this.emit("polling_error", error);
736
+ try {
737
+ await this.emit("polling_error", error);
738
+ } catch {
739
+ }
504
740
  if (error instanceof RiseonlyError && error.retryAfter) {
505
741
  await sleep(error.retryAfter * 1e3);
506
742
  } else {
@@ -510,11 +746,27 @@ var RiseonlyBot = class extends ApiClient {
510
746
  }
511
747
  }
512
748
  async emit(event, ...args) {
749
+ let firstError;
513
750
  for (const listener of [...this.listeners[event]]) {
514
751
  try {
515
752
  await listener(...args);
516
753
  } catch (error) {
517
- await this.emit("error", error);
754
+ firstError ??= error;
755
+ if (event !== "error") {
756
+ await this.emitError(error);
757
+ }
758
+ }
759
+ }
760
+ if (firstError !== void 0) {
761
+ throw firstError;
762
+ }
763
+ }
764
+ async emitError(error) {
765
+ for (const listener of [...this.listeners.error]) {
766
+ try {
767
+ await listener(error);
768
+ } catch {
769
+ continue;
518
770
  }
519
771
  }
520
772
  }
@@ -538,7 +790,11 @@ function sleep(ms) {
538
790
  return new Promise((resolve) => setTimeout(resolve, ms));
539
791
  }
540
792
  function isAbortError(error) {
541
- return error instanceof Error && error.name === "AbortError";
793
+ return error instanceof Error && (error.name === "AbortError" || error.name === "RiseonlyAbortError");
794
+ }
795
+ function testPattern(pattern, value) {
796
+ pattern.lastIndex = 0;
797
+ return pattern.test(value);
542
798
  }
543
799
  function normalizeHeaders(headers) {
544
800
  const normalized = {};
@@ -547,10 +803,19 @@ function normalizeHeaders(headers) {
547
803
  }
548
804
  return normalized;
549
805
  }
550
- async function readJsonBody(req) {
806
+ var WebhookPayloadTooLargeError = class extends Error {
807
+ };
808
+ async function readJsonBody(req, maxBodyBytes = 1024 * 1024) {
551
809
  const chunks = [];
810
+ let received = 0;
552
811
  for await (const chunk of req) {
553
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
812
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
813
+ received += buffer.length;
814
+ if (received > maxBodyBytes) {
815
+ req.resume();
816
+ throw new WebhookPayloadTooLargeError("webhook payload is too large");
817
+ }
818
+ chunks.push(buffer);
554
819
  }
555
820
  if (chunks.length === 0) {
556
821
  return {};
@@ -593,8 +858,12 @@ function verifyInitData(botToken, encoded) {
593
858
  if (left.length !== right.length || !timingSafeEqual(left, right)) {
594
859
  throw new Error("init data signature is invalid");
595
860
  }
596
- const expiresAt = fields.expires_at ? Number(fields.expires_at) : void 0;
597
- if (expiresAt !== void 0 && Number.isFinite(expiresAt) && expiresAt < Date.now()) {
861
+ const authDate = Number(fields.auth_date);
862
+ const expiresAt = Number(fields.expires_at);
863
+ if (!Number.isSafeInteger(authDate) || !Number.isSafeInteger(expiresAt) || expiresAt <= authDate) {
864
+ throw new Error("init data timestamps are invalid");
865
+ }
866
+ if (expiresAt < Date.now()) {
598
867
  throw new Error("init data has expired");
599
868
  }
600
869
  return fields;
@@ -611,6 +880,6 @@ function createRequestId() {
611
880
  return crypto.randomUUID();
612
881
  }
613
882
 
614
- export { ApiClient, CANONICAL_API_PREFIX, CHAT_ACTIONS, COMMAND_SCOPES, DEFAULT_API_BASE, IDEMPOTENCY_HEADER, RiseonlyBot, RiseonlyError, RiseonlyNetworkError, RiseonlyTimeoutError, SUPPORTED_METHODS, UPDATE_TYPES, WEBHOOK_SECRET_HEADER, buildInitDataCheckString, createRequestId, createWebhookHandler, deriveInitDataSecret, extractInitDataFromLaunchUrl, parseInitData, parseWebhookUpdate, signInitData, verifyInitData, verifyWebhookSecret };
883
+ export { ApiClient, CANONICAL_API_PREFIX, CHAT_ACTIONS, COMMAND_SCOPES, DEFAULT_API_BASE, IDEMPOTENCY_HEADER, RiseonlyAbortError, RiseonlyBot, RiseonlyError, RiseonlyNetworkError, RiseonlyResponseError, RiseonlyTimeoutError, SUPPORTED_METHODS, UPDATE_TYPES, WEBHOOK_SECRET_HEADER, buildInitDataCheckString, createRequestId, createWebhookHandler, deriveInitDataSecret, extractInitDataFromLaunchUrl, parseInitData, parseWebhookUpdate, signInitData, verifyInitData, verifyWebhookSecret };
615
884
  //# sourceMappingURL=index.js.map
616
885
  //# sourceMappingURL=index.js.map