scrapebadger 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+ import { EventEmitter } from 'events';
3
+ import WebSocket from 'ws';
4
+
1
5
  // src/internal/exceptions.ts
2
6
  var ScrapeBadgerError = class _ScrapeBadgerError extends Error {
3
7
  constructor(message) {
@@ -92,6 +96,23 @@ var AccountRestrictedError = class _AccountRestrictedError extends ScrapeBadgerE
92
96
  Object.setPrototypeOf(this, _AccountRestrictedError.prototype);
93
97
  }
94
98
  };
99
+ var ConflictError = class _ConflictError extends ScrapeBadgerError {
100
+ constructor(message = "Resource conflict.") {
101
+ super(message);
102
+ this.name = "ConflictError";
103
+ Object.setPrototypeOf(this, _ConflictError.prototype);
104
+ }
105
+ };
106
+ var WebSocketStreamError = class _WebSocketStreamError extends ScrapeBadgerError {
107
+ /** WebSocket close code or server error code */
108
+ code;
109
+ constructor(message = "WebSocket stream error", code) {
110
+ super(message);
111
+ this.name = "WebSocketStreamError";
112
+ this.code = code;
113
+ Object.setPrototypeOf(this, _WebSocketStreamError.prototype);
114
+ }
115
+ };
95
116
 
96
117
  // src/internal/client.ts
97
118
  var BaseClient = class {
@@ -140,9 +161,7 @@ var BaseClient = class {
140
161
  } catch (error) {
141
162
  lastError = error;
142
163
  if (error instanceof ScrapeBadgerError && !(error instanceof RateLimitError)) {
143
- if (error instanceof AuthenticationError || error instanceof NotFoundError || error instanceof ValidationError || error instanceof InsufficientCreditsError || error instanceof AccountRestrictedError) {
144
- throw error;
145
- }
164
+ throw error;
146
165
  }
147
166
  if (attempt === this.config.maxRetries) {
148
167
  break;
@@ -174,7 +193,10 @@ var BaseClient = class {
174
193
  return response;
175
194
  } catch (error) {
176
195
  if (error instanceof Error && error.name === "AbortError") {
177
- throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`, this.config.timeout);
196
+ throw new TimeoutError(
197
+ `Request timed out after ${this.config.timeout}ms`,
198
+ this.config.timeout
199
+ );
178
200
  }
179
201
  throw error;
180
202
  } finally {
@@ -210,6 +232,8 @@ var BaseClient = class {
210
232
  throw new AuthenticationError(message);
211
233
  case 404:
212
234
  throw new NotFoundError(message);
235
+ case 409:
236
+ throw new ConflictError(message);
213
237
  case 422:
214
238
  throw new ValidationError(message, errorData.errors);
215
239
  case 429:
@@ -517,6 +541,7 @@ var TweetsClient = class {
517
541
  params: {
518
542
  query,
519
543
  query_type: options.queryType ?? "Top",
544
+ count: options.count,
520
545
  cursor: options.cursor
521
546
  }
522
547
  }
@@ -1403,6 +1428,667 @@ var GeoClient = class {
1403
1428
  return createPaginatedResponse(response.data ?? []);
1404
1429
  }
1405
1430
  };
1431
+ var MIN_RECONNECT_DELAY_SECONDS = 5;
1432
+ function wsUrlFromBase(baseUrl) {
1433
+ if (baseUrl.startsWith("https://")) {
1434
+ return baseUrl.replace("https://", "wss://") + "/v1/twitter/stream";
1435
+ }
1436
+ if (baseUrl.startsWith("http://")) {
1437
+ return baseUrl.replace("http://", "ws://") + "/v1/twitter/stream";
1438
+ }
1439
+ return baseUrl + "/v1/twitter/stream";
1440
+ }
1441
+ function parseEvent(raw) {
1442
+ const type = raw["type"];
1443
+ switch (type) {
1444
+ case "connected":
1445
+ return {
1446
+ type: "connected",
1447
+ connectionId: raw["connection_id"],
1448
+ apiKeyId: raw["api_key_id"]
1449
+ };
1450
+ case "ping":
1451
+ return {
1452
+ type: "ping",
1453
+ timestamp: raw["timestamp"]
1454
+ };
1455
+ case "tweet":
1456
+ return {
1457
+ type: "tweet",
1458
+ monitorId: raw["monitor_id"],
1459
+ tweetId: raw["tweet_id"],
1460
+ authorUsername: raw["author_username"],
1461
+ tweetPublishedAt: raw["tweet_published_at"],
1462
+ detectedAt: raw["detected_at"],
1463
+ latencyMs: raw["latency_ms"],
1464
+ tweet: raw["tweet"]
1465
+ };
1466
+ case "error":
1467
+ return {
1468
+ type: "error",
1469
+ code: raw["code"],
1470
+ message: raw["message"]
1471
+ };
1472
+ default:
1473
+ return {
1474
+ type: "error",
1475
+ code: 0,
1476
+ message: `Unknown event type: ${String(type)}`
1477
+ };
1478
+ }
1479
+ }
1480
+ var StreamClient = class {
1481
+ client;
1482
+ constructor(client) {
1483
+ this.client = client;
1484
+ }
1485
+ // ===========================================================================
1486
+ // Monitor CRUD
1487
+ // ===========================================================================
1488
+ /**
1489
+ * Create a new stream monitor.
1490
+ *
1491
+ * @param params - Monitor configuration.
1492
+ * @returns The created StreamMonitor.
1493
+ * @throws InsufficientCreditsError - Credit balance below tier threshold (402).
1494
+ * @throws ValidationError - Invalid username or interval (422).
1495
+ * @throws ScrapeBadgerError - Name conflict (409).
1496
+ * @throws AuthenticationError - Invalid API key (401).
1497
+ *
1498
+ * @example
1499
+ * ```typescript
1500
+ * const monitor = await client.twitter.stream.createMonitor({
1501
+ * name: "Breaking News",
1502
+ * usernames: ["cnnbrk", "bbcbreaking"],
1503
+ * pollIntervalSeconds: 5,
1504
+ * });
1505
+ * ```
1506
+ */
1507
+ async createMonitor(params) {
1508
+ const body = {
1509
+ name: params.name,
1510
+ usernames: params.usernames,
1511
+ poll_interval_seconds: params.pollIntervalSeconds
1512
+ };
1513
+ if (params.webhookUrl !== void 0) body["webhook_url"] = params.webhookUrl;
1514
+ if (params.webhookSecret !== void 0) body["webhook_secret"] = params.webhookSecret;
1515
+ return this.client.request("/v1/twitter/stream/monitors", {
1516
+ method: "POST",
1517
+ body
1518
+ });
1519
+ }
1520
+ /**
1521
+ * List stream monitors for the authenticated API key.
1522
+ *
1523
+ * @param options - Filter and pagination options.
1524
+ * @returns StreamMonitorList with pagination metadata.
1525
+ *
1526
+ * @example
1527
+ * ```typescript
1528
+ * const { monitors, total } = await client.twitter.stream.listMonitors({
1529
+ * status: "active",
1530
+ * });
1531
+ * console.log(`${total} active monitors`);
1532
+ * ```
1533
+ */
1534
+ async listMonitors(options) {
1535
+ const params = {
1536
+ page: options?.page ?? 1,
1537
+ page_size: options?.pageSize ?? 20,
1538
+ status: options?.status
1539
+ };
1540
+ return this.client.request("/v1/twitter/stream/monitors", {
1541
+ params
1542
+ });
1543
+ }
1544
+ /**
1545
+ * Get a single stream monitor by ID.
1546
+ *
1547
+ * @param monitorId - UUID of the monitor.
1548
+ * @returns The StreamMonitor.
1549
+ * @throws NotFoundError - No monitor with that ID for this API key.
1550
+ *
1551
+ * @example
1552
+ * ```typescript
1553
+ * const monitor = await client.twitter.stream.getMonitor("550e8400-...");
1554
+ * console.log(`${monitor.name}: ${monitor.status}`);
1555
+ * ```
1556
+ */
1557
+ async getMonitor(monitorId) {
1558
+ return this.client.request(`/v1/twitter/stream/monitors/${monitorId}`);
1559
+ }
1560
+ /**
1561
+ * Partially update a stream monitor.
1562
+ *
1563
+ * Only fields that are explicitly set in params are sent to the server.
1564
+ *
1565
+ * @param monitorId - UUID of the monitor.
1566
+ * @param params - Fields to update (all optional).
1567
+ * @returns The updated StreamMonitor.
1568
+ * @throws NotFoundError - Monitor not found for this API key.
1569
+ * @throws InsufficientCreditsError - When resuming with insufficient credits.
1570
+ *
1571
+ * @example
1572
+ * ```typescript
1573
+ * const monitor = await client.twitter.stream.updateMonitor("550e8400-...", {
1574
+ * pollIntervalSeconds: 60,
1575
+ * });
1576
+ * ```
1577
+ */
1578
+ async updateMonitor(monitorId, params) {
1579
+ const body = {};
1580
+ if (params.name !== void 0) body["name"] = params.name;
1581
+ if (params.usernames !== void 0) body["usernames"] = params.usernames;
1582
+ if (params.pollIntervalSeconds !== void 0)
1583
+ body["poll_interval_seconds"] = params.pollIntervalSeconds;
1584
+ if (params.status !== void 0) body["status"] = params.status;
1585
+ if (params.webhookUrl !== void 0) body["webhook_url"] = params.webhookUrl;
1586
+ if (params.webhookSecret !== void 0) body["webhook_secret"] = params.webhookSecret;
1587
+ return this.client.request(`/v1/twitter/stream/monitors/${monitorId}`, {
1588
+ method: "PATCH",
1589
+ body
1590
+ });
1591
+ }
1592
+ /**
1593
+ * Pause an active stream monitor.
1594
+ *
1595
+ * Convenience wrapper around updateMonitor({ status: "paused" }).
1596
+ *
1597
+ * @param monitorId - UUID of the monitor.
1598
+ * @returns The updated StreamMonitor with status="paused".
1599
+ */
1600
+ async pauseMonitor(monitorId) {
1601
+ return this.updateMonitor(monitorId, { status: "paused" });
1602
+ }
1603
+ /**
1604
+ * Resume a paused stream monitor.
1605
+ *
1606
+ * Convenience wrapper around updateMonitor({ status: "active" }).
1607
+ *
1608
+ * @param monitorId - UUID of the monitor.
1609
+ * @returns The updated StreamMonitor with status="active".
1610
+ * @throws InsufficientCreditsError - If credits are below the tier threshold.
1611
+ */
1612
+ async resumeMonitor(monitorId) {
1613
+ return this.updateMonitor(monitorId, { status: "active" });
1614
+ }
1615
+ /**
1616
+ * Delete a stream monitor and all its associated logs. Irreversible.
1617
+ *
1618
+ * @param monitorId - UUID of the monitor.
1619
+ * @throws NotFoundError - Monitor not found for this API key.
1620
+ *
1621
+ * @example
1622
+ * ```typescript
1623
+ * await client.twitter.stream.deleteMonitor("550e8400-...");
1624
+ * ```
1625
+ */
1626
+ async deleteMonitor(monitorId) {
1627
+ await this.client.request(`/v1/twitter/stream/monitors/${monitorId}`, {
1628
+ method: "DELETE"
1629
+ });
1630
+ }
1631
+ // ===========================================================================
1632
+ // Delivery and Billing Logs
1633
+ // ===========================================================================
1634
+ /**
1635
+ * List tweet delivery logs.
1636
+ *
1637
+ * @param options - Filter and pagination options.
1638
+ * @returns DeliveryLogList with pagination metadata.
1639
+ */
1640
+ async listDeliveryLogs(options) {
1641
+ const params = {
1642
+ page: options?.page ?? 1,
1643
+ page_size: options?.pageSize ?? 20,
1644
+ sort: options?.sort ?? "desc",
1645
+ monitor_id: options?.monitorId,
1646
+ author_username: options?.authorUsername,
1647
+ delivery_status: options?.deliveryStatus
1648
+ };
1649
+ return this.client.request("/v1/twitter/stream/logs", { params });
1650
+ }
1651
+ /**
1652
+ * List billing activity logs.
1653
+ *
1654
+ * @param options - Filter and pagination options.
1655
+ * @returns BillingLogList with pagination metadata.
1656
+ */
1657
+ async listBillingLogs(options) {
1658
+ const params = {
1659
+ page: options?.page ?? 1,
1660
+ page_size: options?.pageSize ?? 20,
1661
+ monitor_id: options?.monitorId
1662
+ };
1663
+ return this.client.request("/v1/twitter/stream/billing-logs", { params });
1664
+ }
1665
+ // ===========================================================================
1666
+ // Filter Rules CRUD
1667
+ // ===========================================================================
1668
+ /**
1669
+ * Create a new tweet filter rule.
1670
+ *
1671
+ * @param params - Filter rule configuration.
1672
+ * @returns The created FilterRuleResponse.
1673
+ * @throws ValidationError - Invalid query or interval (422).
1674
+ * @throws InsufficientCreditsError - Credit balance below tier threshold (402).
1675
+ * @throws AuthenticationError - Invalid API key (401).
1676
+ *
1677
+ * @example
1678
+ * ```typescript
1679
+ * const rule = await client.twitter.stream.createFilterRule({
1680
+ * tag: "python news",
1681
+ * query: "#python lang:en -is:retweet",
1682
+ * interval_seconds: 60,
1683
+ * });
1684
+ * console.log(`Created: ${rule.id}, tier: ${rule.pricing_tier}`);
1685
+ * ```
1686
+ */
1687
+ async createFilterRule(params) {
1688
+ const body = {
1689
+ tag: params.tag,
1690
+ query: params.query,
1691
+ interval_seconds: params.interval_seconds
1692
+ };
1693
+ if (params.webhook_url !== void 0) body["webhook_url"] = params.webhook_url;
1694
+ if (params.webhook_secret !== void 0) body["webhook_secret"] = params.webhook_secret;
1695
+ if (params.max_results_per_poll !== void 0)
1696
+ body["max_results_per_poll"] = params.max_results_per_poll;
1697
+ return this.client.request("/v1/twitter/stream/filter-rules", {
1698
+ method: "POST",
1699
+ body
1700
+ });
1701
+ }
1702
+ /**
1703
+ * List filter rules for the authenticated API key.
1704
+ *
1705
+ * @param options - Filter and pagination options.
1706
+ * @returns FilterRuleListResponse with pagination metadata.
1707
+ *
1708
+ * @example
1709
+ * ```typescript
1710
+ * const { rules, total } = await client.twitter.stream.listFilterRules({
1711
+ * status: "active",
1712
+ * });
1713
+ * console.log(`${total} active rules`);
1714
+ * ```
1715
+ */
1716
+ async listFilterRules(options) {
1717
+ const params = {
1718
+ limit: options?.limit ?? 20,
1719
+ offset: options?.offset ?? 0,
1720
+ status: options?.status
1721
+ };
1722
+ return this.client.request("/v1/twitter/stream/filter-rules", {
1723
+ params
1724
+ });
1725
+ }
1726
+ /**
1727
+ * Get a single filter rule by ID.
1728
+ *
1729
+ * @param ruleId - UUID of the filter rule.
1730
+ * @returns The FilterRuleResponse.
1731
+ * @throws NotFoundError - No rule with that ID for this API key.
1732
+ *
1733
+ * @example
1734
+ * ```typescript
1735
+ * const rule = await client.twitter.stream.getFilterRule("550e8400-...");
1736
+ * console.log(`${rule.tag}: ${rule.status}`);
1737
+ * ```
1738
+ */
1739
+ async getFilterRule(ruleId) {
1740
+ return this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`);
1741
+ }
1742
+ /**
1743
+ * Partially update a filter rule.
1744
+ *
1745
+ * Only fields that are explicitly set in params are sent to the server.
1746
+ *
1747
+ * @param ruleId - UUID of the filter rule.
1748
+ * @param params - Fields to update (all optional).
1749
+ * @returns The updated FilterRuleResponse.
1750
+ * @throws NotFoundError - Rule not found for this API key.
1751
+ * @throws ValidationError - Invalid field values (422).
1752
+ *
1753
+ * @example
1754
+ * ```typescript
1755
+ * const rule = await client.twitter.stream.updateFilterRule("550e8400-...", {
1756
+ * interval_seconds: 120,
1757
+ * });
1758
+ * ```
1759
+ */
1760
+ async updateFilterRule(ruleId, params) {
1761
+ const body = {};
1762
+ if (params.tag !== void 0) body["tag"] = params.tag;
1763
+ if (params.query !== void 0) body["query"] = params.query;
1764
+ if (params.interval_seconds !== void 0) body["interval_seconds"] = params.interval_seconds;
1765
+ if (params.status !== void 0) body["status"] = params.status;
1766
+ if (params.webhook_url !== void 0) body["webhook_url"] = params.webhook_url;
1767
+ if (params.webhook_secret !== void 0) body["webhook_secret"] = params.webhook_secret;
1768
+ if (params.max_results_per_poll !== void 0)
1769
+ body["max_results_per_poll"] = params.max_results_per_poll;
1770
+ return this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`, {
1771
+ method: "PATCH",
1772
+ body
1773
+ });
1774
+ }
1775
+ /**
1776
+ * Delete a filter rule and all its associated logs. Irreversible.
1777
+ *
1778
+ * @param ruleId - UUID of the filter rule.
1779
+ * @throws NotFoundError - Rule not found for this API key.
1780
+ *
1781
+ * @example
1782
+ * ```typescript
1783
+ * await client.twitter.stream.deleteFilterRule("550e8400-...");
1784
+ * ```
1785
+ */
1786
+ async deleteFilterRule(ruleId) {
1787
+ await this.client.request(`/v1/twitter/stream/filter-rules/${ruleId}`, {
1788
+ method: "DELETE"
1789
+ });
1790
+ }
1791
+ // ===========================================================================
1792
+ // Filter Rules Utility
1793
+ // ===========================================================================
1794
+ /**
1795
+ * Validate a Twitter search query before creating a rule.
1796
+ *
1797
+ * @param query - The Twitter search query to validate.
1798
+ * @returns FilterRuleValidateResponse with validity and sample result count.
1799
+ *
1800
+ * @example
1801
+ * ```typescript
1802
+ * const result = await client.twitter.stream.validateFilterRuleQuery(
1803
+ * "#python lang:en -is:retweet"
1804
+ * );
1805
+ * if (!result.valid) {
1806
+ * console.error("Invalid query:", result.error);
1807
+ * }
1808
+ * ```
1809
+ */
1810
+ async validateFilterRuleQuery(query) {
1811
+ return this.client.request(
1812
+ "/v1/twitter/stream/filter-rules/validate",
1813
+ { method: "POST", body: { query } }
1814
+ );
1815
+ }
1816
+ /**
1817
+ * List tweet delivery logs for a specific filter rule.
1818
+ *
1819
+ * @param ruleId - UUID of the filter rule.
1820
+ * @param options - Filter and pagination options.
1821
+ * @returns FilterRuleDeliveryLogListResponse with pagination metadata.
1822
+ *
1823
+ * @example
1824
+ * ```typescript
1825
+ * const { logs, total } = await client.twitter.stream.listFilterRuleLogs(
1826
+ * "550e8400-...",
1827
+ * { limit: 50, deliveryStatus: "webhook_delivered" }
1828
+ * );
1829
+ * ```
1830
+ */
1831
+ async listFilterRuleLogs(ruleId, options) {
1832
+ const params = {
1833
+ limit: options?.limit ?? 20,
1834
+ offset: options?.offset ?? 0,
1835
+ sort: options?.sort ?? "desc",
1836
+ delivery_status: options?.deliveryStatus
1837
+ };
1838
+ return this.client.request(
1839
+ `/v1/twitter/stream/filter-rules/${ruleId}/logs`,
1840
+ { params }
1841
+ );
1842
+ }
1843
+ /**
1844
+ * Get all available filter rule pricing tiers.
1845
+ *
1846
+ * @returns FilterRulePricingTiersResponse listing all tiers.
1847
+ *
1848
+ * @example
1849
+ * ```typescript
1850
+ * const { tiers } = await client.twitter.stream.getFilterRulePricingTiers();
1851
+ * tiers.forEach((t) => console.log(t.tier_label, t.credits_per_rule_per_day));
1852
+ * ```
1853
+ */
1854
+ async getFilterRulePricingTiers() {
1855
+ return this.client.request(
1856
+ "/v1/twitter/stream/filter-rules-pricing"
1857
+ );
1858
+ }
1859
+ // ===========================================================================
1860
+ // WebSocket Streaming -- EventEmitter style
1861
+ // ===========================================================================
1862
+ /**
1863
+ * Connect to the WebSocket stream and return an EventEmitter.
1864
+ *
1865
+ * The caller subscribes to events via `.on("tweet", handler)`.
1866
+ * The SDK handles pong replies to server pings automatically.
1867
+ * Call `.close()` on the emitter to disconnect cleanly.
1868
+ *
1869
+ * If reconnect is true, the emitter automatically reconnects after
1870
+ * disconnects (other than auth failures). A new "connected" event is
1871
+ * emitted on each reconnect.
1872
+ *
1873
+ * @param options - Connection options (reconnect, delay, maxReconnects).
1874
+ * @returns StreamEmitter -- an EventEmitter subclass.
1875
+ *
1876
+ * @example
1877
+ * ```typescript
1878
+ * const stream = client.twitter.stream.connect();
1879
+ * stream.on("connected", (e) => console.log("Connected:", e.connectionId));
1880
+ * stream.on("tweet", (event) => {
1881
+ * console.log(`@${event.authorUsername}: ${event.tweet.text}`);
1882
+ * console.log(` latency: ${event.latencyMs}ms`);
1883
+ * });
1884
+ * stream.on("error", (err) => console.error("Stream error:", err));
1885
+ * stream.on("close", () => console.log("Stream closed"));
1886
+ *
1887
+ * // Later:
1888
+ * stream.close();
1889
+ * ```
1890
+ */
1891
+ connect(options = {}) {
1892
+ const { reconnect = false, reconnectDelaySeconds = 90, maxReconnects } = options;
1893
+ const delay = Math.max(MIN_RECONNECT_DELAY_SECONDS, reconnectDelaySeconds) * 1e3;
1894
+ const wsUrl = wsUrlFromBase(this.client.config.baseUrl);
1895
+ const apiKey = this.client.config.apiKey;
1896
+ const emitter = new EventEmitter();
1897
+ let ws = null;
1898
+ let closed = false;
1899
+ let reconnectCount = 0;
1900
+ const connectOnce = () => {
1901
+ ws = new WebSocket(wsUrl, { headers: { "x-api-key": apiKey } });
1902
+ ws.on("message", (data) => {
1903
+ let raw;
1904
+ try {
1905
+ raw = JSON.parse(String(data));
1906
+ } catch {
1907
+ return;
1908
+ }
1909
+ const event = parseEvent(raw);
1910
+ if (event.type === "ping") {
1911
+ ws?.send(JSON.stringify({ type: "pong" }));
1912
+ emitter.emit("ping", event);
1913
+ return;
1914
+ }
1915
+ if (event.type === "error") {
1916
+ const code = event.code;
1917
+ const err = new WebSocketStreamError(event.message, code);
1918
+ emitter.emit("error", err);
1919
+ if (code === 4001 || code === 4003) {
1920
+ closed = true;
1921
+ ws?.close();
1922
+ }
1923
+ return;
1924
+ }
1925
+ emitter.emit(event.type, event);
1926
+ });
1927
+ ws.on("open", () => {
1928
+ });
1929
+ ws.on("close", (code, reason) => {
1930
+ if (closed) {
1931
+ emitter.emit("close");
1932
+ return;
1933
+ }
1934
+ if (!reconnect) {
1935
+ const reasonStr = reason instanceof Buffer ? reason.toString() : String(reason ?? "");
1936
+ emitter.emit(
1937
+ "error",
1938
+ new WebSocketStreamError(`WebSocket closed: ${reasonStr || String(code)}`)
1939
+ );
1940
+ emitter.emit("close");
1941
+ return;
1942
+ }
1943
+ if (maxReconnects !== void 0 && reconnectCount >= maxReconnects) {
1944
+ emitter.emit(
1945
+ "error",
1946
+ new WebSocketStreamError(`Max reconnects (${maxReconnects}) exhausted`)
1947
+ );
1948
+ emitter.emit("close");
1949
+ return;
1950
+ }
1951
+ reconnectCount++;
1952
+ setTimeout(connectOnce, delay);
1953
+ });
1954
+ ws.on("error", (err) => {
1955
+ const message = err instanceof Error ? err.message : String(err);
1956
+ emitter.emit("error", new WebSocketStreamError(message));
1957
+ });
1958
+ };
1959
+ emitter.close = () => {
1960
+ closed = true;
1961
+ ws?.close(1e3, "Client closed");
1962
+ };
1963
+ connectOnce();
1964
+ return emitter;
1965
+ }
1966
+ // ===========================================================================
1967
+ // WebSocket Streaming -- AsyncIterator style
1968
+ // ===========================================================================
1969
+ /**
1970
+ * Connect to the WebSocket stream and return an AsyncIterator.
1971
+ *
1972
+ * Iterates over StreamEvent objects. The SDK handles pong replies
1973
+ * automatically but still yields PingEvent to the caller (the caller
1974
+ * may ignore ping events).
1975
+ *
1976
+ * @param options - Connection options (reconnect, delay, maxReconnects).
1977
+ * @yields StreamEvent
1978
+ *
1979
+ * @example
1980
+ * ```typescript
1981
+ * for await (const event of client.twitter.stream.connectIter()) {
1982
+ * if (event.type === "tweet") {
1983
+ * console.log(`@${event.authorUsername}: ${event.latencyMs}ms`);
1984
+ * }
1985
+ * }
1986
+ * ```
1987
+ *
1988
+ * @example With auto-reconnect
1989
+ * ```typescript
1990
+ * for await (const event of client.twitter.stream.connectIter({
1991
+ * reconnect: true,
1992
+ * reconnectDelaySeconds: 90,
1993
+ * })) {
1994
+ * if (event.type === "tweet") {
1995
+ * // process(event);
1996
+ * }
1997
+ * }
1998
+ * ```
1999
+ */
2000
+ async *connectIter(options = {}) {
2001
+ const { reconnect = false, reconnectDelaySeconds = 90, maxReconnects } = options;
2002
+ const delay = Math.max(MIN_RECONNECT_DELAY_SECONDS, reconnectDelaySeconds) * 1e3;
2003
+ const wsUrl = wsUrlFromBase(this.client.config.baseUrl);
2004
+ const apiKey = this.client.config.apiKey;
2005
+ let reconnectCount = 0;
2006
+ while (true) {
2007
+ const events = [];
2008
+ let resolveWait = null;
2009
+ let rejectWait = null;
2010
+ let done = false;
2011
+ const ws = new WebSocket(wsUrl, { headers: { "x-api-key": apiKey } });
2012
+ const waitForEvent = () => new Promise((res, rej) => {
2013
+ resolveWait = res;
2014
+ rejectWait = rej;
2015
+ });
2016
+ ws.on("message", (data) => {
2017
+ let raw;
2018
+ try {
2019
+ raw = JSON.parse(String(data));
2020
+ } catch {
2021
+ return;
2022
+ }
2023
+ const event = parseEvent(raw);
2024
+ if (event.type === "ping") {
2025
+ ws.send(JSON.stringify({ type: "pong" }));
2026
+ }
2027
+ if (event.type === "error") {
2028
+ const code = event.code;
2029
+ if (code === 4001 || code === 4003) {
2030
+ rejectWait?.(new WebSocketStreamError(event.message, code));
2031
+ rejectWait = null;
2032
+ resolveWait = null;
2033
+ return;
2034
+ }
2035
+ }
2036
+ events.push(event);
2037
+ resolveWait?.();
2038
+ resolveWait = null;
2039
+ rejectWait = null;
2040
+ });
2041
+ ws.on("close", () => {
2042
+ done = true;
2043
+ resolveWait?.();
2044
+ resolveWait = null;
2045
+ rejectWait = null;
2046
+ });
2047
+ ws.on("error", (err) => {
2048
+ const message = err instanceof Error ? err.message : String(err);
2049
+ rejectWait?.(new WebSocketStreamError(message));
2050
+ rejectWait = null;
2051
+ resolveWait = null;
2052
+ });
2053
+ try {
2054
+ while (!done || events.length > 0) {
2055
+ if (events.length === 0) {
2056
+ await waitForEvent();
2057
+ }
2058
+ while (events.length > 0) {
2059
+ yield events.shift();
2060
+ }
2061
+ }
2062
+ } catch (err) {
2063
+ ws.close();
2064
+ throw err;
2065
+ } finally {
2066
+ ws.close();
2067
+ }
2068
+ if (!reconnect) {
2069
+ return;
2070
+ }
2071
+ if (maxReconnects !== void 0 && reconnectCount >= maxReconnects) {
2072
+ throw new WebSocketStreamError(`Max reconnects (${maxReconnects}) exhausted`);
2073
+ }
2074
+ reconnectCount++;
2075
+ await new Promise((res) => setTimeout(res, delay));
2076
+ }
2077
+ }
2078
+ };
2079
+ function verifyWebhookSignature(secret, body, signatureHeader) {
2080
+ if (!signatureHeader.startsWith("sha256=")) {
2081
+ return false;
2082
+ }
2083
+ const expectedHex = signatureHeader.slice("sha256=".length);
2084
+ const bodyBuffer = typeof body === "string" ? Buffer.from(body, "utf-8") : body;
2085
+ const actualHex = createHmac("sha256", secret).update(bodyBuffer).digest("hex");
2086
+ try {
2087
+ return timingSafeEqual(Buffer.from(expectedHex, "hex"), Buffer.from(actualHex, "hex"));
2088
+ } catch {
2089
+ return false;
2090
+ }
2091
+ }
1406
2092
 
1407
2093
  // src/twitter/client.ts
1408
2094
  var TwitterClient = class {
@@ -1418,6 +2104,8 @@ var TwitterClient = class {
1418
2104
  trends;
1419
2105
  /** Client for geo/places operations */
1420
2106
  geo;
2107
+ /** Client for real-time stream monitor management and WebSocket streaming */
2108
+ stream;
1421
2109
  /**
1422
2110
  * Create a new Twitter client.
1423
2111
  *
@@ -1430,6 +2118,7 @@ var TwitterClient = class {
1430
2118
  this.communities = new CommunitiesClient(client);
1431
2119
  this.trends = new TrendsClient(client);
1432
2120
  this.geo = new GeoClient(client);
2121
+ this.stream = new StreamClient(client);
1433
2122
  }
1434
2123
  };
1435
2124
 
@@ -1578,6 +2267,6 @@ var ScrapeBadger = class {
1578
2267
  }
1579
2268
  };
1580
2269
 
1581
- export { AccountRestrictedError, AuthenticationError, CommunitiesClient, GeoClient, InsufficientCreditsError, ListsClient, NotFoundError, RateLimitError, ScrapeBadger, ScrapeBadgerError, ServerError, TimeoutError, TrendsClient, TweetsClient, TwitterClient, UsersClient, ValidationError, WebClient, collectAll };
2270
+ export { AccountRestrictedError, AuthenticationError, CommunitiesClient, ConflictError, GeoClient, InsufficientCreditsError, ListsClient, NotFoundError, RateLimitError, ScrapeBadger, ScrapeBadgerError, ServerError, StreamClient, TimeoutError, TrendsClient, TweetsClient, TwitterClient, UsersClient, ValidationError, WebClient, WebSocketStreamError, collectAll, verifyWebhookSignature };
1582
2271
  //# sourceMappingURL=index.mjs.map
1583
2272
  //# sourceMappingURL=index.mjs.map