cabalspy-id 0.5.1 → 0.5.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +146 -112
  2. package/LICENSE +21 -21
  3. package/README.md +505 -475
  4. package/dist/api.cjs +5 -3
  5. package/dist/api.cjs.map +1 -1
  6. package/dist/api.d.cts +1 -1
  7. package/dist/api.d.ts +1 -1
  8. package/dist/api.js +16 -14
  9. package/dist/chunk-A2ZAFAWC.cjs.map +1 -1
  10. package/dist/chunk-ABFIPCBY.cjs +109 -0
  11. package/dist/chunk-ABFIPCBY.cjs.map +1 -0
  12. package/dist/{chunk-Z3UPDOBY.js → chunk-DBYCRBVS.js} +9 -7
  13. package/dist/chunk-DBYCRBVS.js.map +1 -0
  14. package/dist/{chunk-P3MDLWLN.cjs → chunk-PKFIMBPW.cjs} +113 -54
  15. package/dist/chunk-PKFIMBPW.cjs.map +1 -0
  16. package/dist/{chunk-L4RHSWOA.cjs → chunk-ULEBNY2A.cjs} +121 -212
  17. package/dist/chunk-ULEBNY2A.cjs.map +1 -0
  18. package/dist/{chunk-X3OFGGSE.cjs → chunk-VDFCMYLR.cjs} +50 -48
  19. package/dist/chunk-VDFCMYLR.cjs.map +1 -0
  20. package/dist/chunk-VOTYIQAY.js +109 -0
  21. package/dist/chunk-VOTYIQAY.js.map +1 -0
  22. package/dist/{chunk-3LKLY5WQ.js → chunk-XLBO2RSL.js} +15 -106
  23. package/dist/chunk-XLBO2RSL.js.map +1 -0
  24. package/dist/{chunk-3P7QK2PG.js → chunk-Z23B5TY6.js} +89 -30
  25. package/dist/chunk-Z23B5TY6.js.map +1 -0
  26. package/dist/index.cjs +13 -5
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +3 -3
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +25 -17
  31. package/dist/index.js.map +1 -1
  32. package/dist/react.cjs +55 -2
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +28 -5
  35. package/dist/react.d.ts +28 -5
  36. package/dist/react.js +55 -2
  37. package/dist/react.js.map +1 -1
  38. package/dist/thesis.cjs +11 -3
  39. package/dist/thesis.cjs.map +1 -1
  40. package/dist/thesis.d.cts +42 -11
  41. package/dist/thesis.d.ts +42 -11
  42. package/dist/thesis.js +24 -16
  43. package/dist/{users-BDLkwW1p.d.cts → users-3DAw88_U.d.cts} +4 -4
  44. package/dist/{users-CDlDRuyv.d.ts → users-B4r33i4O.d.ts} +4 -4
  45. package/dist/widget.cjs +28 -12
  46. package/dist/widget.cjs.map +1 -1
  47. package/dist/widget.d.cts +37 -6
  48. package/dist/widget.d.ts +37 -6
  49. package/dist/widget.js +17 -1
  50. package/dist/widget.js.map +1 -1
  51. package/package.json +147 -147
  52. package/dist/chunk-3LKLY5WQ.js.map +0 -1
  53. package/dist/chunk-3P7QK2PG.js.map +0 -1
  54. package/dist/chunk-L4RHSWOA.cjs.map +0 -1
  55. package/dist/chunk-P3MDLWLN.cjs.map +0 -1
  56. package/dist/chunk-X3OFGGSE.cjs.map +0 -1
  57. package/dist/chunk-Z3UPDOBY.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/cabalspy-id-js/cabalspy-id-js/dist/chunk-ULEBNY2A.cjs","../src/types.ts","../src/stream.ts","../src/thesesStream.ts","../src/util.ts","../src/client.ts","../src/webhooks.ts","../src/users.ts"],"names":[],"mappings":"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A;AAAA;AAGM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMM,EAAA;AACJ,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACJ,IAAA;AACA,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWO,EAAA;AACA,IAAA;AACG,MAAA;AACR,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,EAAA;AACO,IAAA;AACG,MAAA;AACR,IAAA;AACO,IAAA;AACT,EAAA;AACF;AAES;AACG,EAAA;AACH,EAAA;AACT;AL2XY;AACA;AMnoCH;AACH,EAAA;AACM,IAAA;AACV,EAAA;AACM,EAAA;AACA,EAAA;AACI,EAAA;AACH,EAAA;AACT;AAES;AACH,EAAA;AACG,EAAA;AACT;AAES;AACH,EAAA;AACK,IAAA;AACD,EAAA;AACA,IAAA;AACR,EAAA;AACF;AAEA;AACY,EAAA;AACJ,EAAA;AACC,EAAA;AACT;AAES;AACD,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACD,EAAA;AACT;AAMA;AAMS,EAAA;AACT;AASA;AACU,EAAA;AACH,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACG,IAAA;AACE,MAAA;AACP,IAAA;AACH,EAAA;AACM,EAAA;AACD,EAAA;AACG,IAAA;AACR,EAAA;AACM,EAAA;AACA,EAAA;AACI,EAAA;AACN,EAAA;AAEE,EAAA;AACA,EAAA;AACA,EAAA;AAKK,IAAA;AACA,IAAA;AACR,EAAA;AACE,EAAA;AAED,EAAA;AACK,IAAA;AACD,EAAA;AACA,IAAA;AACR,EAAA;AACF;ANymCY;AACA;AOttCI;AACV,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAUgB;AACH,EAAA;AACF,EAAA;AACA,EAAA;AACH,EAAA;AACC,EAAA;AACT;AP+sCY;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/cabalspy-id-js/cabalspy-id-js/dist/chunk-ULEBNY2A.cjs","sourcesContent":[null,"/**\n * Response shapes of the CabalSpy ID Data API, derived from the server source.\n *\n * Every object type keeps an index signature-free shape for editor help, but\n * the server may add fields later; nothing is stripped at runtime.\n *\n * Timestamps are ISO 8601 UTC strings (\"2026-10-12T08:00:00Z\"), except\n * `PnlCalendarDay.date`, which is \"DD-MM-YY\" like the CabalSpy API.\n */\n\nexport const CHAINS = ['solana', 'bnb', 'base', 'eth', 'rh'] as const;\n/** solana, bnb (BNB Chain), base, eth (Ethereum), rh (Robinhood Chain). */\nexport type Chain = (typeof CHAINS)[number];\n\nexport const PERIODS = ['6h', '1d', '7d', '30d'] as const;\nexport type Period = (typeof PERIODS)[number];\n\nexport const LEADERBOARD_SORTS = ['volume', 'pnl', 'pnl_usd', 'win_rate', 'trades'] as const;\nexport type LeaderboardSort = (typeof LEADERBOARD_SORTS)[number];\n\nexport type Currency = 'SOL' | 'BNB' | 'ETH';\nexport type Scope = 'instance' | 'global';\n\nexport const CURRENCY_BY_CHAIN: Record<Chain, Currency> = {\n solana: 'SOL',\n bnb: 'BNB',\n base: 'ETH',\n eth: 'ETH',\n rh: 'ETH',\n};\n\n// ── envelope ────────────────────────────────────────────────────────────────\n\nexport interface ResponseMeta {\n request_id: string;\n cached: boolean;\n cache_age_seconds: number;\n version: string;\n timestamp: string;\n}\n\nexport interface Pagination {\n limit: number;\n /** Absent on /wallets/history, `null` on /transactions/latest. */\n total?: number | null;\n has_more: boolean;\n next_cursor: string | null;\n}\n\n/** Shape of every successful Data API response. */\nexport interface ApiResponse<T> {\n success: true;\n data: T;\n pagination?: Pagination;\n meta: ResponseMeta;\n}\n\n// ── account ─────────────────────────────────────────────────────────────────\n\nexport interface HealthResponse {\n status: 'ok' | 'degraded';\n /** Seconds since the last trade sync; `null` when unknown. */\n trade_sync_age_seconds: number | null;\n}\n\nexport interface MeResponse {\n /** The registrable domain of a live instance, else the client_id. */\n instance: string;\n client_id: string;\n mode: 'sandbox' | 'development' | 'live';\n /** free | starter | pro | business | enterprise, or \"development\" for non-live apps. */\n plan: string;\n /** Requests per second; `null` = unlimited. */\n rate_limit_rps: number | null;\n /** Whether the key may open the live stream (Pro plan and up). */\n websockets: boolean;\n /** Whether `scope=global` is available (Enterprise). */\n global_scope: boolean;\n users_with_data: number;\n}\n\n// ── users ───────────────────────────────────────────────────────────────────\n\nexport interface VerifiedWallet {\n address: string;\n /** \"solana\" or \"evm\". */\n family: string;\n chains: Chain[];\n verified_at: string | null;\n}\n\nexport interface Profile {\n /** The user's id: the `sub` of their ID token. */\n csid_id: string;\n /** X handle; for people without X `tg.<telegram name>` (signed in with Telegram) or `id.<hex>` (signed in with Google). X handles never contain a dot. */\n username: string;\n name: string | null;\n image_url: string | null;\n bio: string | null;\n /** https://x.com/<handle>; `null` for people without X. */\n twitter: string | null;\n /** https://t.me/<name> when Telegram is connected, else an empty string. */\n telegram: string;\n /** X stats; `null` for people who signed in with Telegram or Google and have not connected X. */\n x: {\n id: string;\n followers: number | null;\n following: number | null;\n verified: boolean | null;\n verified_type: string | null;\n } | null;\n kol: { badge: boolean; since: string | null };\n profile_url: string;\n wallets: VerifiedWallet[];\n joined_at: string | null;\n}\n\nexport interface WalletLookupResponse {\n address: string;\n verified: true;\n wallet: VerifiedWallet | null;\n owner: Omit<Profile, 'wallets'>;\n}\n\n// ── trading ─────────────────────────────────────────────────────────────────\n\n/** Trader block on trading responses. */\nexport interface TraderProfile {\n csid_id: string;\n /** X handle; for people without X `tg.<telegram name>` (signed in with Telegram) or `id.<hex>` (signed in with Google). X handles never contain a dot. */\n username: string;\n name: string | null;\n image_url: string | null;\n /** https://x.com/<handle>; `null` for people without X. */\n twitter: string | null;\n /** https://t.me/<name> when Telegram is connected, else an empty string. */\n telegram: string;\n blockchain: Chain;\n currency: Currency;\n type: 'csid';\n kol: boolean;\n /** e.g. \"13:00-22:00 UTC\"; empty when there were no trades in 30 days. */\n active_hours: string;\n profile_url: string;\n wallet_address?: string;\n}\n\nexport interface PeriodStats {\n period: Period;\n total_buy: number;\n total_buy_usd: number;\n total_sell: number;\n total_sell_usd: number;\n volume: number;\n volume_usd: number;\n /** Cash flow inside the window: sells minus buys. */\n realized_pnl: number;\n realized_pnl_usd: number;\n realized_pnl_percentage: number;\n buy_txn: number;\n sell_txn: number;\n largest_buy: number;\n best_trade_pnl: number;\n worst_trade_pnl: number;\n win_count: number;\n loss_count: number;\n avg_hold_time_minutes: number;\n}\n\nexport interface WinRateBuckets {\n above_500: number;\n above_100_to_500: number;\n above_zero_to_100: number;\n below_zero: number;\n}\n\nexport interface WinRateDistribution extends WinRateBuckets {\n win_count: number;\n closed_count: number;\n win_rate_percentage: number;\n}\n\nexport interface ActiveTokens {\n active_tokens_count: number;\n avg_position_size: number;\n still_holding_count: number;\n}\n\nexport interface HistoryToken {\n mint: string;\n token_name: string | null;\n token_symbol: string | null;\n token_logo: string | null;\n buy_value: number;\n buy_value_usd: number;\n sell_value: number;\n sell_value_usd: number;\n buy_tokens: number;\n sell_tokens: number;\n realized_pnl: number;\n realized_pnl_usd: number;\n realized_pnl_percentage: number;\n currency: Currency;\n remaining_tokens: number;\n}\n\nexport interface PnlChartPoint {\n timestamp: string;\n realized_pnl: number;\n realized_pnl_usd: number;\n}\n\nexport interface TrackerTrade {\n mint: string;\n token_name: string | null;\n token_logo: string | null;\n transaction_type: 'buy' | 'sell';\n buy_token: null;\n sell_token: null;\n buy_value?: number;\n buy_value_usd?: number;\n sell_value?: number;\n sell_value_usd?: number;\n token_amount: number | null;\n created_at: string;\n tx_signature: string;\n}\n\nexport interface TrackerResponse {\n wallet: string;\n profile: TraderProfile;\n period_stats: PeriodStats;\n period_win_rate_distribution: WinRateDistribution;\n period_active_tokens: ActiveTokens;\n period_history_tokens: HistoryToken[];\n /** Cumulative realized PnL, starts at 0, one point every 15 minutes. */\n period_realized_pnl_chart: PnlChartPoint[];\n period_trades: TrackerTrade[];\n}\n\nexport interface LifetimeStats {\n buy: number;\n sell: number;\n realized_pnl: number;\n realized_pnl_percentage: number;\n buy_txn: number;\n sell_txn: number;\n total_tx_count: number;\n win_count: number;\n total_sold: number;\n win_rate: number;\n active_tokens_count: number;\n largest_buy: number;\n best_trade_pnl: number;\n worst_trade_pnl: number;\n still_holding_count: number;\n}\n\nexport interface TokenOverview {\n mint: string;\n token_name: string | null;\n token_logo: string | null;\n invested: number;\n sold: number;\n realized_pnl: number;\n realized_pnl_percentage: number;\n currency: Currency;\n buy_txn: number;\n sell_txn: number;\n first_seen: string;\n last_seen: string;\n still_holding: boolean;\n}\n\nexport interface HistoryTrade {\n tx_signature: string;\n mint: string;\n token_name: string | null;\n token_logo: string | null;\n transaction_type: 'buy' | 'sell';\n value: number;\n value_usd: number;\n currency: Currency;\n created_at: string;\n}\n\nexport interface HistoryResponse {\n lifetime_stats: LifetimeStats;\n win_rate_distribution: WinRateBuckets;\n token_overview: TokenOverview[];\n /** One page of trades, newest first. */\n trades: HistoryTrade[];\n profile: TraderProfile;\n}\n\nexport interface HoldingToken {\n token_address: string | null;\n token_name: string | null;\n symbol: string | null;\n logo: string | null;\n amount: string;\n amount_formatted: string;\n decimals: number | null;\n price_usd: number;\n value_usd: number;\n verified_contract: boolean | null;\n avg_buy_price_usd?: number | null;\n unrealized_pnl_usd?: number | null;\n}\n\nexport interface HoldingsResponse {\n wallet: string;\n blockchain: Chain;\n profile: TraderProfile;\n active_holdings: {\n tokens: HoldingToken[];\n count: number;\n total_value_usd: number;\n last_updated: string;\n stale: boolean;\n loading: boolean;\n };\n}\n\nexport interface PnlCalendarDay {\n /** \"DD-MM-YY\" */\n date: string;\n realized_pnl: number;\n realized_pnl_usd: number;\n buy_txn: number;\n sell_txn: number;\n txn_total: number;\n tokens_traded: number;\n volume: number;\n total_buy: number;\n total_sell: number;\n total_buy_usd: number;\n total_sell_usd: number;\n}\n\nexport interface PnlCalendarMonth {\n month: number;\n year: number;\n dates: PnlCalendarDay[];\n realized_pnl: number;\n realized_pnl_usd: number;\n volume: number;\n total_buy: number;\n total_buy_usd: number;\n total_sell: number;\n total_sell_usd: number;\n buy_txn: number;\n sell_txn: number;\n txn_total: number;\n tokens_traded: number;\n positive_streak: number;\n negative_streak: number;\n /** Only on the current month. */\n current_positive_streak?: number;\n current_negative_streak?: number;\n}\n\nexport interface PnlCalendarResponse {\n wallet: string;\n profile: TraderProfile;\n /** Oldest first. */\n months: PnlCalendarMonth[];\n /** Days of the current month. */\n dates: PnlCalendarDay[];\n month: PnlCalendarMonth | null;\n}\n\nexport interface LeaderboardEntry {\n rank: number;\n wallet: string;\n profile: {\n csid_id: string;\n username: string;\n name: string | null;\n image_url: string | null;\n /** https://x.com/<handle>; `null` for people without X. */\n twitter: string | null;\n /** https://t.me/<name> when Telegram is connected, else an empty string. */\n telegram: string;\n blockchain: Chain;\n currency: Currency;\n wallet_type: 'csid';\n kol: boolean;\n public_trading: boolean;\n };\n period_stats: {\n period: Period;\n total_buy: number;\n total_buy_usd: number;\n total_sell: number;\n total_sell_usd: number;\n total_volume: number;\n total_volume_usd: number;\n realized_pnl: number;\n realized_pnl_usd: number;\n realized_pnl_percentage: number;\n avg_hold_time_minutes: number;\n total_txn: number;\n buy_txn: number;\n sell_txn: number;\n };\n period_win_rate_distribution: { win_count: number; closed_count: number; win_rate_percentage: number };\n period_history_tokens: { total_tokens: number };\n}\n\nexport interface LeaderboardResponse {\n blockchain: Chain;\n type: 'csid';\n scope: Scope;\n period: Period;\n sort: LeaderboardSort;\n currency: Currency;\n /** The leaderboard paginates inside `data`. */\n pagination: Pagination;\n leaderboard: LeaderboardEntry[];\n}\n\n/** Trader summary attached to trades. */\nexport interface TradeOwner {\n csid_id: string;\n username: string;\n name: string | null;\n image_url: string | null;\n kol: boolean;\n}\n\n/** One trade, from /transactions/latest and the live stream. */\nexport interface Trade {\n signature: string;\n blockchain: Chain;\n wallet: string;\n mint: string;\n transaction_type: 'buy' | 'sell';\n token_symbol: string | null;\n token_name: string | null;\n token_logo: string | null;\n token_amount: number | null;\n value: number;\n value_usd: number;\n currency: Currency;\n timestamp: string;\n trader: TradeOwner | null;\n}\n\n// ── OpenID Connect ──────────────────────────────────────────────────────────\n\n/**\n * `thesis` lets your app post theses and replies, react and report in the\n * user's name (Data API writes with `csid_id`, or the public thesis API and\n * the thesis widget with the user's access token).\n */\nexport type OidcScope = 'openid' | 'profile' | 'wallets' | 'onchain' | 'thesis' | 'offline_access';\n\nexport interface XProfileClaim {\n id: string;\n username: string;\n followers: number | null;\n following: number | null;\n posts: number | null;\n listed: number | null;\n verified: boolean | null;\n verified_type: string | null;\n created_at: string | null;\n fetched_at: string;\n}\n\nexport interface WalletClaim {\n address: string;\n family: string;\n chains: Chain[];\n verified_at: string | null;\n}\n\n/**\n * Claims of an ID token or a userinfo response.\n *\n * When the platform is over its plan's user limit, `sub` is an anonymous\n * `anon_…` id, `limited` is true and no profile or wallet data is included.\n */\nexport interface CabalSpyIDClaims {\n sub: string;\n iss?: string;\n aud?: string;\n azp?: string;\n exp?: number;\n iat?: number;\n auth_time?: number;\n nonce?: string;\n name?: string | null;\n /** X handle; for people without X `tg.<telegram name>` (signed in with Telegram) or `id.<hex>` (signed in with Google). X handles never contain a dot. */\n preferred_username?: string;\n picture?: string | null;\n /** https://id.cabalspy.xyz/u/<username> */\n profile?: string;\n kol_badge?: boolean;\n /**\n * Full X profile, userinfo only (scope `profile`). `null` for people who signed\n * in with Telegram or Google and have not connected X. See hasX().\n */\n x?: XProfileClaim | null;\n /** Verified wallets, userinfo only (scope `wallets`). */\n wallets?: WalletClaim[];\n /** Latest 7-day on-chain volume, userinfo only (scope `onchain`). */\n onchain?: { volume_7d_usd: number; checked_at: string } | null;\n /** A made-up sandbox identity; never trust it in production. */\n sandbox?: boolean;\n /** True when the platform is over its plan's user limit. */\n limited?: boolean;\n reason?: 'plan_user_limit' | string;\n message?: string;\n [claim: string]: unknown;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: 'Bearer';\n /** Seconds. Access and ID tokens live 15 minutes. */\n expires_in: number;\n id_token: string;\n scope: string;\n /** Only with the `offline_access` scope. Rotates on every refresh. */\n refresh_token?: string;\n}\n\nexport interface DiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n userinfo_endpoint: string;\n jwks_uri: string;\n revocation_endpoint: string;\n scopes_supported: string[];\n response_types_supported: string[];\n grant_types_supported: string[];\n id_token_signing_alg_values_supported: string[];\n token_endpoint_auth_methods_supported: string[];\n code_challenge_methods_supported: string[];\n claims_supported: string[];\n [key: string]: unknown;\n}\n\n// ── webhooks ────────────────────────────────────────────────────────────────\n\nexport type WebhookEventType =\n | 'wallet.verified'\n | 'wallet.removed'\n | 'profile.updated'\n | 'kol.granted'\n | 'consent.revoked'\n | 'thesis.created'\n | 'thesis.reply'\n | 'thesis.reaction'\n | 'webhook.test';\n\nexport interface WebhookEvent<D = Record<string, unknown>> {\n /** Event id; the same on every retry. Dedupe on it. */\n id: string;\n type: WebhookEventType | string;\n created_at: string;\n sandbox: boolean;\n /** Always carries `sub` (the user's CabalSpy ID), except for webhook.test. */\n data: D & { sub?: string };\n}\n\n/** `data` of a `thesis.created` webhook: one of your users posted a thesis. */\nexport interface ThesisCreatedData {\n sub: string;\n thesis: Thesis;\n}\n\n/** `data` of a `thesis.reply` webhook: one of your users replied to a thesis. */\nexport interface ThesisReplyData {\n sub: string;\n reply: Thesis;\n}\n\n/** `data` of a `thesis.reaction` webhook: one of your users added or removed a reaction. */\nexport interface ThesisReactionData {\n sub: string;\n thesis_id: string;\n chain: Chain;\n mint: string;\n emoji: ThesisEmoji | string;\n /** true: added, false: removed. */\n on: boolean;\n /** The new counts. */\n reactions: Partial<Record<ThesisEmoji | string, number>>;\n}\n\n// ── token theses ────────────────────────────────────────────────────────────\n\nexport type ThesisStance = 'bull' | 'bear';\nexport type ThesisSort = 'new' | 'top';\n\n/** Reactions: `like` plus a fixed emoji set. */\nexport const THESIS_EMOJIS = ['like', '🔥', '🚀', '💎', '👀', '😂', '🤝', '💀', '🐂', '🐻'] as const;\nexport type ThesisEmoji = (typeof THESIS_EMOJIS)[number];\n\nexport const THESIS_REPORT_REASONS = ['spam', 'scam', 'abuse', 'impersonation', 'other'] as const;\nexport type ThesisReportReason = (typeof THESIS_REPORT_REASONS)[number];\n\n/** Maximum thesis / reply length in characters. */\nexport const THESIS_MAX_BODY = 1000;\n\nexport interface ThesisAuthor {\n csid_id: string;\n /** X handle; for people without X `tg.<telegram name>` (signed in with Telegram) or `id.<hex>` (signed in with Google). X handles never contain a dot. */\n username: string | null;\n name: string | null;\n image_url: string | null;\n kol_badge: boolean;\n profile_url: string | null;\n}\n\nexport interface ThesisPnl {\n invested_usd: number;\n realized_usd: number;\n holding_value_usd: number | null;\n pnl_usd: number | null;\n pnl_percent: number | null;\n}\n\n/** The author's real position in the token, when they show their trading publicly. */\nexport interface ThesisPositionPublic {\n /** What was bought and sold when the thesis was posted. */\n at_post: ThesisPnl & { tokens_held: number | null; first_buy: string | null };\n /** Live: PnL now, and whether the author still holds or sold. */\n now: (ThesisPnl & { tokens_held: number; status: 'holding' | 'sold' }) | null;\n}\n\n/** The author keeps their trading private: only that they hold the token. */\nexport interface ThesisPositionPrivate {\n private: true;\n}\n\nexport type ThesisPosition = ThesisPositionPublic | ThesisPositionPrivate;\n\n/** Who reacted recently (newest first). */\nexport interface ThesisReactor {\n emoji: ThesisEmoji | string;\n csid_id: string;\n username: string | null;\n at: string;\n}\n\n/** A thesis (`kind: 'thesis'`) or a reply to one (`kind: 'reply'`). */\nexport interface Thesis {\n id: string;\n chain: Chain;\n mint: string;\n kind: 'thesis' | 'reply';\n /** The thesis a reply belongs to. */\n parent_id: string | null;\n body: string;\n stance: ThesisStance | null;\n author: ThesisAuthor;\n likes: number;\n replies: number;\n /** Count per reaction, e.g. { like: 3, '🚀': 1 }. */\n reactions: Partial<Record<ThesisEmoji | string, number>>;\n /** The latest reactions with who reacted, newest first. */\n reacted_by: ThesisReactor[];\n /** The viewer's own reactions (pass `csid_id` / an access token to get them). */\n my_reactions: string[];\n /** Name of the app it was posted through. */\n via: string | null;\n created_at: string;\n is_mine: boolean;\n /** Theses only (not replies). */\n position?: ThesisPosition;\n /** Theses only: a preview of the newest replies, oldest first. */\n latest_replies?: Thesis[];\n}\n\nexport interface ThesisToken {\n chain: Chain;\n mint: string;\n name: string | null;\n symbol: string | null;\n logo: string | null;\n price_usd: number | null;\n}\n\nexport interface ThesisCounts {\n theses: number;\n bull: number;\n bear: number;\n authors: number;\n}\n\nexport interface ThesisSummary extends ThesisCounts {\n token: ThesisToken;\n}\n\nexport interface ThesisEligibility {\n /** Holders only: one of the user's verified wallets bought and still holds the token. */\n can_post: boolean;\n can_reply: boolean;\n holder: boolean | null;\n /** Posting paused after several hidden posts. */\n paused: boolean;\n /** Data API: why the user can't write for your app (consent_required, plan_user_limit, sandbox_user). */\n reason?: string;\n}\n\n/** Moderation notification kinds. */\nexport type ThesisNotificationKind =\n | 'post_hidden'\n | 'paused'\n | 'banned'\n | 'post_removed'\n | 'post_restored'\n | 'appeal_accepted'\n | 'appeal_rejected'\n | 'reporting_paused';\n\n/** A moderation notification (warning, hidden post, pause, appeal result …). */\nexport interface ThesisNotification {\n id: number;\n kind: ThesisNotificationKind | string;\n /** Ready-to-show English text. */\n text: string;\n /** Details, e.g. thesis_id, until, note. */\n data: Record<string, unknown>;\n created_at: string;\n read: boolean;\n /** Can be appealed (within 30 days). */\n appealable: boolean;\n /** Appeal status when appealed: pending | accepted | rejected; null otherwise. */\n appeal: 'pending' | 'accepted' | 'rejected' | string | null;\n}\n\nexport interface ThesisNotifications {\n unread: number;\n items: ThesisNotification[];\n}\n\n/** Unread moderation notices of the signed-in user (public API `me()`). */\nexport interface ThesisNotices {\n unread: number;\n /** Text of the newest notice when there are unread ones. */\n latest: string | null;\n /** Where the user reads all notices on id.cabalspy.xyz. */\n url: string;\n}\n\nexport interface ThesisReactions {\n id: string;\n reactions: Partial<Record<ThesisEmoji | string, number>>;\n}\n\n/** Stream / live payload of a new thesis or reply. */\nexport type ThesisLiveItem = Thesis & { blockchain?: Chain };\n/** Stream / live payload of a removed (deleted or hidden) post. */\nexport interface ThesisRemoved {\n id: string;\n blockchain?: Chain;\n mint?: string;\n}\n/** Stream / live payload of changed reaction counts. */\nexport type ThesisReactionUpdate = ThesisReactions & { blockchain?: Chain; mint?: string };\n","// ═══════════════════════════════════════════════════════════════════════════\n// LIVE STREAM — wss://api-id.cabalspy.xyz/v1/stream (Pro plan and up)\n// ═══════════════════════════════════════════════════════════════════════════\n\nimport {\n BadRequestError,\n CabalSpyIDError,\n ConnectionError,\n PermissionError,\n RateLimitError,\n} from './errors';\nimport {\n CHAINS,\n type Chain,\n type Scope,\n type ThesisLiveItem,\n type ThesisReactionUpdate,\n type ThesisRemoved,\n type Trade,\n} from './types';\n\n/** `trades`: your users' trades. `theses`: new theses, replies, reactions and removals (public, all users). */\nexport type StreamTopic = 'trades' | 'theses';\n\nexport interface StreamOptions {\n /** What to receive. Default: ['trades'] */\n events?: StreamTopic[];\n /** Only this chain. Default: every chain. */\n blockchain?: Chain;\n /** Theses of one token only (needs `blockchain`). */\n mint?: string;\n /** `global` (every CabalSpy ID user) is Enterprise only. Default: instance */\n scope?: Scope;\n /** Instance client_id, when your account has several. */\n instance?: string;\n /** Reconnect automatically after a dropped connection. Default: true */\n reconnect?: boolean;\n /** Initial reconnect delay in ms, doubling up to maxReconnectDelay. Default: 1000 */\n reconnectDelay?: number;\n /** Default: 30_000 */\n maxReconnectDelay?: number;\n /**\n * The server sends a heartbeat every 30 s. When nothing at all arrives for\n * this many ms, the connection is treated as dead and reopened.\n * 0 disables the check. Default: 75_000\n */\n heartbeatTimeout?: number;\n /**\n * WebSocket implementation. Browsers, Deno, Bun and Node 22+ have a global\n * WebSocket. On Node 18-21, `npm i ws` and pass `{ WebSocketImpl: WebSocket }`.\n */\n WebSocketImpl?: unknown;\n}\n\nexport interface SubscribedEvent {\n blockchain: Chain | 'all';\n scope: Scope;\n events?: StreamTopic[];\n mint?: string | null;\n}\n\nexport interface HeartbeatEvent {\n ts: number;\n}\n\nexport interface CloseEvent {\n code?: number;\n reason?: string;\n}\n\nexport interface ReconnectEvent {\n attempt: number;\n delay: number;\n}\n\nexport interface StreamEventMap {\n trade: Trade;\n /** A new thesis (events: theses). */\n thesis: ThesisLiveItem;\n /** A new reply to a thesis. */\n 'thesis.reply': ThesisLiveItem;\n /** Reaction counts of a thesis or reply changed. */\n 'thesis.reaction': ThesisReactionUpdate;\n /** A thesis or reply was deleted or hidden. */\n 'thesis.removed': ThesisRemoved;\n subscribed: SubscribedEvent;\n heartbeat: HeartbeatEvent;\n pong: Record<string, never>;\n open: undefined;\n close: CloseEvent;\n reconnecting: ReconnectEvent;\n error: CabalSpyIDError;\n /** Every parsed message, before dispatch. */\n message: Record<string, unknown>;\n}\n\nexport type StreamEvent = keyof StreamEventMap;\ntype Handler<T> = (payload: T) => void;\n\ninterface MinimalWebSocket {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readyState: number;\n onopen: ((ev: unknown) => void) | null;\n onclose: ((ev: unknown) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n}\n\n/** Server error codes that will not go away by reconnecting. */\nconst FATAL_CODES = new Set([\n 'invalid_api_key',\n 'plan_required',\n 'invalid_parameter',\n 'missing_parameter',\n 'scope_not_allowed',\n 'product_not_enabled',\n]);\n\nfunction streamError(code: string, message: string): CabalSpyIDError {\n const opts = { code };\n if (code === 'invalid_parameter' || code === 'missing_parameter') return new BadRequestError(message, { ...opts, status: 400 });\n if (code === 'too_many_connections') return new RateLimitError(message, { ...opts, status: 429 });\n if (FATAL_CODES.has(code)) return new PermissionError(message, { ...opts, status: 403 });\n return new CabalSpyIDError(message, opts);\n}\n\n/**\n * WebSocket client for the live trade stream.\n *\n * const stream = csid.stream({ blockchain: 'solana' });\n * stream.on('trade', (t) => console.log(t.trader?.username, t.transaction_type, t.value_usd));\n * await stream.connect(); // resolves once the server confirmed the subscription\n *\n * or as an async iterator:\n *\n * for await (const trade of csid.stream()) { … }\n *\n * Reconnects with exponential backoff after a dropped connection or a missed\n * heartbeat. Configuration errors from the server (invalid key, plan without\n * the stream, …) are fatal: they are emitted as 'error' and stop reconnecting.\n */\nexport class CabalSpyIDStream {\n private ws: MinimalWebSocket | null = null;\n private readonly opts: Required<Omit<StreamOptions, 'WebSocketImpl' | 'blockchain' | 'scope' | 'instance' | 'events' | 'mint'>>;\n private readonly handlers = new Map<string, Set<Handler<never>>>();\n private reconnectAttempt = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private watchdog: ReturnType<typeof setTimeout> | null = null;\n private closedByUser = false;\n private fatal: CabalSpyIDError | null = null;\n\n constructor(\n private readonly url: string,\n private readonly apiKey: string,\n private readonly options: StreamOptions = {},\n ) {\n const events = options.events ?? ['trades'];\n if (!events.length || events.some((e) => e !== 'trades' && e !== 'theses')) {\n throw new BadRequestError(\"events must contain 'trades' and/or 'theses'\", {\n code: 'invalid_parameter',\n parameter: 'events',\n allowed: ['trades', 'theses'],\n });\n }\n if (options.mint && !options.blockchain) {\n throw new BadRequestError('mint needs blockchain', { code: 'missing_parameter', parameter: 'blockchain' });\n }\n if (options.blockchain && !(CHAINS as readonly string[]).includes(options.blockchain)) {\n throw new BadRequestError(`Unknown blockchain '${options.blockchain}'`, {\n code: 'invalid_parameter',\n parameter: 'blockchain',\n allowed: [...CHAINS],\n });\n }\n this.opts = {\n reconnect: options.reconnect ?? true,\n reconnectDelay: options.reconnectDelay ?? 1_000,\n maxReconnectDelay: options.maxReconnectDelay ?? 30_000,\n heartbeatTimeout: options.heartbeatTimeout ?? 75_000,\n };\n }\n\n /** True while the socket is open. */\n get connected(): boolean {\n return this.ws?.readyState === 1;\n }\n\n /** The stream URL including query parameters, with the key masked. */\n get safeUrl(): string {\n return this.buildUrl('***');\n }\n\n private buildUrl(key: string): string {\n const sp = new URLSearchParams({ api_key: key });\n if (this.options.events) sp.set('events', [...new Set(this.options.events)].join(','));\n if (this.options.blockchain) sp.set('blockchain', this.options.blockchain);\n if (this.options.mint) sp.set('mint', this.options.mint);\n if (this.options.scope) sp.set('scope', this.options.scope);\n if (this.options.instance) sp.set('instance', this.options.instance);\n return `${this.url}?${sp.toString()}`;\n }\n\n on<E extends StreamEvent>(event: E, handler: Handler<StreamEventMap[E]>): this {\n let set = this.handlers.get(event);\n if (!set) {\n set = new Set();\n this.handlers.set(event, set);\n }\n set.add(handler as Handler<never>);\n return this;\n }\n\n off<E extends StreamEvent>(event: E, handler: Handler<StreamEventMap[E]>): this {\n this.handlers.get(event)?.delete(handler as Handler<never>);\n return this;\n }\n\n private emit<E extends StreamEvent>(event: E, payload: StreamEventMap[E]): void {\n const set = this.handlers.get(event);\n if (!set) return;\n for (const h of [...set]) {\n try {\n (h as Handler<StreamEventMap[E]>)(payload);\n } catch (err) {\n // A throwing handler must not take down the connection.\n if (event !== 'error') {\n this.emit('error', err instanceof CabalSpyIDError ? err : new CabalSpyIDError(String((err as Error)?.message ?? err), { code: 'handler_error' }));\n }\n }\n }\n }\n\n /** Opens the socket. Resolves once the server sent `subscribed`. */\n connect(): Promise<SubscribedEvent> {\n this.closedByUser = false;\n this.fatal = null;\n const WS = (this.options.WebSocketImpl ?? (globalThis as { WebSocket?: unknown }).WebSocket) as\n | (new (url: string) => MinimalWebSocket)\n | undefined;\n if (!WS) {\n return Promise.reject(\n new ConnectionError('No WebSocket implementation found. On Node < 22, run `npm i ws` and pass { WebSocketImpl: WebSocket }.', {\n code: 'no_websocket',\n }),\n );\n }\n\n return new Promise<SubscribedEvent>((resolve, reject) => {\n let settled = false;\n const settle = (err: CabalSpyIDError | null, value?: SubscribedEvent) => {\n if (settled) return;\n settled = true;\n if (err) reject(err);\n else resolve(value!);\n };\n\n let ws: MinimalWebSocket;\n try {\n ws = new WS(this.buildUrl(this.apiKey));\n } catch (err) {\n settle(new ConnectionError(`Could not open ${this.safeUrl}: ${(err as Error)?.message ?? err}`, { code: 'ws_error' }));\n return;\n }\n this.ws = ws;\n\n ws.onopen = () => {\n this.armWatchdog();\n this.emit('open', undefined);\n };\n\n ws.onmessage = (ev) => {\n if (this.ws !== ws) return;\n this.armWatchdog();\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data)) as Record<string, unknown>;\n } catch {\n return;\n }\n this.emit('message', msg);\n switch (msg.event) {\n case 'trade':\n this.emit('trade', msg.data as Trade);\n break;\n case 'subscribed': {\n this.reconnectAttempt = 0;\n const data = (msg.data ?? {}) as SubscribedEvent;\n this.emit('subscribed', data);\n settle(null, data);\n break;\n }\n case 'thesis':\n case 'thesis.reply':\n case 'thesis.reaction':\n case 'thesis.removed':\n this.emit(msg.event, msg.data as never);\n break;\n case 'heartbeat':\n this.emit('heartbeat', { ts: Number(msg.ts) });\n break;\n case 'pong':\n this.emit('pong', {});\n break;\n case 'error': {\n const e = (msg.error ?? {}) as { code?: string; message?: string };\n const err = streamError(e.code ?? 'stream_error', e.message ?? 'Stream error');\n if (FATAL_CODES.has(err.code)) this.fatal = err;\n this.emit('error', err);\n settle(err);\n break;\n }\n default:\n break;\n }\n };\n\n ws.onerror = (ev) => {\n if (this.ws !== ws) return;\n const err = new ConnectionError(`WebSocket error on ${this.safeUrl}: ${describeWsEvent(ev)}`, { code: 'ws_error' });\n this.emit('error', err);\n settle(err);\n };\n\n ws.onclose = (ev) => {\n if (this.ws !== ws) return;\n this.clearWatchdog();\n this.ws = null;\n const e = (ev ?? {}) as { code?: number; reason?: string };\n this.emit('close', { code: e.code, reason: e.reason });\n settle(\n new ConnectionError(`Stream closed before it was ready (code ${e.code ?? 'unknown'}${e.reason ? `, ${e.reason}` : ''})`, {\n code: 'ws_closed_early',\n }),\n );\n if (!this.closedByUser && !this.fatal && this.opts.reconnect) this.scheduleReconnect();\n };\n });\n }\n\n private scheduleReconnect(): void {\n const delay = Math.min(this.opts.reconnectDelay * 2 ** this.reconnectAttempt, this.opts.maxReconnectDelay) + Math.random() * 250;\n this.reconnectAttempt++;\n this.emit('reconnecting', { attempt: this.reconnectAttempt, delay });\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUser) return;\n this.connect().catch(() => {\n /* reported through 'error'; onclose schedules the next attempt */\n });\n }, delay);\n }\n\n private armWatchdog(): void {\n if (!this.opts.heartbeatTimeout) return;\n this.clearWatchdog();\n this.watchdog = setTimeout(() => {\n // Nothing heard, not even a heartbeat: the connection is dead.\n this.emit('error', new ConnectionError('No heartbeat from the stream; reconnecting', { code: 'heartbeat_timeout' }));\n this.ws?.close(4000, 'heartbeat timeout');\n }, this.opts.heartbeatTimeout);\n }\n\n private clearWatchdog(): void {\n if (this.watchdog) clearTimeout(this.watchdog);\n this.watchdog = null;\n }\n\n /**\n * Sends a ping; the server answers with 'pong'. Not needed to keep the\n * connection alive (the server sends heartbeats), and pinging more often\n * than every 30 s suppresses those heartbeats.\n */\n ping(): void {\n if (this.ws?.readyState === 1) this.ws.send('ping');\n }\n\n /** Closes the connection and stops reconnecting. */\n close(): void {\n this.closedByUser = true;\n this.clearWatchdog();\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n const ws = this.ws;\n this.ws = null;\n ws?.close(1000, 'client closed');\n this.emit('close', { code: 1000, reason: 'client closed' });\n }\n\n /**\n * Iterates over trades. Connects if needed; ends when close() is called or\n * a fatal error occurs (which is thrown). Breaking out of the loop closes a\n * connection the iterator opened.\n */\n [Symbol.asyncIterator](): AsyncGenerator<Trade, void, void> {\n return this.iterate<Trade>(['trade'], (_e, d) => d as Trade);\n }\n\n /**\n * Iterates over thesis events (`thesis`, `thesis.reply`, `thesis.reaction`,\n * `thesis.removed`). Open the stream with `events: ['theses']`.\n *\n * for await (const { event, data } of csid.stream({ events: ['theses'] }).theses()) { … }\n */\n theses(): AsyncGenerator<ThesisStreamEvent, void, void> {\n return this.iterate<ThesisStreamEvent>(\n ['thesis', 'thesis.reply', 'thesis.reaction', 'thesis.removed'],\n (event, data) => ({ event, data }) as ThesisStreamEvent,\n );\n }\n\n private async *iterate<T>(\n names: StreamEvent[],\n wrap: (event: StreamEvent, data: unknown) => T,\n ): AsyncGenerator<T, void, void> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let failure: CabalSpyIDError | null = null;\n let done = false;\n const listeners = names.map((name) => {\n const fn = (data: unknown) => {\n queue.push(wrap(name, data));\n wake?.();\n };\n return [name, fn] as const;\n });\n const onError = () => {\n if (this.fatal) {\n failure = this.fatal;\n wake?.();\n }\n };\n const onClose = () => {\n if (this.closedByUser || !this.opts.reconnect) {\n done = true;\n wake?.();\n }\n };\n for (const [name, fn] of listeners) this.on(name, fn as never);\n this.on('error', onError).on('close', onClose);\n const ownsConnection = !this.ws;\n try {\n if (ownsConnection) await this.connect();\n while (true) {\n while (queue.length) yield queue.shift()!;\n if (failure) throw failure;\n if (done) return;\n await new Promise<void>((r) => (wake = r));\n wake = null;\n }\n } finally {\n for (const [name, fn] of listeners) this.off(name, fn as never);\n this.off('error', onError).off('close', onClose);\n // Breaking out of `for await` closes a connection the iterator opened.\n if (ownsConnection && !this.closedByUser) this.close();\n }\n }\n}\n\n/** One thesis event from `stream.theses()`. */\nexport type ThesisStreamEvent =\n | { event: 'thesis'; data: ThesisLiveItem }\n | { event: 'thesis.reply'; data: ThesisLiveItem }\n | { event: 'thesis.reaction'; data: ThesisReactionUpdate }\n | { event: 'thesis.removed'; data: ThesisRemoved };\n\n/** Pulls whatever detail the runtime attached to a websocket error event. */\nfunction describeWsEvent(ev: unknown): string {\n if (!ev || typeof ev !== 'object') return 'no detail available';\n const e = ev as { message?: string; error?: { message?: string; code?: string }; type?: string };\n return e.error?.message ?? e.message ?? (e.error?.code ? `errno ${e.error.code}` : undefined) ?? e.type ?? 'no detail available';\n}\n","// ═══════════════════════════════════════════════════════════════════════════\n// THESES STREAM — wss://api-id.cabalspy.xyz/v1/theses/stream (every plan)\n// ═══════════════════════════════════════════════════════════════════════════\n//\n// → {\"op\": \"subscribe\", \"tokens\": [{\"blockchain\": \"solana\", \"mint\": \"…\"}, …]} (up to 200)\n// → {\"op\": \"unsubscribe\", \"tokens\": [...]} → {\"op\": \"subscribe\", \"all\": true}\n// ← ready, subscribed, thesis, thesis.reply, thesis.reaction, thesis.removed, heartbeat (30 s), error\n\nimport { BadRequestError, CabalSpyIDError, ConnectionError, PermissionError, RateLimitError } from './errors';\nimport type { ThesisStreamEvent } from './stream';\nimport {\n CHAINS,\n type Chain,\n type ThesisLiveItem,\n type ThesisReactionUpdate,\n type ThesisRemoved,\n} from './types';\n\n/** Tokens per connection. */\nexport const MAX_THESES_STREAM_TOKENS = 200;\n\nexport interface ThesisTokenRef {\n blockchain: Chain;\n mint: string;\n}\n\nexport interface ThesesStreamOptions {\n /** `global` (default): theses from every platform. `instance`: only the ones written on your instance. */\n scope?: 'global' | 'instance';\n /** Instance client_id, when your account has several. */\n instance?: string;\n /** Default: true */\n reconnect?: boolean;\n /** Default: 1000 */\n reconnectDelay?: number;\n /** Default: 30_000 */\n maxReconnectDelay?: number;\n /** The server sends a heartbeat every 30 s; silence this long reopens the socket. 0 disables. Default: 75_000 */\n heartbeatTimeout?: number;\n /** WebSocket implementation for Node < 22 (the `ws` package). */\n WebSocketImpl?: unknown;\n}\n\nexport interface ThesesReadyEvent {\n scope: 'global' | 'instance';\n max_tokens: number;\n}\n\nexport interface ThesesSubscribedEvent {\n all: boolean;\n tokens: ThesisTokenRef[];\n}\n\nexport interface ThesesStreamEventMap {\n ready: ThesesReadyEvent;\n subscribed: ThesesSubscribedEvent;\n thesis: ThesisLiveItem;\n 'thesis.reply': ThesisLiveItem;\n 'thesis.reaction': ThesisReactionUpdate;\n 'thesis.removed': ThesisRemoved;\n heartbeat: { ts: number };\n pong: Record<string, never>;\n open: undefined;\n close: { code?: number; reason?: string };\n reconnecting: { attempt: number; delay: number };\n error: CabalSpyIDError;\n}\n\ntype Handler<T> = (payload: T) => void;\n\ninterface MinimalWebSocket {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readyState: number;\n onopen: ((ev: unknown) => void) | null;\n onclose: ((ev: unknown) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n}\n\nconst THESIS_EVENT_NAMES = ['thesis', 'thesis.reply', 'thesis.reaction', 'thesis.removed'] as const;\n\nfunction keyOf(t: ThesisTokenRef): string {\n const mint = t.blockchain === 'solana' ? t.mint.trim() : t.mint.trim().toLowerCase();\n return `${t.blockchain}:${mint}`;\n}\n\nfunction connectionError(code: string, message: string): CabalSpyIDError {\n if (code === 'invalid_parameter') return new BadRequestError(message, { code, status: 400 });\n if (code === 'too_many_connections') return new RateLimitError(message, { code, status: 429 });\n return new PermissionError(message, { code, status: 403 });\n}\n\n/**\n * Live theses for the tokens you choose. Subscriptions are remembered and sent\n * again after every reconnect.\n *\n * const ts = csid.thesesStream();\n * ts.on('thesis', (t) => addMarker(t.blockchain, t.mint, t));\n * await ts.connect();\n * ts.subscribe([{ blockchain: 'solana', mint: MINT_A }, { blockchain: 'base', mint: TOKEN_B }]);\n *\n * for await (const { event, data } of ts) { … }\n */\nexport class ThesesStream {\n private ws: MinimalWebSocket | null = null;\n private readonly handlers = new Map<string, Set<Handler<never>>>();\n private readonly tokens = new Map<string, ThesisTokenRef>();\n private all = false;\n private attempt = 0;\n private closedByUser = false;\n private fatal: CabalSpyIDError | null = null;\n private ready = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private watchdog: ReturnType<typeof setTimeout> | null = null;\n private readonly opts: Required<Pick<ThesesStreamOptions, 'reconnect' | 'reconnectDelay' | 'maxReconnectDelay' | 'heartbeatTimeout'>>;\n\n constructor(\n private readonly url: string,\n private readonly apiKey: string,\n private readonly options: ThesesStreamOptions = {},\n ) {\n if (options.scope && options.scope !== 'global' && options.scope !== 'instance') {\n throw new BadRequestError(`Invalid value for 'scope': ${options.scope}`, {\n code: 'invalid_parameter',\n parameter: 'scope',\n allowed: ['global', 'instance'],\n });\n }\n this.opts = {\n reconnect: options.reconnect ?? true,\n reconnectDelay: options.reconnectDelay ?? 1_000,\n maxReconnectDelay: options.maxReconnectDelay ?? 30_000,\n heartbeatTimeout: options.heartbeatTimeout ?? 75_000,\n };\n }\n\n private buildUrl(key: string): string {\n const sp = new URLSearchParams({ api_key: key });\n if (this.options.scope) sp.set('scope', this.options.scope);\n if (this.options.instance) sp.set('instance', this.options.instance);\n return `${this.url}?${sp.toString()}`;\n }\n\n /** The URL with the key masked, safe to log. */\n get safeUrl(): string {\n return this.buildUrl('***');\n }\n\n get connected(): boolean {\n return this.ws?.readyState === 1 && this.ready;\n }\n\n /** The tokens currently subscribed (kept across reconnects). */\n get subscriptions(): { all: boolean; tokens: ThesisTokenRef[] } {\n return { all: this.all, tokens: [...this.tokens.values()] };\n }\n\n on<E extends keyof ThesesStreamEventMap>(event: E, handler: Handler<ThesesStreamEventMap[E]>): this {\n let set = this.handlers.get(event);\n if (!set) this.handlers.set(event, (set = new Set()));\n set.add(handler as Handler<never>);\n return this;\n }\n\n off<E extends keyof ThesesStreamEventMap>(event: E, handler: Handler<ThesesStreamEventMap[E]>): this {\n this.handlers.get(event)?.delete(handler as Handler<never>);\n return this;\n }\n\n private emit<E extends keyof ThesesStreamEventMap>(event: E, payload: ThesesStreamEventMap[E]): void {\n for (const h of [...(this.handlers.get(event) ?? [])]) {\n try {\n (h as Handler<ThesesStreamEventMap[E]>)(payload);\n } catch (err) {\n if (event !== 'error') this.emit('error', new CabalSpyIDError(String((err as Error)?.message ?? err), { code: 'handler_error' }));\n }\n }\n }\n\n private send(frame: unknown): void {\n if (this.ws?.readyState === 1 && this.ready) this.ws.send(JSON.stringify(frame));\n }\n\n // ── subscriptions ─────────────────────────────────────────────────────────\n\n /** Adds tokens (at most 200 per connection). Sent now if connected, and after every reconnect. */\n subscribe(tokens: ThesisTokenRef[]): this {\n const fresh: ThesisTokenRef[] = [];\n for (const t of tokens) {\n if (!t || !(CHAINS as readonly string[]).includes(t.blockchain)) {\n throw new BadRequestError(`Unknown blockchain '${t?.blockchain}'`, {\n code: 'invalid_parameter',\n parameter: 'blockchain',\n allowed: [...CHAINS],\n });\n }\n if (!t.mint) throw new BadRequestError(\"Parameter 'mint' is required\", { code: 'missing_parameter', parameter: 'mint' });\n const k = keyOf(t);\n if (!this.tokens.has(k)) fresh.push({ blockchain: t.blockchain, mint: t.mint.trim() });\n }\n if (this.tokens.size + fresh.length > MAX_THESES_STREAM_TOKENS) {\n throw new BadRequestError(`At most ${MAX_THESES_STREAM_TOKENS} tokens per connection`, {\n code: 'too_many_tokens',\n parameter: 'tokens',\n });\n }\n for (const t of fresh) this.tokens.set(keyOf(t), t);\n if (fresh.length) this.send({ op: 'subscribe', tokens: fresh });\n return this;\n }\n\n /** Removes tokens. */\n unsubscribe(tokens: ThesisTokenRef[]): this {\n const gone = tokens.filter((t) => this.tokens.delete(keyOf(t)));\n if (gone.length) this.send({ op: 'unsubscribe', tokens: gone });\n return this;\n }\n\n /** Every token (all theses). */\n subscribeAll(): this {\n this.all = true;\n this.send({ op: 'subscribe', all: true });\n return this;\n }\n\n /** Drops every subscription, including subscribeAll(). */\n unsubscribeAll(): this {\n this.all = false;\n this.tokens.clear();\n this.send({ op: 'unsubscribe', all: true });\n return this;\n }\n\n private resubscribe(): void {\n if (this.all) this.send({ op: 'subscribe', all: true });\n const list = [...this.tokens.values()];\n if (list.length) this.send({ op: 'subscribe', tokens: list });\n }\n\n // ── connection ────────────────────────────────────────────────────────────\n\n /** Opens the socket; resolves on `ready` (after sending the remembered subscriptions). */\n connect(): Promise<ThesesReadyEvent> {\n this.closedByUser = false;\n this.fatal = null;\n const WS = (this.options.WebSocketImpl ?? (globalThis as { WebSocket?: unknown }).WebSocket) as\n | (new (url: string) => MinimalWebSocket)\n | undefined;\n if (!WS) {\n return Promise.reject(\n new ConnectionError('No WebSocket implementation found. On Node < 22, run `npm i ws` and pass { WebSocketImpl: WebSocket }.', {\n code: 'no_websocket',\n }),\n );\n }\n return new Promise<ThesesReadyEvent>((resolve, reject) => {\n let settled = false;\n const settle = (err: CabalSpyIDError | null, value?: ThesesReadyEvent) => {\n if (settled) return;\n settled = true;\n if (err) reject(err);\n else resolve(value!);\n };\n this.ready = false;\n const ws = new WS(this.buildUrl(this.apiKey));\n this.ws = ws;\n ws.onopen = () => {\n this.arm();\n this.emit('open', undefined);\n };\n ws.onmessage = (ev) => {\n if (this.ws !== ws) return;\n this.arm();\n let msg: { event?: string; data?: unknown; ts?: number; error?: { code?: string; message?: string } };\n try {\n msg = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data));\n } catch {\n return;\n }\n const event = msg.event;\n if (event === 'ready') {\n this.ready = true;\n this.attempt = 0;\n this.resubscribe();\n const data = msg.data as ThesesReadyEvent;\n this.emit('ready', data);\n settle(null, data);\n } else if (event === 'subscribed') {\n this.emit('subscribed', msg.data as ThesesSubscribedEvent);\n } else if (event && (THESIS_EVENT_NAMES as readonly string[]).includes(event)) {\n this.emit(event as (typeof THESIS_EVENT_NAMES)[number], msg.data as never);\n } else if (event === 'heartbeat') {\n this.emit('heartbeat', { ts: Number(msg.ts) });\n } else if (event === 'pong') {\n this.emit('pong', {});\n } else if (event === 'error') {\n const code = msg.error?.code ?? 'stream_error';\n const message = msg.error?.message ?? 'Stream error';\n if (!this.ready) {\n // Refused before ready: the server closes the socket.\n const err = connectionError(code, message);\n if (code !== 'too_many_connections') this.fatal = err;\n this.emit('error', err);\n settle(err);\n } else {\n // A bad frame (too many tokens, unknown token …): the connection stays.\n this.emit('error', new BadRequestError(message, { code, status: 400 }));\n }\n }\n };\n ws.onerror = () => {\n if (this.ws !== ws) return;\n const err = new ConnectionError(`WebSocket error on ${this.safeUrl}`, { code: 'ws_error' });\n this.emit('error', err);\n settle(err);\n };\n ws.onclose = (ev) => {\n if (this.ws !== ws) return;\n this.ws = null;\n this.ready = false;\n this.disarm();\n const e = (ev ?? {}) as { code?: number; reason?: string };\n this.emit('close', { code: e.code, reason: e.reason });\n settle(new ConnectionError(`Theses stream closed before it was ready (code ${e.code ?? 'unknown'})`, { code: 'ws_closed_early' }));\n if (!this.closedByUser && !this.fatal && this.opts.reconnect) this.scheduleReconnect();\n };\n });\n }\n\n private scheduleReconnect(): void {\n const delay = Math.min(this.opts.reconnectDelay * 2 ** this.attempt, this.opts.maxReconnectDelay) + Math.random() * 250;\n this.attempt++;\n this.emit('reconnecting', { attempt: this.attempt, delay });\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.closedByUser) this.connect().catch(() => undefined);\n }, delay);\n }\n\n private arm(): void {\n if (!this.opts.heartbeatTimeout) return;\n this.disarm();\n this.watchdog = setTimeout(() => {\n this.emit('error', new ConnectionError('No heartbeat from the theses stream; reconnecting', { code: 'heartbeat_timeout' }));\n this.ws?.close(4000, 'heartbeat timeout');\n }, this.opts.heartbeatTimeout);\n }\n\n private disarm(): void {\n if (this.watchdog) clearTimeout(this.watchdog);\n this.watchdog = null;\n }\n\n /** Sends a ping (answered with `pong`). Pinging more often than every 30 s suppresses heartbeats. */\n ping(): void {\n if (this.ws?.readyState === 1) this.ws.send(JSON.stringify({ op: 'ping' }));\n }\n\n /** Closes the socket and stops reconnecting. Subscriptions are kept for a later connect(). */\n close(): void {\n this.closedByUser = true;\n this.disarm();\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n const ws = this.ws;\n this.ws = null;\n this.ready = false;\n ws?.close(1000, 'client closed');\n this.emit('close', { code: 1000, reason: 'client closed' });\n }\n\n /**\n * Iterates over thesis events as `{ event, data }`. Connects if needed;\n * breaking out closes a connection the iterator opened.\n */\n async *[Symbol.asyncIterator](): AsyncGenerator<ThesisStreamEvent, void, void> {\n const queue: ThesisStreamEvent[] = [];\n let wake: (() => void) | null = null;\n let failure: CabalSpyIDError | null = null;\n let done = false;\n const listeners = THESIS_EVENT_NAMES.map((name) => {\n const fn = (data: unknown) => {\n queue.push({ event: name, data } as ThesisStreamEvent);\n wake?.();\n };\n return [name, fn] as const;\n });\n const onError = () => {\n if (this.fatal) {\n failure = this.fatal;\n wake?.();\n }\n };\n const onClose = () => {\n if (this.closedByUser || !this.opts.reconnect) {\n done = true;\n wake?.();\n }\n };\n for (const [name, fn] of listeners) this.on(name, fn as never);\n this.on('error', onError).on('close', onClose);\n const owns = !this.ws;\n try {\n if (owns) await this.connect();\n while (true) {\n while (queue.length) yield queue.shift()!;\n if (failure) throw failure;\n if (done) return;\n await new Promise<void>((r) => (wake = r));\n wake = null;\n }\n } finally {\n for (const [name, fn] of listeners) this.off(name, fn as never);\n this.off('error', onError).off('close', onClose);\n if (owns && !this.closedByUser) this.close();\n }\n }\n}\n","/** Internal helpers: encoding, WebCrypto access, environment. */\n\nexport const SDK_VERSION = '0.5.2';\nexport const USER_AGENT = `cabalspy-id-sdk/${SDK_VERSION}`;\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\n/** Bytes backed by a plain ArrayBuffer, as WebCrypto expects. */\nexport type Bytes = Uint8Array<ArrayBuffer>;\n\nexport function utf8(text: string): Bytes {\n return enc.encode(text);\n}\n\nexport function fromUtf8(bytes: Uint8Array): string {\n return dec.decode(bytes);\n}\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!);\n return btoa(s);\n}\n\nexport function base64ToBytes(b64: string): Bytes {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nexport function base64UrlEncode(bytes: Uint8Array): string {\n return bytesToBase64(bytes).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function base64UrlDecode(text: string): Bytes {\n const b64 = text.replace(/-/g, '+').replace(/_/g, '/');\n return base64ToBytes(b64 + '='.repeat((4 - (b64.length % 4)) % 4));\n}\n\n/**\n * WebCrypto. Global in browsers, Deno, Bun and Node 19+; Node 18 exposes it as\n * `require('node:crypto').webcrypto`.\n */\nlet cryptoPromise: Promise<Crypto> | null = null;\nexport function getCrypto(): Promise<Crypto> {\n const g = (globalThis as { crypto?: Crypto }).crypto;\n if (g?.subtle) return Promise.resolve(g);\n if (!cryptoPromise) {\n const mod = 'node:crypto';\n cryptoPromise = (import(/* webpackIgnore: true */ /* @vite-ignore */ mod) as Promise<{ webcrypto: Crypto }>).then(\n (m) => m.webcrypto,\n );\n }\n return cryptoPromise;\n}\n\nexport async function randomToken(bytes = 32): Promise<string> {\n const c = await getCrypto();\n const a = new Uint8Array(bytes);\n c.getRandomValues(a);\n return base64UrlEncode(a);\n}\n\nexport async function sha256Base64Url(text: string): Promise<string> {\n const c = await getCrypto();\n return base64UrlEncode(new Uint8Array(await c.subtle.digest('SHA-256', utf8(text))));\n}\n\n/** Constant-time comparison of two strings. */\nexport function timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\nexport function env(name: string): string | undefined {\n return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[name];\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n","/**\n * CabalSpy ID Data API client — https://api-id.cabalspy.xyz/v1\n *\n * Auth: Authorization: Bearer <API_KEY>. The same key as the CabalSpy API, with\n * the \"CabalSpy ID\" toggle enabled on the API keys page of\n * https://apidashboard.cabalspy.xyz.\n */\n\nimport {\n type ApiErrorBody,\n AuthenticationError,\n BadRequestError,\n CabalSpyIDError,\n ConnectionError,\n EMPTY_RATE_LIMIT,\n InvalidResponseError,\n type RateLimitInfo,\n RateLimitError,\n errorFromStatus,\n isRetryable,\n} from './errors';\nimport { CabalSpyIDStream, type StreamOptions } from './stream';\nimport { ThesesStream, type ThesesStreamOptions } from './thesesStream';\nimport {\n type ApiResponse,\n CHAINS,\n type Chain,\n type HealthResponse,\n type HistoryResponse,\n type HistoryTrade,\n type HoldingsResponse,\n LEADERBOARD_SORTS,\n type LeaderboardEntry,\n type LeaderboardResponse,\n type LeaderboardSort,\n type MeResponse,\n type Pagination,\n PERIODS,\n type Period,\n type PnlCalendarResponse,\n type Profile,\n type Scope,\n THESIS_EMOJIS,\n THESIS_REPORT_REASONS,\n type Thesis,\n type ThesisEligibility,\n type ThesisEmoji,\n type ThesisNotifications,\n type ThesisReactions,\n type ThesisReportReason,\n type ThesisSort,\n type ThesisStance,\n type ThesisSummary,\n type Trade,\n type TrackerResponse,\n type WalletLookupResponse,\n} from './types';\nimport { USER_AGENT, env, sleep } from './util';\n\nexport const DEFAULT_BASE_URL = 'https://api-id.cabalspy.xyz/v1';\nexport const DEFAULT_STREAM_URL = 'wss://api-id.cabalspy.xyz/v1/stream';\nexport const DEFAULT_THESES_STREAM_URL = 'wss://api-id.cabalspy.xyz/v1/theses/stream';\n\nexport interface CabalSpyIDOptions {\n /**\n * API key. Falls back to CABALSPY_ID_API_KEY, then CABALSPY_API_KEY.\n * Only `health()` works without one.\n */\n apiKey?: string;\n /**\n * Your instance's client_id (`csid_…`). Only needed when your account has\n * several CabalSpy ID instances. Falls back to CABALSPY_ID_INSTANCE.\n */\n instance?: string;\n /** REST base URL including /v1. Falls back to CABALSPY_ID_BASE_URL, then the default. */\n baseUrl?: string;\n /** Stream URL. Falls back to CABALSPY_ID_STREAM_URL, then the default. */\n streamUrl?: string;\n /** Theses stream URL. Default: wss://api-id.cabalspy.xyz/v1/theses/stream */\n thesesStreamUrl?: string;\n /** Per-request timeout in milliseconds. Default: 30_000 */\n timeout?: number;\n /** Retries on 429, 5xx and network errors. Default: 2 */\n maxRetries?: number;\n /** Extra headers added to every request. */\n headers?: Record<string, string>;\n /** Custom fetch implementation, for tests or proxies. */\n fetch?: typeof globalThis.fetch;\n}\n\n/** Response body plus the metadata carried in HTTP headers. */\nexport interface Envelope<T> extends ApiResponse<T> {\n rateLimit: RateLimitInfo;\n status: number;\n}\n\ntype QueryValue = string | number | boolean | undefined | null;\nexport type Query = Record<string, QueryValue>;\n\n// ── parameters ──────────────────────────────────────────────────────────────\n\nexport interface ScopedParams {\n /** `global` (every CabalSpy ID user) is Enterprise only. Default: instance */\n scope?: Scope;\n}\n\nexport interface UsersParams extends ScopedParams {\n /** 1-100. Default: 50 */\n limit?: number;\n /** Only users with the KOL badge. */\n kol?: boolean;\n cursor?: string;\n}\n\n/** Exactly one of username, csid_id or address. */\nexport type ProfileParams = ScopedParams &\n (\n | { username: string; csid_id?: never; address?: never }\n | { csid_id: string; username?: never; address?: never }\n | { address: string; username?: never; csid_id?: never }\n );\n\nexport interface WalletParams extends ScopedParams {\n blockchain: Chain;\n address: string;\n}\n\nexport interface TrackerParams extends WalletParams {\n /** Default: 1d */\n period?: Period;\n}\n\nexport interface HistoryParams extends WalletParams {\n /** 1-1000. Default: 500 */\n limit?: number;\n cursor?: string;\n}\n\nexport interface LeaderboardParams extends ScopedParams {\n blockchain: Chain;\n /** Default: 1d */\n period?: Period;\n /** Default: volume */\n sort?: LeaderboardSort;\n /** 1-500. Default: 100 */\n limit?: number;\n /** The `next_cursor` of the previous page (a wallet address). */\n cursor?: string;\n}\n\nexport interface LatestTransactionsParams extends ScopedParams {\n /** Omit for every chain. */\n blockchain?: Chain;\n /** 1-100. Default: 50 */\n limit?: number;\n}\n\n// -- theses ------------------------------------------------------------------\n\nexport interface ThesisTokenParams {\n blockchain: Chain;\n /** Token mint / contract address. */\n mint: string;\n}\n\n/**\n * `global` (default): theses written on every platform. `instance`: only the\n * ones written on your instance (your apps). Same for every plan.\n */\nexport interface ThesisScopeParam {\n scope?: Scope;\n}\n\nexport interface ListThesesParams extends ThesisTokenParams, ThesisScopeParam {\n /** Default: new */\n sort?: ThesisSort;\n stance?: ThesisStance;\n /** 1-50. Default: 20 */\n limit?: number;\n cursor?: string;\n /** Viewer: fills `my_reactions` and `is_mine` for this user. */\n csid_id?: string;\n}\n\nexport interface ThesisRepliesParams {\n /** The thesis id. */\n id: string;\n /** 1-100. Default: 50 */\n limit?: number;\n cursor?: string;\n csid_id?: string;\n}\n\nexport interface LatestThesesParams extends ThesisScopeParam {\n /** Omit for every chain. */\n blockchain?: Chain;\n /** `instance`: only theses of your instance's users. Default: all */\n users?: 'all' | 'instance';\n /** 1-50. Default: 20 */\n limit?: number;\n cursor?: string;\n csid_id?: string;\n}\n\n/** Exactly one of csid_id or username. */\nexport type UserThesesParams = { limit?: number; cursor?: string } & (\n | { csid_id: string; username?: never }\n | { username: string; csid_id?: never }\n);\n\n/**\n * Writes happen in the name of one of your users: `csid_id` is the `sub` of\n * their ID token. They must have signed in to your app with the scope\n * `thesis` and be within your plan's user limit.\n */\nexport interface ThesisWriter {\n csid_id: string;\n}\n\nexport interface PostThesisParams extends ThesisTokenParams, ThesisWriter {\n /** Up to 1000 characters. Only holders of the token can post. */\n body: string;\n stance?: ThesisStance;\n}\n\nexport interface ReplyToThesisParams extends ThesisWriter {\n thesis_id: string;\n body: string;\n}\n\nexport interface ReactToThesisParams extends ThesisWriter {\n thesis_id: string;\n emoji: ThesisEmoji;\n /** false removes the reaction. Default: true */\n on?: boolean;\n}\n\nexport interface ReportThesisParams extends ThesisWriter {\n thesis_id: string;\n reason: ThesisReportReason;\n note?: string;\n}\n\nexport interface DeleteThesisParams extends ThesisWriter {\n thesis_id: string;\n}\n\nfunction buildQuery(params: Query): string {\n const sp = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null || v === '') continue;\n sp.append(k, typeof v === 'boolean' ? (v ? 'true' : 'false') : String(v));\n }\n const s = sp.toString();\n return s ? `?${s}` : '';\n}\n\nfunction parseNumHeader(h: Headers, name: string): number | null {\n const raw = h.get(name);\n if (raw == null) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction backoffDelay(attempt: number, lastError?: unknown): number {\n // The server's Retry-After takes precedence over our own backoff.\n if (lastError instanceof RateLimitError && lastError.retryAfter != null) {\n return Math.min(lastError.retryAfter * 1000, 60_000);\n }\n const base = 500 * 2 ** (attempt - 1); // 500, 1000, 2000 …\n return Math.min(base, 8_000) + Math.random() * 250;\n}\n\nfunction assertOneOf(value: string | undefined, allowed: readonly string[], parameter: string): void {\n if (value === undefined) return;\n if (!allowed.includes(value)) {\n throw new BadRequestError(`Invalid value for '${parameter}': ${value}`, {\n code: 'invalid_parameter',\n parameter,\n allowed: [...allowed],\n });\n }\n}\n\nfunction assertRequired(value: unknown, parameter: string): void {\n if (value === undefined || value === null || value === '') {\n throw new BadRequestError(`Parameter '${parameter}' is required`, { code: 'missing_parameter', parameter });\n }\n}\n\nfunction assertToken(p: { blockchain?: string; mint?: string }): void {\n assertRequired(p.blockchain, 'blockchain');\n assertOneOf(p.blockchain, CHAINS, 'blockchain');\n assertRequired(p.mint, 'mint');\n}\n\nfunction assertWallet(p: { blockchain?: string; address?: string }): void {\n assertRequired(p.blockchain, 'blockchain');\n assertOneOf(p.blockchain, CHAINS, 'blockchain');\n assertRequired(p.address, 'address');\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// CLIENT\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * Data API client.\n *\n * const csid = new CabalSpyID({ apiKey: process.env.CABALSPY_API_KEY });\n * const me = await csid.me();\n * const user = await csid.profile({ csid_id: idToken.sub });\n */\nexport class CabalSpyID {\n readonly baseUrl: string;\n readonly streamUrl: string;\n readonly thesesStreamUrl: string;\n readonly timeout: number;\n readonly maxRetries: number;\n readonly instance?: string;\n\n private readonly apiKey: string;\n private readonly extraHeaders: Record<string, string>;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n /** Rate limit state from the most recent request. */\n lastRateLimit: RateLimitInfo = { ...EMPTY_RATE_LIMIT };\n\n constructor(options: CabalSpyIDOptions = {}) {\n this.apiKey = options.apiKey ?? (env('CABALSPY_ID_API_KEY') || env('CABALSPY_API_KEY') || '');\n this.instance = options.instance ?? (env('CABALSPY_ID_INSTANCE') || undefined);\n this.baseUrl = (options.baseUrl ?? (env('CABALSPY_ID_BASE_URL') || DEFAULT_BASE_URL)).replace(/\\/+$/, '');\n this.streamUrl = (options.streamUrl ?? (env('CABALSPY_ID_STREAM_URL') || DEFAULT_STREAM_URL)).replace(/\\/+$/, '');\n this.thesesStreamUrl = (options.thesesStreamUrl ?? DEFAULT_THESES_STREAM_URL).replace(/\\/+$/, '');\n this.timeout = options.timeout ?? 30_000;\n this.maxRetries = options.maxRetries ?? 2;\n this.extraHeaders = options.headers ?? {};\n const f = options.fetch ?? globalThis.fetch;\n if (typeof f !== 'function') {\n throw new CabalSpyIDError('No fetch implementation available. Use Node 18+ or pass { fetch }.', {\n code: 'no_fetch',\n });\n }\n this.fetchImpl = options.fetch ?? f.bind(globalThis);\n }\n\n // ── HTTP core ─────────────────────────────────────────────────────────────\n\n private authedQuery(query: Query, auth: boolean): Query {\n if (auth && !this.apiKey) {\n throw new AuthenticationError(\n 'Missing API key. Pass { apiKey } or set CABALSPY_ID_API_KEY (the CabalSpy API key with CabalSpy ID enabled).',\n { status: 401, code: 'missing_api_key' },\n );\n }\n return auth && this.instance && query.instance === undefined ? { ...query, instance: this.instance } : query;\n }\n\n /** GET returning the full envelope: data, pagination, meta and rate limit. */\n async getRaw<T>(path: string, query: Query = {}, opts: { auth?: boolean } = {}): Promise<Envelope<T>> {\n const auth = opts.auth ?? true;\n return this.request<T>('GET', path + buildQuery(this.authedQuery(query, auth)), auth);\n }\n\n /** GET returning only the data payload. */\n async get<T>(path: string, query: Query = {}): Promise<T> {\n return (await this.getRaw<T>(path, query)).data;\n }\n\n /**\n * POST with a JSON body, returning the full envelope. POSTs are not retried\n * after a server or network error (the write may have happened); only the\n * API's own rate limit (429 rate_limit_exceeded) is retried.\n */\n async postRaw<T>(path: string, body: Record<string, unknown>): Promise<Envelope<T>> {\n return this.request<T>('POST', path + buildQuery(this.authedQuery({}, true)), true, body);\n }\n\n /** POST returning only the data payload. */\n async post<T>(path: string, body: Record<string, unknown>): Promise<T> {\n return (await this.postRaw<T>(path, body)).data;\n }\n\n private async request<T>(method: 'GET' | 'POST', path: string, auth: boolean, body?: unknown): Promise<Envelope<T>> {\n const url = `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;\n const label = `${method} ${stripQuery(path)}`;\n let lastError: CabalSpyIDError | undefined;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n if (attempt > 0) await sleep(backoffDelay(attempt, lastError));\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n let res: Response;\n let text: string;\n try {\n res = await this.fetchImpl(url, {\n method,\n signal: controller.signal,\n headers: {\n ...(auth ? { Authorization: `Bearer ${this.apiKey}` } : {}),\n Accept: 'application/json',\n 'User-Agent': USER_AGENT,\n ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...this.extraHeaders,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n text = await res.text();\n } catch (err) {\n const aborted = (err as Error)?.name === 'AbortError';\n lastError = new ConnectionError(\n aborted ? `Request timed out after ${this.timeout} ms: ${label}` : `Network error: ${(err as Error)?.message ?? String(err)}`,\n { code: aborted ? 'timeout' : 'connection_error' },\n );\n if (method !== 'GET') throw lastError; // the write may have happened\n continue; // network errors are retryable\n } finally {\n clearTimeout(timer);\n }\n\n const rateLimit: RateLimitInfo = {\n limit: parseNumHeader(res.headers, 'X-RateLimit-Limit'),\n remaining: parseNumHeader(res.headers, 'X-RateLimit-Remaining'),\n reset: parseNumHeader(res.headers, 'X-RateLimit-Reset'),\n };\n this.lastRateLimit = rateLimit;\n\n let parsed: unknown = null;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n }\n\n if (!res.ok) {\n const errBody = (parsed as { error?: ApiErrorBody } | null)?.error ?? null;\n const retryAfter = parseNumHeader(res.headers, 'Retry-After');\n const error = errorFromStatus(res.status, errBody, rateLimit, retryAfter, `HTTP ${res.status} on ${label}`);\n const retryable =\n method === 'GET' ? isRetryable(error) : error instanceof RateLimitError && error.code === 'rate_limit_exceeded';\n if (retryable && attempt < this.maxRetries) {\n lastError = error;\n continue;\n }\n throw error;\n }\n\n if (parsed == null || typeof parsed !== 'object' || !('data' in parsed)) {\n throw new InvalidResponseError(`Unexpected response shape on ${label}: no data field`, {\n status: res.status,\n code: 'invalid_response',\n rateLimit,\n });\n }\n return { ...(parsed as ApiResponse<T>), rateLimit, status: res.status };\n }\n throw lastError ?? new ConnectionError(`Request failed: ${label}`);\n }\n\n /**\n * Walks a cursor-paginated route and yields whole pages.\n *\n * for await (const page of csid.pages((cursor) => csid.usersRaw({ cursor }))) …\n *\n * Reads `pagination` from the envelope or, for the leaderboard, from `data`.\n */\n async *pages<T>(\n fetchPage: (cursor: string | undefined) => Promise<Envelope<T>>,\n opts: { cursor?: string; maxPages?: number } = {},\n ): AsyncGenerator<Envelope<T>, void, void> {\n let cursor = opts.cursor;\n const maxPages = opts.maxPages ?? Infinity;\n for (let i = 0; i < maxPages; i++) {\n const page = await fetchPage(cursor);\n const inner = (page.data as { pagination?: Pagination } | null)?.pagination;\n const pagination = page.pagination ?? inner;\n yield { ...page, pagination };\n const next = pagination?.next_cursor;\n if (!pagination?.has_more || !next) return;\n cursor = next;\n }\n }\n\n // ── account ───────────────────────────────────────────────────────────────\n\n /** GET /v1/health — service health and trade sync freshness. No API key required. */\n async health(): Promise<HealthResponse> {\n return (await this.getRaw<HealthResponse>('/health', {}, { auth: false })).data;\n }\n\n /** GET /v1/me — the key's instance, plan, request rate and number of users with data. */\n async me(): Promise<MeResponse> {\n return this.get<MeResponse>('/me');\n }\n\n // ── users ─────────────────────────────────────────────────────────────────\n\n /** GET /v1/users — users of your instance (within the plan's user limit), newest first. */\n async users(params: UsersParams = {}): Promise<Profile[]> {\n return (await this.usersRaw(params)).data;\n }\n\n async usersRaw(params: UsersParams = {}): Promise<Envelope<Profile[]>> {\n return this.getRaw<Profile[]>('/users', {\n limit: params.limit,\n kol: params.kol ? 'true' : undefined,\n cursor: params.cursor,\n scope: params.scope,\n });\n }\n\n /** Iterates over every user, following `next_cursor`. */\n async *iterUsers(params: Omit<UsersParams, 'cursor'> & { cursor?: string } = {}): AsyncGenerator<Profile, void, void> {\n for await (const page of this.pages((cursor) => this.usersRaw({ ...params, cursor }), { cursor: params.cursor })) {\n yield* page.data;\n }\n }\n\n /**\n * GET /v1/profile — one user by X username, by `csid_id` (the `sub` of their\n * ID token) or by a verified wallet address. Pass exactly one.\n *\n * An anonymous `anon_…` sub throws PlanUserLimitError.\n */\n async profile(params: ProfileParams): Promise<Profile> {\n const p = params as { username?: string; csid_id?: string; address?: string; scope?: Scope };\n const given = [p.username, p.csid_id, p.address].filter((v) => v !== undefined && v !== '').length;\n if (given !== 1) {\n throw new BadRequestError('Pass exactly one of: username, csid_id, address.', {\n code: 'missing_parameter',\n parameter: 'username|csid_id|address',\n });\n }\n return this.get<Profile>('/profile', {\n username: p.username?.replace(/^@/, ''),\n csid_id: p.csid_id,\n address: p.address,\n scope: p.scope,\n });\n }\n\n /** GET /v1/wallets/lookup — which of your users verified this wallet (by an on-chain payment). */\n async walletLookup(address: string, params: ScopedParams = {}): Promise<WalletLookupResponse> {\n assertRequired(address, 'address');\n return this.get<WalletLookupResponse>('/wallets/lookup', { address, scope: params.scope });\n }\n\n // ── trading ───────────────────────────────────────────────────────────────\n\n /**\n * GET /v1/wallets/tracker — stats, realized PnL, win-rate distribution,\n * tokens, PnL chart (cumulative from 0) and trades of a verified wallet for a\n * period. Same numbers as the CabalSpy API.\n */\n async tracker(params: TrackerParams): Promise<TrackerResponse> {\n assertWallet(params);\n assertOneOf(params.period, PERIODS, 'period');\n return this.get<TrackerResponse>('/wallets/tracker', { ...params });\n }\n\n /** GET /v1/wallets/history — lifetime stats, token overview and one page of trades. */\n async history(params: HistoryParams): Promise<HistoryResponse> {\n return (await this.historyRaw(params)).data;\n }\n\n async historyRaw(params: HistoryParams): Promise<Envelope<HistoryResponse>> {\n assertWallet(params);\n return this.getRaw<HistoryResponse>('/wallets/history', { ...params });\n }\n\n /** Iterates over every trade of a wallet, newest first, following `next_cursor`. */\n async *iterHistory(params: HistoryParams): AsyncGenerator<HistoryTrade, void, void> {\n for await (const page of this.pages((cursor) => this.historyRaw({ ...params, cursor }), { cursor: params.cursor })) {\n yield* page.data.trades;\n }\n }\n\n /** GET /v1/wallets/holdings — current token balances with USD value (dust under $0.01 hidden). */\n async holdings(params: WalletParams): Promise<HoldingsResponse> {\n assertWallet(params);\n return this.get<HoldingsResponse>('/wallets/holdings', { ...params });\n }\n\n /** GET /v1/wallets/pnl_calendar — realized PnL per UTC day, grouped by month. */\n async pnlCalendar(params: WalletParams): Promise<PnlCalendarResponse> {\n assertWallet(params);\n return this.get<PnlCalendarResponse>('/wallets/pnl_calendar', { ...params });\n }\n\n /** GET /v1/wallets/leaderboard — your users ranked by volume, PnL, win rate or trades. */\n async leaderboard(params: LeaderboardParams): Promise<LeaderboardResponse> {\n return (await this.leaderboardRaw(params)).data;\n }\n\n async leaderboardRaw(params: LeaderboardParams): Promise<Envelope<LeaderboardResponse>> {\n assertRequired(params.blockchain, 'blockchain');\n assertOneOf(params.blockchain, CHAINS, 'blockchain');\n assertOneOf(params.period, PERIODS, 'period');\n assertOneOf(params.sort, LEADERBOARD_SORTS, 'sort');\n return this.getRaw<LeaderboardResponse>('/wallets/leaderboard', { ...params });\n }\n\n /** Iterates over every leaderboard entry, following `next_cursor`. */\n async *iterLeaderboard(params: LeaderboardParams): AsyncGenerator<LeaderboardEntry, void, void> {\n for await (const page of this.pages((cursor) => this.leaderboardRaw({ ...params, cursor }), { cursor: params.cursor })) {\n yield* page.data.leaderboard;\n }\n }\n\n /** GET /v1/transactions/latest — the newest trades of your users, each with the trader. */\n async latestTransactions(params: LatestTransactionsParams = {}): Promise<Trade[]> {\n assertOneOf(params.blockchain, CHAINS, 'blockchain');\n return this.get<Trade[]>('/transactions/latest', { ...params });\n }\n\n // -- theses ----------------------------------------------------------------\n // Reading: theses are public; any key reads them, also outside its instance.\n\n /** GET /v1/theses — theses on a token, newest or top first. */\n async listTheses(params: ListThesesParams): Promise<Thesis[]> {\n return (await this.listThesesRaw(params)).data;\n }\n\n async listThesesRaw(params: ListThesesParams): Promise<Envelope<Thesis[]>> {\n assertToken(params);\n assertOneOf(params.sort, ['new', 'top'], 'sort');\n assertOneOf(params.stance, ['bull', 'bear'], 'stance');\n assertOneOf(params.scope, ['global', 'instance'], 'scope');\n return this.getRaw<Thesis[]>('/theses', { ...params });\n }\n\n /** Iterates over every thesis on a token, following `next_cursor`. */\n async *iterTheses(params: ListThesesParams): AsyncGenerator<Thesis, void, void> {\n for await (const page of this.pages((cursor) => this.listThesesRaw({ ...params, cursor }), { cursor: params.cursor })) {\n yield* page.data;\n }\n }\n\n /** GET /v1/theses/summary — counts (theses, bull, bear, authors) and token info. */\n async thesisSummary(params: ThesisTokenParams & ThesisScopeParam): Promise<ThesisSummary> {\n assertToken(params);\n assertOneOf(params.scope, ['global', 'instance'], 'scope');\n return this.get<ThesisSummary>('/theses/summary', { blockchain: params.blockchain, mint: params.mint, scope: params.scope });\n }\n\n /** GET /v1/theses/item — one thesis or reply. */\n async thesis(id: string, params: { csid_id?: string } = {}): Promise<Thesis> {\n assertRequired(id, 'id');\n return this.get<Thesis>('/theses/item', { id, csid_id: params.csid_id });\n }\n\n /** GET /v1/theses/replies — replies to a thesis. */\n async thesisReplies(params: ThesisRepliesParams): Promise<Thesis[]> {\n return (await this.thesisRepliesRaw(params)).data;\n }\n\n async thesisRepliesRaw(params: ThesisRepliesParams): Promise<Envelope<Thesis[]>> {\n assertRequired(params.id, 'id');\n return this.getRaw<Thesis[]>('/theses/replies', { ...params });\n }\n\n /** Iterates over every reply of a thesis. */\n async *iterThesisReplies(params: ThesisRepliesParams): AsyncGenerator<Thesis, void, void> {\n for await (const page of this.pages((cursor) => this.thesisRepliesRaw({ ...params, cursor }), { cursor: params.cursor })) {\n yield* page.data;\n }\n }\n\n /** GET /v1/theses/latest — newest theses on every token (or only your users'). */\n async latestTheses(params: LatestThesesParams = {}): Promise<Thesis[]> {\n return (await this.latestThesesRaw(params)).data;\n }\n\n async latestThesesRaw(params: LatestThesesParams = {}): Promise<Envelope<Thesis[]>> {\n assertOneOf(params.blockchain, CHAINS, 'blockchain');\n assertOneOf(params.users, ['all', 'instance'], 'users');\n assertOneOf(params.scope, ['global', 'instance'], 'scope');\n return this.getRaw<Thesis[]>('/theses/latest', { ...params });\n }\n\n /** GET /v1/theses/user — theses of one user, by csid_id or X username. */\n async userTheses(params: UserThesesParams): Promise<Thesis[]> {\n return (await this.userThesesRaw(params)).data;\n }\n\n async userThesesRaw(params: UserThesesParams): Promise<Envelope<Thesis[]>> {\n const p = params as { csid_id?: string; username?: string; limit?: number; cursor?: string };\n if (!!p.csid_id === !!p.username) {\n throw new BadRequestError('Pass exactly one of: csid_id, username.', {\n code: 'missing_parameter',\n parameter: 'csid_id|username',\n });\n }\n return this.getRaw<Thesis[]>('/theses/user', { ...p, username: p.username?.replace(/^@/, '') });\n }\n\n /**\n * GET /v1/theses/eligibility — may this user post (holder, not paused) and\n * reply on this token through your app? `reason` says why not\n * (consent_required, plan_user_limit, sandbox_user).\n */\n async thesisEligibility(params: ThesisTokenParams & ThesisWriter): Promise<ThesisEligibility> {\n assertToken(params);\n assertRequired(params.csid_id, 'csid_id');\n return this.get<ThesisEligibility>('/theses/eligibility', { ...params });\n }\n\n // Writing, in the name of one of your users (scope `thesis`).\n\n /** POST /v1/theses — posts a thesis. Holders only (`not_a_holder` otherwise). */\n async postThesis(params: PostThesisParams): Promise<Thesis> {\n assertToken(params);\n assertRequired(params.body, 'body');\n assertRequired(params.csid_id, 'csid_id');\n assertOneOf(params.stance, ['bull', 'bear'], 'stance');\n return this.post<Thesis>('/theses', { ...params });\n }\n\n /** POST /v1/theses/replies — replies to a thesis. */\n async replyToThesis(params: ReplyToThesisParams): Promise<Thesis> {\n assertRequired(params.thesis_id, 'thesis_id');\n assertRequired(params.body, 'body');\n assertRequired(params.csid_id, 'csid_id');\n return this.post<Thesis>('/theses/replies', { ...params });\n }\n\n /** POST /v1/theses/reactions — adds (or with `on: false` removes) a reaction. */\n async reactToThesis(params: ReactToThesisParams): Promise<ThesisReactions> {\n assertRequired(params.thesis_id, 'thesis_id');\n assertOneOf(params.emoji, THESIS_EMOJIS, 'emoji');\n assertRequired(params.csid_id, 'csid_id');\n return this.post<ThesisReactions>('/theses/reactions', { ...params, on: params.on ?? true });\n }\n\n /** POST /v1/theses/report — reports a post (hidden for review after 3 reports). */\n async reportThesis(params: ReportThesisParams): Promise<{ reported: true }> {\n assertRequired(params.thesis_id, 'thesis_id');\n assertOneOf(params.reason, THESIS_REPORT_REASONS, 'reason');\n assertRequired(params.csid_id, 'csid_id');\n return this.post<{ reported: true }>('/theses/report', { ...params });\n }\n\n /**\n * GET /v1/theses/notifications — moderation notifications of one of your users\n * (warnings, hidden posts, pauses, appeal results), with the unread count.\n */\n async thesisNotifications(csidId: string): Promise<ThesisNotifications> {\n assertRequired(csidId, 'csid_id');\n return this.get<ThesisNotifications>('/theses/notifications', { csid_id: csidId });\n }\n\n /**\n * POST /v1/theses/appeal — appeals a moderation decision in the user's name\n * (an `appealable` notification, within 30 days; message 10-1000 characters).\n */\n async appealThesisDecision(csidId: string, notificationId: number, message: string): Promise<{ appealed: true }> {\n assertRequired(csidId, 'csid_id');\n assertRequired(notificationId, 'notification_id');\n assertRequired(message, 'message');\n return this.post<{ appealed: true }>('/theses/appeal', { csid_id: csidId, notification_id: notificationId, message });\n }\n\n /** POST /v1/theses/delete — deletes the user's own thesis or reply. */\n async deleteThesis(params: DeleteThesisParams): Promise<{ deleted: true }> {\n assertRequired(params.thesis_id, 'thesis_id');\n assertRequired(params.csid_id, 'csid_id');\n return this.post<{ deleted: true }>('/theses/delete', { ...params });\n }\n\n // ── stream ────────────────────────────────────────────────────────────────\n\n /**\n * Live trades of your users over WebSocket (Pro plan and up).\n *\n * const stream = csid.stream({ blockchain: 'solana' });\n * stream.on('trade', (trade) => console.log(trade));\n * await stream.connect();\n */\n stream(options: StreamOptions = {}): CabalSpyIDStream {\n if (!this.apiKey) {\n throw new AuthenticationError('The stream needs an API key.', { status: 401, code: 'missing_api_key' });\n }\n return new CabalSpyIDStream(this.streamUrl, this.apiKey, { instance: this.instance, ...options });\n }\n\n /**\n * Live theses for the tokens you subscribe to (every plan), e.g. for a chart.\n *\n * const ts = csid.thesesStream();\n * ts.on('thesis', (t) => addMarker(t));\n * await ts.connect();\n * ts.subscribe([{ blockchain: 'solana', mint }]);\n */\n thesesStream(options: ThesesStreamOptions = {}): ThesesStream {\n if (!this.apiKey) {\n throw new AuthenticationError('The theses stream needs an API key.', { status: 401, code: 'missing_api_key' });\n }\n return new ThesesStream(this.thesesStreamUrl, this.apiKey, { instance: this.instance, ...options });\n }\n}\n\nfunction stripQuery(path: string): string {\n const i = path.indexOf('?');\n return i === -1 ? path : path.slice(0, i);\n}\n","// ═══════════════════════════════════════════════════════════════════════════\n// WEBHOOKS — Standard Webhooks signatures (standardwebhooks.com)\n// ═══════════════════════════════════════════════════════════════════════════\n//\n// webhook-id event id, the same on every retry (dedupe on it)\n// webhook-timestamp unix seconds\n// webhook-signature \"v1,<base64 HMAC-SHA256>\" over \"<id>.<timestamp>.<body>\",\n// keyed with the base64-decoded part of the whsec_ secret.\n// During a secret roll: several, separated by spaces.\n\nimport { WebhookVerificationError } from './errors';\nimport type { WebhookEvent } from './types';\nimport { type Bytes, base64ToBytes, bytesToBase64, fromUtf8, getCrypto, timingSafeEqual, utf8 } from './util';\n\nexport type HeaderSource =\n | Headers\n | Record<string, string | string[] | undefined>\n | { get(name: string): string | null | undefined };\n\nexport interface VerifyWebhookOptions {\n /** The raw request body, exactly as received (do not re-serialize parsed JSON). */\n payload: string | Uint8Array | ArrayBuffer;\n /** Request headers (Fetch Headers, Node's IncomingHttpHeaders or a plain object). */\n headers: HeaderSource;\n /** Signing secret from the dashboard, `whsec_…`. */\n secret: string;\n /** Maximum age (and future skew) of webhook-timestamp in seconds. Default: 300 */\n toleranceSeconds?: number;\n /** Current time in seconds; for tests. */\n now?: number;\n}\n\nfunction header(headers: HeaderSource, name: string): string | undefined {\n if (typeof (headers as { get?: unknown }).get === 'function') {\n return (headers as { get(n: string): string | null | undefined }).get(name) ?? undefined;\n }\n const rec = headers as Record<string, string | string[] | undefined>;\n const key = Object.keys(rec).find((k) => k.toLowerCase() === name);\n const v = key === undefined ? undefined : rec[key];\n return Array.isArray(v) ? v[0] : v;\n}\n\nfunction toBytes(payload: string | Uint8Array | ArrayBuffer): Bytes {\n if (typeof payload === 'string') return utf8(payload);\n return new Uint8Array(payload instanceof Uint8Array ? payload : new Uint8Array(payload));\n}\n\nfunction secretKey(secret: string): Bytes {\n try {\n return base64ToBytes(secret.startsWith('whsec_') ? secret.slice(6) : secret);\n } catch {\n throw new WebhookVerificationError('The webhook secret is not valid base64 (expected whsec_…)', { code: 'invalid_secret' });\n }\n}\n\nasync function hmac(secret: string, content: Bytes): Promise<string> {\n const c = await getCrypto();\n const key = await c.subtle.importKey('raw', secretKey(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n return bytesToBase64(new Uint8Array(await c.subtle.sign('HMAC', key, content)));\n}\n\nfunction signedContent(id: string, timestamp: number | string, body: Uint8Array): Bytes {\n const prefix = utf8(`${id}.${timestamp}.`);\n const out = new Uint8Array(prefix.length + body.length);\n out.set(prefix, 0);\n out.set(body, prefix.length);\n return out;\n}\n\n/**\n * Computes the `webhook-signature` header value for a payload. Useful to\n * build test requests for your receiver.\n */\nexport async function signWebhook(\n secret: string,\n id: string,\n timestamp: number,\n payload: string | Uint8Array,\n): Promise<string> {\n return `v1,${await hmac(secret, signedContent(id, timestamp, toBytes(payload)))}`;\n}\n\n/**\n * Verifies a webhook and returns the parsed event. Throws\n * WebhookVerificationError when a header is missing, the timestamp is outside\n * the tolerance or no signature matches.\n *\n * const event = await verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.CSID_WEBHOOK_SECRET });\n */\nexport async function verifyWebhook<D = Record<string, unknown>>(options: VerifyWebhookOptions): Promise<WebhookEvent<D>> {\n const { headers, secret } = options;\n if (!secret) throw new WebhookVerificationError('Missing webhook secret', { code: 'invalid_secret' });\n const id = header(headers, 'webhook-id');\n const ts = header(headers, 'webhook-timestamp');\n const sigs = header(headers, 'webhook-signature');\n if (!id || !ts || !sigs) {\n throw new WebhookVerificationError('Missing webhook-id, webhook-timestamp or webhook-signature header', {\n code: 'missing_headers',\n });\n }\n const timestamp = Number(ts);\n if (!/^\\d+$/.test(ts) || !Number.isSafeInteger(timestamp)) {\n throw new WebhookVerificationError('Invalid webhook-timestamp', { code: 'invalid_timestamp' });\n }\n const now = options.now ?? Math.floor(Date.now() / 1000);\n const tolerance = options.toleranceSeconds ?? 300;\n if (now - timestamp > tolerance) throw new WebhookVerificationError('Webhook timestamp is too old', { code: 'timestamp_too_old' });\n if (timestamp - now > tolerance) throw new WebhookVerificationError('Webhook timestamp is in the future', { code: 'timestamp_too_new' });\n\n const body = toBytes(options.payload);\n const expected = await hmac(secret, signedContent(id, ts, body));\n const match = sigs\n .split(' ')\n .map((s) => s.trim())\n .filter(Boolean)\n .some((s) => {\n const [version, sig] = s.split(',', 2);\n return version === 'v1' && sig !== undefined && timingSafeEqual(sig, expected);\n });\n if (!match) throw new WebhookVerificationError('No matching webhook signature', { code: 'invalid_signature' });\n\n try {\n return JSON.parse(fromUtf8(body)) as WebhookEvent<D>;\n } catch {\n throw new WebhookVerificationError('Webhook body is not valid JSON', { code: 'invalid_payload' });\n }\n}\n","/**\n * People can sign in with X, Telegram or Google. Accounts without X have no\n * X profile: `x` is null, `twitter` is null and the handle is `tg.<name>`\n * (Telegram) or `id.<hex>` (Google). X handles never contain a dot.\n */\n\n/** Anything that describes a CabalSpy ID user: claims, a Data API profile, a trader or thesis author. */\nexport interface UserLike {\n x?: unknown;\n twitter?: string | null;\n preferred_username?: string | null;\n username?: string | null;\n}\n\n/** The sign-in identity a handle belongs to. */\nexport type HandleKind = 'x' | 'telegram' | 'google';\n\n/** Which identity a CabalSpy ID handle comes from: `tg.` = Telegram, `id.` = Google, else X. */\nexport function handleKind(handle: string): HandleKind {\n if (handle.startsWith('tg.')) return 'telegram';\n if (handle.startsWith('id.')) return 'google';\n return 'x';\n}\n\n/**\n * True when the user has an X account connected.\n *\n * Uses the most reliable field available: `x` (userinfo claims, Data API\n * profiles), then `twitter` (trading profiles, leaderboards), then the handle\n * (ID token claims, thesis authors). Returns false for null/undefined. The ID\n * token carries no `x` claim, so there the handle decides.\n */\nexport function hasX(user: UserLike | null | undefined): boolean {\n if (!user) return false;\n if (user.x !== undefined) return user.x !== null;\n if (user.twitter !== undefined) return Boolean(user.twitter);\n const handle = user.preferred_username ?? user.username;\n return typeof handle === 'string' && handle.length > 0 && handleKind(handle) === 'x';\n}\n"]}
@@ -8,24 +8,26 @@
8
8
 
9
9
 
10
10
 
11
+ var _chunkULEBNY2Acjs = require('./chunk-ULEBNY2A.cjs');
11
12
 
12
13
 
13
14
 
14
15
 
15
16
 
16
- var _chunkL4RHSWOAcjs = require('./chunk-L4RHSWOA.cjs');
17
+
18
+ var _chunkABFIPCBYcjs = require('./chunk-ABFIPCBY.cjs');
17
19
 
18
20
  // src/jwt.ts
19
21
  function decodeJwt(token) {
20
22
  const parts = token.split(".");
21
- if (parts.length !== 3) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Malformed JWT", { code: "malformed_token" });
23
+ if (parts.length !== 3) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Malformed JWT", { code: "malformed_token" });
22
24
  try {
23
25
  return {
24
- header: JSON.parse(_chunkL4RHSWOAcjs.fromUtf8.call(void 0, _chunkL4RHSWOAcjs.base64UrlDecode.call(void 0, parts[0]))),
25
- payload: JSON.parse(_chunkL4RHSWOAcjs.fromUtf8.call(void 0, _chunkL4RHSWOAcjs.base64UrlDecode.call(void 0, parts[1])))
26
+ header: JSON.parse(_chunkULEBNY2Acjs.fromUtf8.call(void 0, _chunkULEBNY2Acjs.base64UrlDecode.call(void 0, parts[0]))),
27
+ payload: JSON.parse(_chunkULEBNY2Acjs.fromUtf8.call(void 0, _chunkULEBNY2Acjs.base64UrlDecode.call(void 0, parts[1])))
26
28
  };
27
29
  } catch (e) {
28
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Malformed JWT", { code: "malformed_token" });
30
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Malformed JWT", { code: "malformed_token" });
29
31
  }
30
32
  }
31
33
  var JwksCache = (_class = class {
@@ -42,7 +44,7 @@ var JwksCache = (_class = class {
42
44
  if (!this.inflight) {
43
45
  this.inflight = (async () => {
44
46
  const jwks = await this.load();
45
- const c = await _chunkL4RHSWOAcjs.getCrypto.call(void 0, );
47
+ const c = await _chunkULEBNY2Acjs.getCrypto.call(void 0, );
46
48
  const next = /* @__PURE__ */ new Map();
47
49
  for (const jwk of _nullishCoalesce(jwks.keys, () => ( []))) {
48
50
  if (jwk.kty !== "EC" || jwk.crv !== "P-256" || jwk.alg && jwk.alg !== "ES256") continue;
@@ -71,7 +73,7 @@ var JwksCache = (_class = class {
71
73
  await this.refresh();
72
74
  key = this.keys.get(id);
73
75
  }
74
- if (!key) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`No signing key with kid '${id}' in the JWKS`, { code: "unknown_kid" });
76
+ if (!key) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`No signing key with kid '${id}' in the JWKS`, { code: "unknown_kid" });
75
77
  return key;
76
78
  }
77
79
  }, _class);
@@ -79,36 +81,36 @@ async function verifyJwt(token, jwks, opts) {
79
81
  const decoded = decodeJwt(token);
80
82
  const { header, payload } = decoded;
81
83
  if (header.alg !== "ES256") {
82
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`Unexpected alg '${header.alg}', expected ES256`, { code: "invalid_alg" });
84
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`Unexpected alg '${header.alg}', expected ES256`, { code: "invalid_alg" });
83
85
  }
84
86
  if (opts.typ && header.typ !== opts.typ) {
85
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`Unexpected token type '${header.typ}', expected '${opts.typ}'`, { code: "invalid_typ" });
87
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`Unexpected token type '${header.typ}', expected '${opts.typ}'`, { code: "invalid_typ" });
86
88
  }
87
89
  if (opts.rejectTyp && header.typ === opts.rejectTyp) {
88
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`A '${header.typ}' token is not accepted here`, { code: "invalid_typ" });
90
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`A '${header.typ}' token is not accepted here`, { code: "invalid_typ" });
89
91
  }
90
92
  const key = await jwks.getKey(header.kid);
91
93
  const [h, p, s] = token.split(".");
92
- const signature = _chunkL4RHSWOAcjs.base64UrlDecode.call(void 0, s);
93
- if (signature.length !== 64) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Invalid ES256 signature length", { code: "invalid_signature" });
94
- const c = await _chunkL4RHSWOAcjs.getCrypto.call(void 0, );
95
- const ok = await c.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, signature, _chunkL4RHSWOAcjs.utf8.call(void 0, `${h}.${p}`));
96
- if (!ok) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Invalid token signature", { code: "invalid_signature" });
94
+ const signature = _chunkULEBNY2Acjs.base64UrlDecode.call(void 0, s);
95
+ if (signature.length !== 64) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Invalid ES256 signature length", { code: "invalid_signature" });
96
+ const c = await _chunkULEBNY2Acjs.getCrypto.call(void 0, );
97
+ const ok = await c.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, signature, _chunkULEBNY2Acjs.utf8.call(void 0, `${h}.${p}`));
98
+ if (!ok) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Invalid token signature", { code: "invalid_signature" });
97
99
  const now = _nullishCoalesce(opts.now, () => ( Math.floor(Date.now() / 1e3)));
98
100
  const tol = _nullishCoalesce(opts.clockTolerance, () => ( 60));
99
101
  if (payload.iss !== opts.issuer) {
100
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`Unexpected issuer '${String(payload.iss)}'`, { code: "invalid_issuer" });
102
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`Unexpected issuer '${String(payload.iss)}'`, { code: "invalid_issuer" });
101
103
  }
102
104
  const aud = payload.aud;
103
105
  const audOk = Array.isArray(aud) ? aud.includes(opts.audience) : aud === opts.audience;
104
- if (!audOk) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Token was issued for another client (aud)", { code: "invalid_audience" });
105
- if (typeof payload.exp !== "number") throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Token has no exp", { code: "missing_exp" });
106
- if (payload.exp + tol < now) throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Token has expired", { code: "token_expired" });
106
+ if (!audOk) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Token was issued for another client (aud)", { code: "invalid_audience" });
107
+ if (typeof payload.exp !== "number") throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Token has no exp", { code: "missing_exp" });
108
+ if (payload.exp + tol < now) throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Token has expired", { code: "token_expired" });
107
109
  if (typeof payload.nbf === "number" && payload.nbf - tol > now) {
108
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Token is not valid yet (nbf)", { code: "token_not_yet_valid" });
110
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Token is not valid yet (nbf)", { code: "token_not_yet_valid" });
109
111
  }
110
112
  if (typeof payload.iat === "number" && payload.iat - tol > now) {
111
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Token was issued in the future (iat)", { code: "invalid_iat" });
113
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Token was issued in the future (iat)", { code: "invalid_iat" });
112
114
  }
113
115
  return decoded;
114
116
  }
@@ -116,8 +118,8 @@ async function verifyJwt(token, jwks, opts) {
116
118
  // src/oidc.ts
117
119
  var DEFAULT_ISSUER = "https://id.cabalspy.xyz";
118
120
  async function generatePkce() {
119
- const codeVerifier = await _chunkL4RHSWOAcjs.randomToken.call(void 0, 48);
120
- return { codeVerifier, codeChallenge: await _chunkL4RHSWOAcjs.sha256Base64Url.call(void 0, codeVerifier) };
121
+ const codeVerifier = await _chunkULEBNY2Acjs.randomToken.call(void 0, 48);
122
+ return { codeVerifier, codeChallenge: await _chunkULEBNY2Acjs.sha256Base64Url.call(void 0, codeVerifier) };
121
123
  }
122
124
  function isLimited(claims) {
123
125
  return claims.limited === true || claims.sub.startsWith("anon_");
@@ -135,15 +137,15 @@ var CabalSpyIDAuth = (_class2 = class {
135
137
 
136
138
  constructor(options) {;_class2.prototype.__init4.call(this);
137
139
  if (!_optionalChain([options, 'optionalAccess', _ => _.clientId])) {
138
- throw new (0, _chunkL4RHSWOAcjs.CabalSpyIDError)("clientId is required", { code: "missing_client_id" });
140
+ throw new (0, _chunkABFIPCBYcjs.CabalSpyIDError)("clientId is required", { code: "missing_client_id" });
139
141
  }
140
142
  this.clientId = options.clientId;
141
143
  this.clientSecret = options.clientSecret;
142
144
  this.redirectUri = options.redirectUri;
143
- this.issuer = (_nullishCoalesce(options.issuer, () => ( (_chunkL4RHSWOAcjs.env.call(void 0, "CABALSPY_ID_ISSUER") || DEFAULT_ISSUER)))).replace(/\/+$/, "");
145
+ this.issuer = (_nullishCoalesce(options.issuer, () => ( (_chunkULEBNY2Acjs.env.call(void 0, "CABALSPY_ID_ISSUER") || DEFAULT_ISSUER)))).replace(/\/+$/, "");
144
146
  this.authMethod = _nullishCoalesce(options.tokenEndpointAuthMethod, () => ( (options.clientSecret ? "client_secret_basic" : "none")));
145
147
  if (this.authMethod !== "none" && !this.clientSecret) {
146
- throw new (0, _chunkL4RHSWOAcjs.CabalSpyIDError)(`${this.authMethod} needs clientSecret`, { code: "missing_client_secret" });
148
+ throw new (0, _chunkABFIPCBYcjs.CabalSpyIDError)(`${this.authMethod} needs clientSecret`, { code: "missing_client_secret" });
147
149
  }
148
150
  this.clockTolerance = _nullishCoalesce(options.clockTolerance, () => ( 60));
149
151
  this.timeout = _nullishCoalesce(options.timeout, () => ( 15e3));
@@ -161,7 +163,7 @@ var CabalSpyIDAuth = (_class2 = class {
161
163
  const res = await this.fetchImpl(url, {
162
164
  ...init,
163
165
  signal: controller.signal,
164
- headers: { "User-Agent": _chunkL4RHSWOAcjs.USER_AGENT, ...init.headers }
166
+ headers: { "User-Agent": _chunkULEBNY2Acjs.USER_AGENT, ...init.headers }
165
167
  });
166
168
  const text = await res.text();
167
169
  let body = null;
@@ -175,7 +177,7 @@ var CabalSpyIDAuth = (_class2 = class {
175
177
  return { res, body };
176
178
  } catch (err) {
177
179
  const aborted = _optionalChain([err, 'optionalAccess', _2 => _2.name]) === "AbortError";
178
- throw new (0, _chunkL4RHSWOAcjs.ConnectionError)(aborted ? `Request timed out after ${this.timeout} ms: ${url}` : `Network error: ${_optionalChain([err, 'optionalAccess', _3 => _3.message])}`, {
180
+ throw new (0, _chunkABFIPCBYcjs.ConnectionError)(aborted ? `Request timed out after ${this.timeout} ms: ${url}` : `Network error: ${_optionalChain([err, 'optionalAccess', _3 => _3.message])}`, {
179
181
  code: aborted ? "timeout" : "connection_error"
180
182
  });
181
183
  } finally {
@@ -186,7 +188,7 @@ var CabalSpyIDAuth = (_class2 = class {
186
188
  const { res, body } = await this.send(url, init);
187
189
  if (!res.ok) throw oauthError(res, body);
188
190
  if (body == null || typeof body !== "object") {
189
- throw new (0, _chunkL4RHSWOAcjs.InvalidResponseError)(`Expected JSON from ${url}`, { status: res.status, code: "invalid_response" });
191
+ throw new (0, _chunkABFIPCBYcjs.InvalidResponseError)(`Expected JSON from ${url}`, { status: res.status, code: "invalid_response" });
190
192
  }
191
193
  return body;
192
194
  }
@@ -217,7 +219,7 @@ var CabalSpyIDAuth = (_class2 = class {
217
219
  headers: { Accept: "application/json" }
218
220
  }).then((d) => {
219
221
  if (d.issuer !== this.issuer) {
220
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)(`Discovery issuer '${d.issuer}' does not match '${this.issuer}'`, {
222
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)(`Discovery issuer '${d.issuer}' does not match '${this.issuer}'`, {
221
223
  code: "invalid_issuer"
222
224
  });
223
225
  }
@@ -242,12 +244,12 @@ var CabalSpyIDAuth = (_class2 = class {
242
244
  async createAuthorizationUrl(params = {}) {
243
245
  const d = await this.discover();
244
246
  const redirectUri = _nullishCoalesce(params.redirectUri, () => ( this.redirectUri));
245
- if (!redirectUri) throw new (0, _chunkL4RHSWOAcjs.CabalSpyIDError)("redirectUri is required", { code: "missing_redirect_uri" });
247
+ if (!redirectUri) throw new (0, _chunkABFIPCBYcjs.CabalSpyIDError)("redirectUri is required", { code: "missing_redirect_uri" });
246
248
  const scopes = typeof params.scope === "string" ? params.scope.split(/\s+/).filter(Boolean) : _nullishCoalesce(params.scope, () => ( ["openid", "profile"]));
247
249
  if (!scopes.includes("openid")) scopes.unshift("openid");
248
250
  const { codeVerifier, codeChallenge } = await generatePkce();
249
- const state = await _asyncNullishCoalesce(params.state, async () => ( await _chunkL4RHSWOAcjs.randomToken.call(void 0, 24)));
250
- const nonce = await _asyncNullishCoalesce(params.nonce, async () => ( await _chunkL4RHSWOAcjs.randomToken.call(void 0, 24)));
251
+ const state = await _asyncNullishCoalesce(params.state, async () => ( await _chunkULEBNY2Acjs.randomToken.call(void 0, 24)));
252
+ const nonce = await _asyncNullishCoalesce(params.nonce, async () => ( await _chunkULEBNY2Acjs.randomToken.call(void 0, 24)));
251
253
  const q = new URLSearchParams({
252
254
  response_type: "code",
253
255
  client_id: this.clientId,
@@ -272,17 +274,17 @@ var CabalSpyIDAuth = (_class2 = class {
272
274
  async handleCallback(callback, checks) {
273
275
  const params = toParams(callback);
274
276
  const error = params.get("error");
275
- if (error) throw new (0, _chunkL4RHSWOAcjs.OAuthError)(error, _nullishCoalesce(params.get("error_description"), () => ( void 0)), { status: 400 });
277
+ if (error) throw new (0, _chunkABFIPCBYcjs.OAuthError)(error, _nullishCoalesce(params.get("error_description"), () => ( void 0)), { status: 400 });
276
278
  const state = _nullishCoalesce(params.get("state"), () => ( ""));
277
- if (!checks.state || !_chunkL4RHSWOAcjs.timingSafeEqual.call(void 0, state, checks.state)) {
278
- throw new (0, _chunkL4RHSWOAcjs.OAuthError)("invalid_state", "The state parameter does not match (possible CSRF).", { status: 400 });
279
+ if (!checks.state || !_chunkULEBNY2Acjs.timingSafeEqual.call(void 0, state, checks.state)) {
280
+ throw new (0, _chunkABFIPCBYcjs.OAuthError)("invalid_state", "The state parameter does not match (possible CSRF).", { status: 400 });
279
281
  }
280
282
  const iss = params.get("iss");
281
283
  if (iss !== null && iss !== this.issuer) {
282
- throw new (0, _chunkL4RHSWOAcjs.OAuthError)("invalid_issuer", `Unexpected iss '${iss}' in the authorization response.`, { status: 400 });
284
+ throw new (0, _chunkABFIPCBYcjs.OAuthError)("invalid_issuer", `Unexpected iss '${iss}' in the authorization response.`, { status: 400 });
283
285
  }
284
286
  const code = params.get("code");
285
- if (!code) throw new (0, _chunkL4RHSWOAcjs.OAuthError)("invalid_request", "The callback has no code.", { status: 400 });
287
+ if (!code) throw new (0, _chunkABFIPCBYcjs.OAuthError)("invalid_request", "The callback has no code.", { status: 400 });
286
288
  const tokens = await this.exchangeCode({ code, codeVerifier: checks.codeVerifier, redirectUri: checks.redirectUri });
287
289
  const claims = await this.verifyIdToken(tokens.id_token, { nonce: checks.nonce });
288
290
  return { tokens, claims };
@@ -291,7 +293,7 @@ var CabalSpyIDAuth = (_class2 = class {
291
293
  async exchangeCode(params) {
292
294
  const d = await this.discover();
293
295
  const redirectUri = _nullishCoalesce(params.redirectUri, () => ( this.redirectUri));
294
- if (!redirectUri) throw new (0, _chunkL4RHSWOAcjs.CabalSpyIDError)("redirectUri is required", { code: "missing_redirect_uri" });
296
+ if (!redirectUri) throw new (0, _chunkABFIPCBYcjs.CabalSpyIDError)("redirectUri is required", { code: "missing_redirect_uri" });
295
297
  return this.postForm(d.token_endpoint, {
296
298
  grant_type: "authorization_code",
297
299
  code: params.code,
@@ -337,18 +339,18 @@ var CabalSpyIDAuth = (_class2 = class {
337
339
  clockTolerance: this.clockTolerance
338
340
  });
339
341
  if (typeof payload.sub !== "string" || !payload.sub) {
340
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("ID token has no sub", { code: "missing_sub" });
342
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("ID token has no sub", { code: "missing_sub" });
341
343
  }
342
344
  if (payload.azp !== void 0 && payload.azp !== this.clientId) {
343
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("ID token azp does not match the client", { code: "invalid_azp" });
345
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("ID token azp does not match the client", { code: "invalid_azp" });
344
346
  }
345
- if (options.nonce !== void 0 && (typeof payload.nonce !== "string" || !_chunkL4RHSWOAcjs.timingSafeEqual.call(void 0, payload.nonce, options.nonce))) {
346
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("ID token nonce does not match", { code: "invalid_nonce" });
347
+ if (options.nonce !== void 0 && (typeof payload.nonce !== "string" || !_chunkULEBNY2Acjs.timingSafeEqual.call(void 0, payload.nonce, options.nonce))) {
348
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("ID token nonce does not match", { code: "invalid_nonce" });
347
349
  }
348
350
  if (options.maxAge !== void 0) {
349
351
  const now = Math.floor(Date.now() / 1e3);
350
352
  if (typeof payload.auth_time !== "number" || payload.auth_time + options.maxAge + this.clockTolerance < now) {
351
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("The user authenticated too long ago (max_age)", { code: "auth_time_too_old" });
353
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("The user authenticated too long ago (max_age)", { code: "auth_time_too_old" });
352
354
  }
353
355
  }
354
356
  return payload;
@@ -365,7 +367,7 @@ var CabalSpyIDAuth = (_class2 = class {
365
367
  { issuer: this.issuer, audience: this.clientId, typ: "at+jwt", clockTolerance: this.clockTolerance }
366
368
  );
367
369
  if (payload.client_id !== this.clientId) {
368
- throw new (0, _chunkL4RHSWOAcjs.TokenVerificationError)("Access token was issued to another client", { code: "invalid_audience" });
370
+ throw new (0, _chunkABFIPCBYcjs.TokenVerificationError)("Access token was issued to another client", { code: "invalid_audience" });
369
371
  }
370
372
  return payload;
371
373
  }
@@ -380,8 +382,8 @@ function toParams(callback) {
380
382
  }
381
383
  function oauthError(res, body) {
382
384
  const b = _nullishCoalesce(body, () => ( {}));
383
- if (typeof b.error === "string") return new (0, _chunkL4RHSWOAcjs.OAuthError)(b.error, b.error_description, { status: res.status, body });
384
- return new (0, _chunkL4RHSWOAcjs.OAuthError)(`http_${res.status}`, `HTTP ${res.status} from ${res.url || "the identity provider"}`, {
385
+ if (typeof b.error === "string") return new (0, _chunkABFIPCBYcjs.OAuthError)(b.error, b.error_description, { status: res.status, body });
386
+ return new (0, _chunkABFIPCBYcjs.OAuthError)(`http_${res.status}`, `HTTP ${res.status} from ${res.url || "the identity provider"}`, {
385
387
  status: res.status,
386
388
  body
387
389
  });
@@ -395,4 +397,4 @@ function oauthError(res, body) {
395
397
 
396
398
 
397
399
  exports.decodeJwt = decodeJwt; exports.JwksCache = JwksCache; exports.DEFAULT_ISSUER = DEFAULT_ISSUER; exports.generatePkce = generatePkce; exports.isLimited = isLimited; exports.CabalSpyIDAuth = CabalSpyIDAuth;
398
- //# sourceMappingURL=chunk-X3OFGGSE.cjs.map
400
+ //# sourceMappingURL=chunk-VDFCMYLR.cjs.map