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