cabalspy-id 0.0.0-stage → 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 (58) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/LICENSE +21 -0
  3. package/README.md +504 -2
  4. package/dist/api.cjs +74 -0
  5. package/dist/api.cjs.map +1 -0
  6. package/dist/api.d.cts +193 -0
  7. package/dist/api.d.ts +193 -0
  8. package/dist/api.js +74 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/chunk-A2ZAFAWC.cjs +1 -0
  11. package/dist/chunk-A2ZAFAWC.cjs.map +1 -0
  12. package/dist/chunk-ABFIPCBY.cjs +109 -0
  13. package/dist/chunk-ABFIPCBY.cjs.map +1 -0
  14. package/dist/chunk-CJFIOVBA.js +1 -0
  15. package/dist/chunk-CJFIOVBA.js.map +1 -0
  16. package/dist/chunk-DBYCRBVS.js +400 -0
  17. package/dist/chunk-DBYCRBVS.js.map +1 -0
  18. package/dist/chunk-PKFIMBPW.cjs +444 -0
  19. package/dist/chunk-PKFIMBPW.cjs.map +1 -0
  20. package/dist/chunk-ULEBNY2A.cjs +1301 -0
  21. package/dist/chunk-ULEBNY2A.cjs.map +1 -0
  22. package/dist/chunk-VDFCMYLR.cjs +400 -0
  23. package/dist/chunk-VDFCMYLR.cjs.map +1 -0
  24. package/dist/chunk-VOTYIQAY.js +109 -0
  25. package/dist/chunk-VOTYIQAY.js.map +1 -0
  26. package/dist/chunk-XLBO2RSL.js +1301 -0
  27. package/dist/chunk-XLBO2RSL.js.map +1 -0
  28. package/dist/chunk-Z23B5TY6.js +444 -0
  29. package/dist/chunk-Z23B5TY6.js.map +1 -0
  30. package/dist/index.cjs +105 -0
  31. package/dist/index.cjs.map +1 -0
  32. package/dist/index.d.cts +21 -0
  33. package/dist/index.d.ts +21 -0
  34. package/dist/index.js +105 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/react.cjs +361 -0
  37. package/dist/react.cjs.map +1 -0
  38. package/dist/react.d.cts +315 -0
  39. package/dist/react.d.ts +315 -0
  40. package/dist/react.js +333 -0
  41. package/dist/react.js.map +1 -0
  42. package/dist/thesis.cjs +76 -0
  43. package/dist/thesis.cjs.map +1 -0
  44. package/dist/thesis.d.cts +221 -0
  45. package/dist/thesis.d.ts +221 -0
  46. package/dist/thesis.js +76 -0
  47. package/dist/thesis.js.map +1 -0
  48. package/dist/types-UZeY9n1e.d.cts +689 -0
  49. package/dist/types-UZeY9n1e.d.ts +689 -0
  50. package/dist/users-3DAw88_U.d.cts +767 -0
  51. package/dist/users-B4r33i4O.d.ts +767 -0
  52. package/dist/widget.cjs +233 -0
  53. package/dist/widget.cjs.map +1 -0
  54. package/dist/widget.d.cts +226 -0
  55. package/dist/widget.d.ts +226 -0
  56. package/dist/widget.js +233 -0
  57. package/dist/widget.js.map +1 -0
  58. package/package.json +145 -4
@@ -0,0 +1,1301 @@
1
+ import {
2
+ AuthenticationError,
3
+ BadRequestError,
4
+ CabalSpyIDError,
5
+ ConnectionError,
6
+ EMPTY_RATE_LIMIT,
7
+ InvalidResponseError,
8
+ PermissionError,
9
+ RateLimitError,
10
+ WebhookVerificationError,
11
+ errorFromStatus,
12
+ isRetryable
13
+ } from "./chunk-VOTYIQAY.js";
14
+
15
+ // src/types.ts
16
+ var CHAINS = ["solana", "bnb", "base", "eth", "rh"];
17
+ var PERIODS = ["6h", "1d", "7d", "30d"];
18
+ var LEADERBOARD_SORTS = ["volume", "pnl", "pnl_usd", "win_rate", "trades"];
19
+ var CURRENCY_BY_CHAIN = {
20
+ solana: "SOL",
21
+ bnb: "BNB",
22
+ base: "ETH",
23
+ eth: "ETH",
24
+ rh: "ETH"
25
+ };
26
+ var THESIS_EMOJIS = ["like", "\u{1F525}", "\u{1F680}", "\u{1F48E}", "\u{1F440}", "\u{1F602}", "\u{1F91D}", "\u{1F480}", "\u{1F402}", "\u{1F43B}"];
27
+ var THESIS_REPORT_REASONS = ["spam", "scam", "abuse", "impersonation", "other"];
28
+ var THESIS_MAX_BODY = 1e3;
29
+
30
+ // src/stream.ts
31
+ var FATAL_CODES = /* @__PURE__ */ new Set([
32
+ "invalid_api_key",
33
+ "plan_required",
34
+ "invalid_parameter",
35
+ "missing_parameter",
36
+ "scope_not_allowed",
37
+ "product_not_enabled"
38
+ ]);
39
+ function streamError(code, message) {
40
+ const opts = { code };
41
+ if (code === "invalid_parameter" || code === "missing_parameter") return new BadRequestError(message, { ...opts, status: 400 });
42
+ if (code === "too_many_connections") return new RateLimitError(message, { ...opts, status: 429 });
43
+ if (FATAL_CODES.has(code)) return new PermissionError(message, { ...opts, status: 403 });
44
+ return new CabalSpyIDError(message, opts);
45
+ }
46
+ var CabalSpyIDStream = class {
47
+ constructor(url, apiKey, options = {}) {
48
+ this.url = url;
49
+ this.apiKey = apiKey;
50
+ this.options = options;
51
+ const events = options.events ?? ["trades"];
52
+ if (!events.length || events.some((e) => e !== "trades" && e !== "theses")) {
53
+ throw new BadRequestError("events must contain 'trades' and/or 'theses'", {
54
+ code: "invalid_parameter",
55
+ parameter: "events",
56
+ allowed: ["trades", "theses"]
57
+ });
58
+ }
59
+ if (options.mint && !options.blockchain) {
60
+ throw new BadRequestError("mint needs blockchain", { code: "missing_parameter", parameter: "blockchain" });
61
+ }
62
+ if (options.blockchain && !CHAINS.includes(options.blockchain)) {
63
+ throw new BadRequestError(`Unknown blockchain '${options.blockchain}'`, {
64
+ code: "invalid_parameter",
65
+ parameter: "blockchain",
66
+ allowed: [...CHAINS]
67
+ });
68
+ }
69
+ this.opts = {
70
+ reconnect: options.reconnect ?? true,
71
+ reconnectDelay: options.reconnectDelay ?? 1e3,
72
+ maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
73
+ heartbeatTimeout: options.heartbeatTimeout ?? 75e3
74
+ };
75
+ }
76
+ url;
77
+ apiKey;
78
+ options;
79
+ ws = null;
80
+ opts;
81
+ handlers = /* @__PURE__ */ new Map();
82
+ reconnectAttempt = 0;
83
+ reconnectTimer = null;
84
+ watchdog = null;
85
+ closedByUser = false;
86
+ fatal = null;
87
+ /** True while the socket is open. */
88
+ get connected() {
89
+ return this.ws?.readyState === 1;
90
+ }
91
+ /** The stream URL including query parameters, with the key masked. */
92
+ get safeUrl() {
93
+ return this.buildUrl("***");
94
+ }
95
+ buildUrl(key) {
96
+ const sp = new URLSearchParams({ api_key: key });
97
+ if (this.options.events) sp.set("events", [...new Set(this.options.events)].join(","));
98
+ if (this.options.blockchain) sp.set("blockchain", this.options.blockchain);
99
+ if (this.options.mint) sp.set("mint", this.options.mint);
100
+ if (this.options.scope) sp.set("scope", this.options.scope);
101
+ if (this.options.instance) sp.set("instance", this.options.instance);
102
+ return `${this.url}?${sp.toString()}`;
103
+ }
104
+ on(event, handler) {
105
+ let set = this.handlers.get(event);
106
+ if (!set) {
107
+ set = /* @__PURE__ */ new Set();
108
+ this.handlers.set(event, set);
109
+ }
110
+ set.add(handler);
111
+ return this;
112
+ }
113
+ off(event, handler) {
114
+ this.handlers.get(event)?.delete(handler);
115
+ return this;
116
+ }
117
+ emit(event, payload) {
118
+ const set = this.handlers.get(event);
119
+ if (!set) return;
120
+ for (const h of [...set]) {
121
+ try {
122
+ h(payload);
123
+ } catch (err) {
124
+ if (event !== "error") {
125
+ this.emit("error", err instanceof CabalSpyIDError ? err : new CabalSpyIDError(String(err?.message ?? err), { code: "handler_error" }));
126
+ }
127
+ }
128
+ }
129
+ }
130
+ /** Opens the socket. Resolves once the server sent `subscribed`. */
131
+ connect() {
132
+ this.closedByUser = false;
133
+ this.fatal = null;
134
+ const WS = this.options.WebSocketImpl ?? globalThis.WebSocket;
135
+ if (!WS) {
136
+ return Promise.reject(
137
+ new ConnectionError("No WebSocket implementation found. On Node < 22, run `npm i ws` and pass { WebSocketImpl: WebSocket }.", {
138
+ code: "no_websocket"
139
+ })
140
+ );
141
+ }
142
+ return new Promise((resolve, reject) => {
143
+ let settled = false;
144
+ const settle = (err, value) => {
145
+ if (settled) return;
146
+ settled = true;
147
+ if (err) reject(err);
148
+ else resolve(value);
149
+ };
150
+ let ws;
151
+ try {
152
+ ws = new WS(this.buildUrl(this.apiKey));
153
+ } catch (err) {
154
+ settle(new ConnectionError(`Could not open ${this.safeUrl}: ${err?.message ?? err}`, { code: "ws_error" }));
155
+ return;
156
+ }
157
+ this.ws = ws;
158
+ ws.onopen = () => {
159
+ this.armWatchdog();
160
+ this.emit("open", void 0);
161
+ };
162
+ ws.onmessage = (ev) => {
163
+ if (this.ws !== ws) return;
164
+ this.armWatchdog();
165
+ let msg;
166
+ try {
167
+ msg = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
168
+ } catch {
169
+ return;
170
+ }
171
+ this.emit("message", msg);
172
+ switch (msg.event) {
173
+ case "trade":
174
+ this.emit("trade", msg.data);
175
+ break;
176
+ case "subscribed": {
177
+ this.reconnectAttempt = 0;
178
+ const data = msg.data ?? {};
179
+ this.emit("subscribed", data);
180
+ settle(null, data);
181
+ break;
182
+ }
183
+ case "thesis":
184
+ case "thesis.reply":
185
+ case "thesis.reaction":
186
+ case "thesis.removed":
187
+ this.emit(msg.event, msg.data);
188
+ break;
189
+ case "heartbeat":
190
+ this.emit("heartbeat", { ts: Number(msg.ts) });
191
+ break;
192
+ case "pong":
193
+ this.emit("pong", {});
194
+ break;
195
+ case "error": {
196
+ const e = msg.error ?? {};
197
+ const err = streamError(e.code ?? "stream_error", e.message ?? "Stream error");
198
+ if (FATAL_CODES.has(err.code)) this.fatal = err;
199
+ this.emit("error", err);
200
+ settle(err);
201
+ break;
202
+ }
203
+ default:
204
+ break;
205
+ }
206
+ };
207
+ ws.onerror = (ev) => {
208
+ if (this.ws !== ws) return;
209
+ const err = new ConnectionError(`WebSocket error on ${this.safeUrl}: ${describeWsEvent(ev)}`, { code: "ws_error" });
210
+ this.emit("error", err);
211
+ settle(err);
212
+ };
213
+ ws.onclose = (ev) => {
214
+ if (this.ws !== ws) return;
215
+ this.clearWatchdog();
216
+ this.ws = null;
217
+ const e = ev ?? {};
218
+ this.emit("close", { code: e.code, reason: e.reason });
219
+ settle(
220
+ new ConnectionError(`Stream closed before it was ready (code ${e.code ?? "unknown"}${e.reason ? `, ${e.reason}` : ""})`, {
221
+ code: "ws_closed_early"
222
+ })
223
+ );
224
+ if (!this.closedByUser && !this.fatal && this.opts.reconnect) this.scheduleReconnect();
225
+ };
226
+ });
227
+ }
228
+ scheduleReconnect() {
229
+ const delay = Math.min(this.opts.reconnectDelay * 2 ** this.reconnectAttempt, this.opts.maxReconnectDelay) + Math.random() * 250;
230
+ this.reconnectAttempt++;
231
+ this.emit("reconnecting", { attempt: this.reconnectAttempt, delay });
232
+ this.reconnectTimer = setTimeout(() => {
233
+ this.reconnectTimer = null;
234
+ if (this.closedByUser) return;
235
+ this.connect().catch(() => {
236
+ });
237
+ }, delay);
238
+ }
239
+ armWatchdog() {
240
+ if (!this.opts.heartbeatTimeout) return;
241
+ this.clearWatchdog();
242
+ this.watchdog = setTimeout(() => {
243
+ this.emit("error", new ConnectionError("No heartbeat from the stream; reconnecting", { code: "heartbeat_timeout" }));
244
+ this.ws?.close(4e3, "heartbeat timeout");
245
+ }, this.opts.heartbeatTimeout);
246
+ }
247
+ clearWatchdog() {
248
+ if (this.watchdog) clearTimeout(this.watchdog);
249
+ this.watchdog = null;
250
+ }
251
+ /**
252
+ * Sends a ping; the server answers with 'pong'. Not needed to keep the
253
+ * connection alive (the server sends heartbeats), and pinging more often
254
+ * than every 30 s suppresses those heartbeats.
255
+ */
256
+ ping() {
257
+ if (this.ws?.readyState === 1) this.ws.send("ping");
258
+ }
259
+ /** Closes the connection and stops reconnecting. */
260
+ close() {
261
+ this.closedByUser = true;
262
+ this.clearWatchdog();
263
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
264
+ this.reconnectTimer = null;
265
+ const ws = this.ws;
266
+ this.ws = null;
267
+ ws?.close(1e3, "client closed");
268
+ this.emit("close", { code: 1e3, reason: "client closed" });
269
+ }
270
+ /**
271
+ * Iterates over trades. Connects if needed; ends when close() is called or
272
+ * a fatal error occurs (which is thrown). Breaking out of the loop closes a
273
+ * connection the iterator opened.
274
+ */
275
+ [Symbol.asyncIterator]() {
276
+ return this.iterate(["trade"], (_e, d) => d);
277
+ }
278
+ /**
279
+ * Iterates over thesis events (`thesis`, `thesis.reply`, `thesis.reaction`,
280
+ * `thesis.removed`). Open the stream with `events: ['theses']`.
281
+ *
282
+ * for await (const { event, data } of csid.stream({ events: ['theses'] }).theses()) { … }
283
+ */
284
+ theses() {
285
+ return this.iterate(
286
+ ["thesis", "thesis.reply", "thesis.reaction", "thesis.removed"],
287
+ (event, data) => ({ event, data })
288
+ );
289
+ }
290
+ async *iterate(names, wrap) {
291
+ const queue = [];
292
+ let wake = null;
293
+ let failure = null;
294
+ let done = false;
295
+ const listeners = names.map((name) => {
296
+ const fn = (data) => {
297
+ queue.push(wrap(name, data));
298
+ wake?.();
299
+ };
300
+ return [name, fn];
301
+ });
302
+ const onError = () => {
303
+ if (this.fatal) {
304
+ failure = this.fatal;
305
+ wake?.();
306
+ }
307
+ };
308
+ const onClose = () => {
309
+ if (this.closedByUser || !this.opts.reconnect) {
310
+ done = true;
311
+ wake?.();
312
+ }
313
+ };
314
+ for (const [name, fn] of listeners) this.on(name, fn);
315
+ this.on("error", onError).on("close", onClose);
316
+ const ownsConnection = !this.ws;
317
+ try {
318
+ if (ownsConnection) await this.connect();
319
+ while (true) {
320
+ while (queue.length) yield queue.shift();
321
+ if (failure) throw failure;
322
+ if (done) return;
323
+ await new Promise((r) => wake = r);
324
+ wake = null;
325
+ }
326
+ } finally {
327
+ for (const [name, fn] of listeners) this.off(name, fn);
328
+ this.off("error", onError).off("close", onClose);
329
+ if (ownsConnection && !this.closedByUser) this.close();
330
+ }
331
+ }
332
+ };
333
+ function describeWsEvent(ev) {
334
+ if (!ev || typeof ev !== "object") return "no detail available";
335
+ const e = ev;
336
+ return e.error?.message ?? e.message ?? (e.error?.code ? `errno ${e.error.code}` : void 0) ?? e.type ?? "no detail available";
337
+ }
338
+
339
+ // src/thesesStream.ts
340
+ var MAX_THESES_STREAM_TOKENS = 200;
341
+ var THESIS_EVENT_NAMES = ["thesis", "thesis.reply", "thesis.reaction", "thesis.removed"];
342
+ function keyOf(t) {
343
+ const mint = t.blockchain === "solana" ? t.mint.trim() : t.mint.trim().toLowerCase();
344
+ return `${t.blockchain}:${mint}`;
345
+ }
346
+ function connectionError(code, message) {
347
+ if (code === "invalid_parameter") return new BadRequestError(message, { code, status: 400 });
348
+ if (code === "too_many_connections") return new RateLimitError(message, { code, status: 429 });
349
+ return new PermissionError(message, { code, status: 403 });
350
+ }
351
+ var ThesesStream = class {
352
+ constructor(url, apiKey, options = {}) {
353
+ this.url = url;
354
+ this.apiKey = apiKey;
355
+ this.options = options;
356
+ if (options.scope && options.scope !== "global" && options.scope !== "instance") {
357
+ throw new BadRequestError(`Invalid value for 'scope': ${options.scope}`, {
358
+ code: "invalid_parameter",
359
+ parameter: "scope",
360
+ allowed: ["global", "instance"]
361
+ });
362
+ }
363
+ this.opts = {
364
+ reconnect: options.reconnect ?? true,
365
+ reconnectDelay: options.reconnectDelay ?? 1e3,
366
+ maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
367
+ heartbeatTimeout: options.heartbeatTimeout ?? 75e3
368
+ };
369
+ }
370
+ url;
371
+ apiKey;
372
+ options;
373
+ ws = null;
374
+ handlers = /* @__PURE__ */ new Map();
375
+ tokens = /* @__PURE__ */ new Map();
376
+ all = false;
377
+ attempt = 0;
378
+ closedByUser = false;
379
+ fatal = null;
380
+ ready = false;
381
+ reconnectTimer = null;
382
+ watchdog = null;
383
+ opts;
384
+ buildUrl(key) {
385
+ const sp = new URLSearchParams({ api_key: key });
386
+ if (this.options.scope) sp.set("scope", this.options.scope);
387
+ if (this.options.instance) sp.set("instance", this.options.instance);
388
+ return `${this.url}?${sp.toString()}`;
389
+ }
390
+ /** The URL with the key masked, safe to log. */
391
+ get safeUrl() {
392
+ return this.buildUrl("***");
393
+ }
394
+ get connected() {
395
+ return this.ws?.readyState === 1 && this.ready;
396
+ }
397
+ /** The tokens currently subscribed (kept across reconnects). */
398
+ get subscriptions() {
399
+ return { all: this.all, tokens: [...this.tokens.values()] };
400
+ }
401
+ on(event, handler) {
402
+ let set = this.handlers.get(event);
403
+ if (!set) this.handlers.set(event, set = /* @__PURE__ */ new Set());
404
+ set.add(handler);
405
+ return this;
406
+ }
407
+ off(event, handler) {
408
+ this.handlers.get(event)?.delete(handler);
409
+ return this;
410
+ }
411
+ emit(event, payload) {
412
+ for (const h of [...this.handlers.get(event) ?? []]) {
413
+ try {
414
+ h(payload);
415
+ } catch (err) {
416
+ if (event !== "error") this.emit("error", new CabalSpyIDError(String(err?.message ?? err), { code: "handler_error" }));
417
+ }
418
+ }
419
+ }
420
+ send(frame) {
421
+ if (this.ws?.readyState === 1 && this.ready) this.ws.send(JSON.stringify(frame));
422
+ }
423
+ // ── subscriptions ─────────────────────────────────────────────────────────
424
+ /** Adds tokens (at most 200 per connection). Sent now if connected, and after every reconnect. */
425
+ subscribe(tokens) {
426
+ const fresh = [];
427
+ for (const t of tokens) {
428
+ if (!t || !CHAINS.includes(t.blockchain)) {
429
+ throw new BadRequestError(`Unknown blockchain '${t?.blockchain}'`, {
430
+ code: "invalid_parameter",
431
+ parameter: "blockchain",
432
+ allowed: [...CHAINS]
433
+ });
434
+ }
435
+ if (!t.mint) throw new BadRequestError("Parameter 'mint' is required", { code: "missing_parameter", parameter: "mint" });
436
+ const k = keyOf(t);
437
+ if (!this.tokens.has(k)) fresh.push({ blockchain: t.blockchain, mint: t.mint.trim() });
438
+ }
439
+ if (this.tokens.size + fresh.length > MAX_THESES_STREAM_TOKENS) {
440
+ throw new BadRequestError(`At most ${MAX_THESES_STREAM_TOKENS} tokens per connection`, {
441
+ code: "too_many_tokens",
442
+ parameter: "tokens"
443
+ });
444
+ }
445
+ for (const t of fresh) this.tokens.set(keyOf(t), t);
446
+ if (fresh.length) this.send({ op: "subscribe", tokens: fresh });
447
+ return this;
448
+ }
449
+ /** Removes tokens. */
450
+ unsubscribe(tokens) {
451
+ const gone = tokens.filter((t) => this.tokens.delete(keyOf(t)));
452
+ if (gone.length) this.send({ op: "unsubscribe", tokens: gone });
453
+ return this;
454
+ }
455
+ /** Every token (all theses). */
456
+ subscribeAll() {
457
+ this.all = true;
458
+ this.send({ op: "subscribe", all: true });
459
+ return this;
460
+ }
461
+ /** Drops every subscription, including subscribeAll(). */
462
+ unsubscribeAll() {
463
+ this.all = false;
464
+ this.tokens.clear();
465
+ this.send({ op: "unsubscribe", all: true });
466
+ return this;
467
+ }
468
+ resubscribe() {
469
+ if (this.all) this.send({ op: "subscribe", all: true });
470
+ const list = [...this.tokens.values()];
471
+ if (list.length) this.send({ op: "subscribe", tokens: list });
472
+ }
473
+ // ── connection ────────────────────────────────────────────────────────────
474
+ /** Opens the socket; resolves on `ready` (after sending the remembered subscriptions). */
475
+ connect() {
476
+ this.closedByUser = false;
477
+ this.fatal = null;
478
+ const WS = this.options.WebSocketImpl ?? globalThis.WebSocket;
479
+ if (!WS) {
480
+ return Promise.reject(
481
+ new ConnectionError("No WebSocket implementation found. On Node < 22, run `npm i ws` and pass { WebSocketImpl: WebSocket }.", {
482
+ code: "no_websocket"
483
+ })
484
+ );
485
+ }
486
+ return new Promise((resolve, reject) => {
487
+ let settled = false;
488
+ const settle = (err, value) => {
489
+ if (settled) return;
490
+ settled = true;
491
+ if (err) reject(err);
492
+ else resolve(value);
493
+ };
494
+ this.ready = false;
495
+ const ws = new WS(this.buildUrl(this.apiKey));
496
+ this.ws = ws;
497
+ ws.onopen = () => {
498
+ this.arm();
499
+ this.emit("open", void 0);
500
+ };
501
+ ws.onmessage = (ev) => {
502
+ if (this.ws !== ws) return;
503
+ this.arm();
504
+ let msg;
505
+ try {
506
+ msg = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
507
+ } catch {
508
+ return;
509
+ }
510
+ const event = msg.event;
511
+ if (event === "ready") {
512
+ this.ready = true;
513
+ this.attempt = 0;
514
+ this.resubscribe();
515
+ const data = msg.data;
516
+ this.emit("ready", data);
517
+ settle(null, data);
518
+ } else if (event === "subscribed") {
519
+ this.emit("subscribed", msg.data);
520
+ } else if (event && THESIS_EVENT_NAMES.includes(event)) {
521
+ this.emit(event, msg.data);
522
+ } else if (event === "heartbeat") {
523
+ this.emit("heartbeat", { ts: Number(msg.ts) });
524
+ } else if (event === "pong") {
525
+ this.emit("pong", {});
526
+ } else if (event === "error") {
527
+ const code = msg.error?.code ?? "stream_error";
528
+ const message = msg.error?.message ?? "Stream error";
529
+ if (!this.ready) {
530
+ const err = connectionError(code, message);
531
+ if (code !== "too_many_connections") this.fatal = err;
532
+ this.emit("error", err);
533
+ settle(err);
534
+ } else {
535
+ this.emit("error", new BadRequestError(message, { code, status: 400 }));
536
+ }
537
+ }
538
+ };
539
+ ws.onerror = () => {
540
+ if (this.ws !== ws) return;
541
+ const err = new ConnectionError(`WebSocket error on ${this.safeUrl}`, { code: "ws_error" });
542
+ this.emit("error", err);
543
+ settle(err);
544
+ };
545
+ ws.onclose = (ev) => {
546
+ if (this.ws !== ws) return;
547
+ this.ws = null;
548
+ this.ready = false;
549
+ this.disarm();
550
+ const e = ev ?? {};
551
+ this.emit("close", { code: e.code, reason: e.reason });
552
+ settle(new ConnectionError(`Theses stream closed before it was ready (code ${e.code ?? "unknown"})`, { code: "ws_closed_early" }));
553
+ if (!this.closedByUser && !this.fatal && this.opts.reconnect) this.scheduleReconnect();
554
+ };
555
+ });
556
+ }
557
+ scheduleReconnect() {
558
+ const delay = Math.min(this.opts.reconnectDelay * 2 ** this.attempt, this.opts.maxReconnectDelay) + Math.random() * 250;
559
+ this.attempt++;
560
+ this.emit("reconnecting", { attempt: this.attempt, delay });
561
+ this.reconnectTimer = setTimeout(() => {
562
+ this.reconnectTimer = null;
563
+ if (!this.closedByUser) this.connect().catch(() => void 0);
564
+ }, delay);
565
+ }
566
+ arm() {
567
+ if (!this.opts.heartbeatTimeout) return;
568
+ this.disarm();
569
+ this.watchdog = setTimeout(() => {
570
+ this.emit("error", new ConnectionError("No heartbeat from the theses stream; reconnecting", { code: "heartbeat_timeout" }));
571
+ this.ws?.close(4e3, "heartbeat timeout");
572
+ }, this.opts.heartbeatTimeout);
573
+ }
574
+ disarm() {
575
+ if (this.watchdog) clearTimeout(this.watchdog);
576
+ this.watchdog = null;
577
+ }
578
+ /** Sends a ping (answered with `pong`). Pinging more often than every 30 s suppresses heartbeats. */
579
+ ping() {
580
+ if (this.ws?.readyState === 1) this.ws.send(JSON.stringify({ op: "ping" }));
581
+ }
582
+ /** Closes the socket and stops reconnecting. Subscriptions are kept for a later connect(). */
583
+ close() {
584
+ this.closedByUser = true;
585
+ this.disarm();
586
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
587
+ this.reconnectTimer = null;
588
+ const ws = this.ws;
589
+ this.ws = null;
590
+ this.ready = false;
591
+ ws?.close(1e3, "client closed");
592
+ this.emit("close", { code: 1e3, reason: "client closed" });
593
+ }
594
+ /**
595
+ * Iterates over thesis events as `{ event, data }`. Connects if needed;
596
+ * breaking out closes a connection the iterator opened.
597
+ */
598
+ async *[Symbol.asyncIterator]() {
599
+ const queue = [];
600
+ let wake = null;
601
+ let failure = null;
602
+ let done = false;
603
+ const listeners = THESIS_EVENT_NAMES.map((name) => {
604
+ const fn = (data) => {
605
+ queue.push({ event: name, data });
606
+ wake?.();
607
+ };
608
+ return [name, fn];
609
+ });
610
+ const onError = () => {
611
+ if (this.fatal) {
612
+ failure = this.fatal;
613
+ wake?.();
614
+ }
615
+ };
616
+ const onClose = () => {
617
+ if (this.closedByUser || !this.opts.reconnect) {
618
+ done = true;
619
+ wake?.();
620
+ }
621
+ };
622
+ for (const [name, fn] of listeners) this.on(name, fn);
623
+ this.on("error", onError).on("close", onClose);
624
+ const owns = !this.ws;
625
+ try {
626
+ if (owns) await this.connect();
627
+ while (true) {
628
+ while (queue.length) yield queue.shift();
629
+ if (failure) throw failure;
630
+ if (done) return;
631
+ await new Promise((r) => wake = r);
632
+ wake = null;
633
+ }
634
+ } finally {
635
+ for (const [name, fn] of listeners) this.off(name, fn);
636
+ this.off("error", onError).off("close", onClose);
637
+ if (owns && !this.closedByUser) this.close();
638
+ }
639
+ }
640
+ };
641
+
642
+ // src/util.ts
643
+ var SDK_VERSION = "0.5.2";
644
+ var USER_AGENT = `cabalspy-id-sdk/${SDK_VERSION}`;
645
+ var enc = new TextEncoder();
646
+ var dec = new TextDecoder();
647
+ function utf8(text) {
648
+ return enc.encode(text);
649
+ }
650
+ function fromUtf8(bytes) {
651
+ return dec.decode(bytes);
652
+ }
653
+ function bytesToBase64(bytes) {
654
+ let s = "";
655
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
656
+ return btoa(s);
657
+ }
658
+ function base64ToBytes(b64) {
659
+ const bin = atob(b64);
660
+ const out = new Uint8Array(bin.length);
661
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
662
+ return out;
663
+ }
664
+ function base64UrlEncode(bytes) {
665
+ return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
666
+ }
667
+ function base64UrlDecode(text) {
668
+ const b64 = text.replace(/-/g, "+").replace(/_/g, "/");
669
+ return base64ToBytes(b64 + "=".repeat((4 - b64.length % 4) % 4));
670
+ }
671
+ var cryptoPromise = null;
672
+ function getCrypto() {
673
+ const g = globalThis.crypto;
674
+ if (g?.subtle) return Promise.resolve(g);
675
+ if (!cryptoPromise) {
676
+ const mod = "node:crypto";
677
+ cryptoPromise = import(
678
+ /* webpackIgnore: true */
679
+ /* @vite-ignore */
680
+ mod
681
+ ).then(
682
+ (m) => m.webcrypto
683
+ );
684
+ }
685
+ return cryptoPromise;
686
+ }
687
+ async function randomToken(bytes = 32) {
688
+ const c = await getCrypto();
689
+ const a = new Uint8Array(bytes);
690
+ c.getRandomValues(a);
691
+ return base64UrlEncode(a);
692
+ }
693
+ async function sha256Base64Url(text) {
694
+ const c = await getCrypto();
695
+ return base64UrlEncode(new Uint8Array(await c.subtle.digest("SHA-256", utf8(text))));
696
+ }
697
+ function timingSafeEqual(a, b) {
698
+ if (a.length !== b.length) return false;
699
+ let diff = 0;
700
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
701
+ return diff === 0;
702
+ }
703
+ function env(name) {
704
+ return globalThis.process?.env?.[name];
705
+ }
706
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
707
+
708
+ // src/client.ts
709
+ var DEFAULT_BASE_URL = "https://api-id.cabalspy.xyz/v1";
710
+ var DEFAULT_STREAM_URL = "wss://api-id.cabalspy.xyz/v1/stream";
711
+ var DEFAULT_THESES_STREAM_URL = "wss://api-id.cabalspy.xyz/v1/theses/stream";
712
+ function buildQuery(params) {
713
+ const sp = new URLSearchParams();
714
+ for (const [k, v] of Object.entries(params)) {
715
+ if (v === void 0 || v === null || v === "") continue;
716
+ sp.append(k, typeof v === "boolean" ? v ? "true" : "false" : String(v));
717
+ }
718
+ const s = sp.toString();
719
+ return s ? `?${s}` : "";
720
+ }
721
+ function parseNumHeader(h, name) {
722
+ const raw = h.get(name);
723
+ if (raw == null) return null;
724
+ const n = Number.parseInt(raw, 10);
725
+ return Number.isFinite(n) ? n : null;
726
+ }
727
+ function backoffDelay(attempt, lastError) {
728
+ if (lastError instanceof RateLimitError && lastError.retryAfter != null) {
729
+ return Math.min(lastError.retryAfter * 1e3, 6e4);
730
+ }
731
+ const base = 500 * 2 ** (attempt - 1);
732
+ return Math.min(base, 8e3) + Math.random() * 250;
733
+ }
734
+ function assertOneOf(value, allowed, parameter) {
735
+ if (value === void 0) return;
736
+ if (!allowed.includes(value)) {
737
+ throw new BadRequestError(`Invalid value for '${parameter}': ${value}`, {
738
+ code: "invalid_parameter",
739
+ parameter,
740
+ allowed: [...allowed]
741
+ });
742
+ }
743
+ }
744
+ function assertRequired(value, parameter) {
745
+ if (value === void 0 || value === null || value === "") {
746
+ throw new BadRequestError(`Parameter '${parameter}' is required`, { code: "missing_parameter", parameter });
747
+ }
748
+ }
749
+ function assertToken(p) {
750
+ assertRequired(p.blockchain, "blockchain");
751
+ assertOneOf(p.blockchain, CHAINS, "blockchain");
752
+ assertRequired(p.mint, "mint");
753
+ }
754
+ function assertWallet(p) {
755
+ assertRequired(p.blockchain, "blockchain");
756
+ assertOneOf(p.blockchain, CHAINS, "blockchain");
757
+ assertRequired(p.address, "address");
758
+ }
759
+ var CabalSpyID = class {
760
+ baseUrl;
761
+ streamUrl;
762
+ thesesStreamUrl;
763
+ timeout;
764
+ maxRetries;
765
+ instance;
766
+ apiKey;
767
+ extraHeaders;
768
+ fetchImpl;
769
+ /** Rate limit state from the most recent request. */
770
+ lastRateLimit = { ...EMPTY_RATE_LIMIT };
771
+ constructor(options = {}) {
772
+ this.apiKey = options.apiKey ?? (env("CABALSPY_ID_API_KEY") || env("CABALSPY_API_KEY") || "");
773
+ this.instance = options.instance ?? (env("CABALSPY_ID_INSTANCE") || void 0);
774
+ this.baseUrl = (options.baseUrl ?? (env("CABALSPY_ID_BASE_URL") || DEFAULT_BASE_URL)).replace(/\/+$/, "");
775
+ this.streamUrl = (options.streamUrl ?? (env("CABALSPY_ID_STREAM_URL") || DEFAULT_STREAM_URL)).replace(/\/+$/, "");
776
+ this.thesesStreamUrl = (options.thesesStreamUrl ?? DEFAULT_THESES_STREAM_URL).replace(/\/+$/, "");
777
+ this.timeout = options.timeout ?? 3e4;
778
+ this.maxRetries = options.maxRetries ?? 2;
779
+ this.extraHeaders = options.headers ?? {};
780
+ const f = options.fetch ?? globalThis.fetch;
781
+ if (typeof f !== "function") {
782
+ throw new CabalSpyIDError("No fetch implementation available. Use Node 18+ or pass { fetch }.", {
783
+ code: "no_fetch"
784
+ });
785
+ }
786
+ this.fetchImpl = options.fetch ?? f.bind(globalThis);
787
+ }
788
+ // ── HTTP core ─────────────────────────────────────────────────────────────
789
+ authedQuery(query, auth) {
790
+ if (auth && !this.apiKey) {
791
+ throw new AuthenticationError(
792
+ "Missing API key. Pass { apiKey } or set CABALSPY_ID_API_KEY (the CabalSpy API key with CabalSpy ID enabled).",
793
+ { status: 401, code: "missing_api_key" }
794
+ );
795
+ }
796
+ return auth && this.instance && query.instance === void 0 ? { ...query, instance: this.instance } : query;
797
+ }
798
+ /** GET returning the full envelope: data, pagination, meta and rate limit. */
799
+ async getRaw(path, query = {}, opts = {}) {
800
+ const auth = opts.auth ?? true;
801
+ return this.request("GET", path + buildQuery(this.authedQuery(query, auth)), auth);
802
+ }
803
+ /** GET returning only the data payload. */
804
+ async get(path, query = {}) {
805
+ return (await this.getRaw(path, query)).data;
806
+ }
807
+ /**
808
+ * POST with a JSON body, returning the full envelope. POSTs are not retried
809
+ * after a server or network error (the write may have happened); only the
810
+ * API's own rate limit (429 rate_limit_exceeded) is retried.
811
+ */
812
+ async postRaw(path, body) {
813
+ return this.request("POST", path + buildQuery(this.authedQuery({}, true)), true, body);
814
+ }
815
+ /** POST returning only the data payload. */
816
+ async post(path, body) {
817
+ return (await this.postRaw(path, body)).data;
818
+ }
819
+ async request(method, path, auth, body) {
820
+ const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
821
+ const label = `${method} ${stripQuery(path)}`;
822
+ let lastError;
823
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
824
+ if (attempt > 0) await sleep(backoffDelay(attempt, lastError));
825
+ const controller = new AbortController();
826
+ const timer = setTimeout(() => controller.abort(), this.timeout);
827
+ let res;
828
+ let text;
829
+ try {
830
+ res = await this.fetchImpl(url, {
831
+ method,
832
+ signal: controller.signal,
833
+ headers: {
834
+ ...auth ? { Authorization: `Bearer ${this.apiKey}` } : {},
835
+ Accept: "application/json",
836
+ "User-Agent": USER_AGENT,
837
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {},
838
+ ...this.extraHeaders
839
+ },
840
+ body: body !== void 0 ? JSON.stringify(body) : void 0
841
+ });
842
+ text = await res.text();
843
+ } catch (err) {
844
+ const aborted = err?.name === "AbortError";
845
+ lastError = new ConnectionError(
846
+ aborted ? `Request timed out after ${this.timeout} ms: ${label}` : `Network error: ${err?.message ?? String(err)}`,
847
+ { code: aborted ? "timeout" : "connection_error" }
848
+ );
849
+ if (method !== "GET") throw lastError;
850
+ continue;
851
+ } finally {
852
+ clearTimeout(timer);
853
+ }
854
+ const rateLimit = {
855
+ limit: parseNumHeader(res.headers, "X-RateLimit-Limit"),
856
+ remaining: parseNumHeader(res.headers, "X-RateLimit-Remaining"),
857
+ reset: parseNumHeader(res.headers, "X-RateLimit-Reset")
858
+ };
859
+ this.lastRateLimit = rateLimit;
860
+ let parsed = null;
861
+ if (text) {
862
+ try {
863
+ parsed = JSON.parse(text);
864
+ } catch {
865
+ parsed = null;
866
+ }
867
+ }
868
+ if (!res.ok) {
869
+ const errBody = parsed?.error ?? null;
870
+ const retryAfter = parseNumHeader(res.headers, "Retry-After");
871
+ const error = errorFromStatus(res.status, errBody, rateLimit, retryAfter, `HTTP ${res.status} on ${label}`);
872
+ const retryable = method === "GET" ? isRetryable(error) : error instanceof RateLimitError && error.code === "rate_limit_exceeded";
873
+ if (retryable && attempt < this.maxRetries) {
874
+ lastError = error;
875
+ continue;
876
+ }
877
+ throw error;
878
+ }
879
+ if (parsed == null || typeof parsed !== "object" || !("data" in parsed)) {
880
+ throw new InvalidResponseError(`Unexpected response shape on ${label}: no data field`, {
881
+ status: res.status,
882
+ code: "invalid_response",
883
+ rateLimit
884
+ });
885
+ }
886
+ return { ...parsed, rateLimit, status: res.status };
887
+ }
888
+ throw lastError ?? new ConnectionError(`Request failed: ${label}`);
889
+ }
890
+ /**
891
+ * Walks a cursor-paginated route and yields whole pages.
892
+ *
893
+ * for await (const page of csid.pages((cursor) => csid.usersRaw({ cursor }))) …
894
+ *
895
+ * Reads `pagination` from the envelope or, for the leaderboard, from `data`.
896
+ */
897
+ async *pages(fetchPage, opts = {}) {
898
+ let cursor = opts.cursor;
899
+ const maxPages = opts.maxPages ?? Infinity;
900
+ for (let i = 0; i < maxPages; i++) {
901
+ const page = await fetchPage(cursor);
902
+ const inner = page.data?.pagination;
903
+ const pagination = page.pagination ?? inner;
904
+ yield { ...page, pagination };
905
+ const next = pagination?.next_cursor;
906
+ if (!pagination?.has_more || !next) return;
907
+ cursor = next;
908
+ }
909
+ }
910
+ // ── account ───────────────────────────────────────────────────────────────
911
+ /** GET /v1/health — service health and trade sync freshness. No API key required. */
912
+ async health() {
913
+ return (await this.getRaw("/health", {}, { auth: false })).data;
914
+ }
915
+ /** GET /v1/me — the key's instance, plan, request rate and number of users with data. */
916
+ async me() {
917
+ return this.get("/me");
918
+ }
919
+ // ── users ─────────────────────────────────────────────────────────────────
920
+ /** GET /v1/users — users of your instance (within the plan's user limit), newest first. */
921
+ async users(params = {}) {
922
+ return (await this.usersRaw(params)).data;
923
+ }
924
+ async usersRaw(params = {}) {
925
+ return this.getRaw("/users", {
926
+ limit: params.limit,
927
+ kol: params.kol ? "true" : void 0,
928
+ cursor: params.cursor,
929
+ scope: params.scope
930
+ });
931
+ }
932
+ /** Iterates over every user, following `next_cursor`. */
933
+ async *iterUsers(params = {}) {
934
+ for await (const page of this.pages((cursor) => this.usersRaw({ ...params, cursor }), { cursor: params.cursor })) {
935
+ yield* page.data;
936
+ }
937
+ }
938
+ /**
939
+ * GET /v1/profile — one user by X username, by `csid_id` (the `sub` of their
940
+ * ID token) or by a verified wallet address. Pass exactly one.
941
+ *
942
+ * An anonymous `anon_…` sub throws PlanUserLimitError.
943
+ */
944
+ async profile(params) {
945
+ const p = params;
946
+ const given = [p.username, p.csid_id, p.address].filter((v) => v !== void 0 && v !== "").length;
947
+ if (given !== 1) {
948
+ throw new BadRequestError("Pass exactly one of: username, csid_id, address.", {
949
+ code: "missing_parameter",
950
+ parameter: "username|csid_id|address"
951
+ });
952
+ }
953
+ return this.get("/profile", {
954
+ username: p.username?.replace(/^@/, ""),
955
+ csid_id: p.csid_id,
956
+ address: p.address,
957
+ scope: p.scope
958
+ });
959
+ }
960
+ /** GET /v1/wallets/lookup — which of your users verified this wallet (by an on-chain payment). */
961
+ async walletLookup(address, params = {}) {
962
+ assertRequired(address, "address");
963
+ return this.get("/wallets/lookup", { address, scope: params.scope });
964
+ }
965
+ // ── trading ───────────────────────────────────────────────────────────────
966
+ /**
967
+ * GET /v1/wallets/tracker — stats, realized PnL, win-rate distribution,
968
+ * tokens, PnL chart (cumulative from 0) and trades of a verified wallet for a
969
+ * period. Same numbers as the CabalSpy API.
970
+ */
971
+ async tracker(params) {
972
+ assertWallet(params);
973
+ assertOneOf(params.period, PERIODS, "period");
974
+ return this.get("/wallets/tracker", { ...params });
975
+ }
976
+ /** GET /v1/wallets/history — lifetime stats, token overview and one page of trades. */
977
+ async history(params) {
978
+ return (await this.historyRaw(params)).data;
979
+ }
980
+ async historyRaw(params) {
981
+ assertWallet(params);
982
+ return this.getRaw("/wallets/history", { ...params });
983
+ }
984
+ /** Iterates over every trade of a wallet, newest first, following `next_cursor`. */
985
+ async *iterHistory(params) {
986
+ for await (const page of this.pages((cursor) => this.historyRaw({ ...params, cursor }), { cursor: params.cursor })) {
987
+ yield* page.data.trades;
988
+ }
989
+ }
990
+ /** GET /v1/wallets/holdings — current token balances with USD value (dust under $0.01 hidden). */
991
+ async holdings(params) {
992
+ assertWallet(params);
993
+ return this.get("/wallets/holdings", { ...params });
994
+ }
995
+ /** GET /v1/wallets/pnl_calendar — realized PnL per UTC day, grouped by month. */
996
+ async pnlCalendar(params) {
997
+ assertWallet(params);
998
+ return this.get("/wallets/pnl_calendar", { ...params });
999
+ }
1000
+ /** GET /v1/wallets/leaderboard — your users ranked by volume, PnL, win rate or trades. */
1001
+ async leaderboard(params) {
1002
+ return (await this.leaderboardRaw(params)).data;
1003
+ }
1004
+ async leaderboardRaw(params) {
1005
+ assertRequired(params.blockchain, "blockchain");
1006
+ assertOneOf(params.blockchain, CHAINS, "blockchain");
1007
+ assertOneOf(params.period, PERIODS, "period");
1008
+ assertOneOf(params.sort, LEADERBOARD_SORTS, "sort");
1009
+ return this.getRaw("/wallets/leaderboard", { ...params });
1010
+ }
1011
+ /** Iterates over every leaderboard entry, following `next_cursor`. */
1012
+ async *iterLeaderboard(params) {
1013
+ for await (const page of this.pages((cursor) => this.leaderboardRaw({ ...params, cursor }), { cursor: params.cursor })) {
1014
+ yield* page.data.leaderboard;
1015
+ }
1016
+ }
1017
+ /** GET /v1/transactions/latest — the newest trades of your users, each with the trader. */
1018
+ async latestTransactions(params = {}) {
1019
+ assertOneOf(params.blockchain, CHAINS, "blockchain");
1020
+ return this.get("/transactions/latest", { ...params });
1021
+ }
1022
+ // -- theses ----------------------------------------------------------------
1023
+ // Reading: theses are public; any key reads them, also outside its instance.
1024
+ /** GET /v1/theses — theses on a token, newest or top first. */
1025
+ async listTheses(params) {
1026
+ return (await this.listThesesRaw(params)).data;
1027
+ }
1028
+ async listThesesRaw(params) {
1029
+ assertToken(params);
1030
+ assertOneOf(params.sort, ["new", "top"], "sort");
1031
+ assertOneOf(params.stance, ["bull", "bear"], "stance");
1032
+ assertOneOf(params.scope, ["global", "instance"], "scope");
1033
+ return this.getRaw("/theses", { ...params });
1034
+ }
1035
+ /** Iterates over every thesis on a token, following `next_cursor`. */
1036
+ async *iterTheses(params) {
1037
+ for await (const page of this.pages((cursor) => this.listThesesRaw({ ...params, cursor }), { cursor: params.cursor })) {
1038
+ yield* page.data;
1039
+ }
1040
+ }
1041
+ /** GET /v1/theses/summary — counts (theses, bull, bear, authors) and token info. */
1042
+ async thesisSummary(params) {
1043
+ assertToken(params);
1044
+ assertOneOf(params.scope, ["global", "instance"], "scope");
1045
+ return this.get("/theses/summary", { blockchain: params.blockchain, mint: params.mint, scope: params.scope });
1046
+ }
1047
+ /** GET /v1/theses/item — one thesis or reply. */
1048
+ async thesis(id, params = {}) {
1049
+ assertRequired(id, "id");
1050
+ return this.get("/theses/item", { id, csid_id: params.csid_id });
1051
+ }
1052
+ /** GET /v1/theses/replies — replies to a thesis. */
1053
+ async thesisReplies(params) {
1054
+ return (await this.thesisRepliesRaw(params)).data;
1055
+ }
1056
+ async thesisRepliesRaw(params) {
1057
+ assertRequired(params.id, "id");
1058
+ return this.getRaw("/theses/replies", { ...params });
1059
+ }
1060
+ /** Iterates over every reply of a thesis. */
1061
+ async *iterThesisReplies(params) {
1062
+ for await (const page of this.pages((cursor) => this.thesisRepliesRaw({ ...params, cursor }), { cursor: params.cursor })) {
1063
+ yield* page.data;
1064
+ }
1065
+ }
1066
+ /** GET /v1/theses/latest — newest theses on every token (or only your users'). */
1067
+ async latestTheses(params = {}) {
1068
+ return (await this.latestThesesRaw(params)).data;
1069
+ }
1070
+ async latestThesesRaw(params = {}) {
1071
+ assertOneOf(params.blockchain, CHAINS, "blockchain");
1072
+ assertOneOf(params.users, ["all", "instance"], "users");
1073
+ assertOneOf(params.scope, ["global", "instance"], "scope");
1074
+ return this.getRaw("/theses/latest", { ...params });
1075
+ }
1076
+ /** GET /v1/theses/user — theses of one user, by csid_id or X username. */
1077
+ async userTheses(params) {
1078
+ return (await this.userThesesRaw(params)).data;
1079
+ }
1080
+ async userThesesRaw(params) {
1081
+ const p = params;
1082
+ if (!!p.csid_id === !!p.username) {
1083
+ throw new BadRequestError("Pass exactly one of: csid_id, username.", {
1084
+ code: "missing_parameter",
1085
+ parameter: "csid_id|username"
1086
+ });
1087
+ }
1088
+ return this.getRaw("/theses/user", { ...p, username: p.username?.replace(/^@/, "") });
1089
+ }
1090
+ /**
1091
+ * GET /v1/theses/eligibility — may this user post (holder, not paused) and
1092
+ * reply on this token through your app? `reason` says why not
1093
+ * (consent_required, plan_user_limit, sandbox_user).
1094
+ */
1095
+ async thesisEligibility(params) {
1096
+ assertToken(params);
1097
+ assertRequired(params.csid_id, "csid_id");
1098
+ return this.get("/theses/eligibility", { ...params });
1099
+ }
1100
+ // Writing, in the name of one of your users (scope `thesis`).
1101
+ /** POST /v1/theses — posts a thesis. Holders only (`not_a_holder` otherwise). */
1102
+ async postThesis(params) {
1103
+ assertToken(params);
1104
+ assertRequired(params.body, "body");
1105
+ assertRequired(params.csid_id, "csid_id");
1106
+ assertOneOf(params.stance, ["bull", "bear"], "stance");
1107
+ return this.post("/theses", { ...params });
1108
+ }
1109
+ /** POST /v1/theses/replies — replies to a thesis. */
1110
+ async replyToThesis(params) {
1111
+ assertRequired(params.thesis_id, "thesis_id");
1112
+ assertRequired(params.body, "body");
1113
+ assertRequired(params.csid_id, "csid_id");
1114
+ return this.post("/theses/replies", { ...params });
1115
+ }
1116
+ /** POST /v1/theses/reactions — adds (or with `on: false` removes) a reaction. */
1117
+ async reactToThesis(params) {
1118
+ assertRequired(params.thesis_id, "thesis_id");
1119
+ assertOneOf(params.emoji, THESIS_EMOJIS, "emoji");
1120
+ assertRequired(params.csid_id, "csid_id");
1121
+ return this.post("/theses/reactions", { ...params, on: params.on ?? true });
1122
+ }
1123
+ /** POST /v1/theses/report — reports a post (hidden for review after 3 reports). */
1124
+ async reportThesis(params) {
1125
+ assertRequired(params.thesis_id, "thesis_id");
1126
+ assertOneOf(params.reason, THESIS_REPORT_REASONS, "reason");
1127
+ assertRequired(params.csid_id, "csid_id");
1128
+ return this.post("/theses/report", { ...params });
1129
+ }
1130
+ /**
1131
+ * GET /v1/theses/notifications — moderation notifications of one of your users
1132
+ * (warnings, hidden posts, pauses, appeal results), with the unread count.
1133
+ */
1134
+ async thesisNotifications(csidId) {
1135
+ assertRequired(csidId, "csid_id");
1136
+ return this.get("/theses/notifications", { csid_id: csidId });
1137
+ }
1138
+ /**
1139
+ * POST /v1/theses/appeal — appeals a moderation decision in the user's name
1140
+ * (an `appealable` notification, within 30 days; message 10-1000 characters).
1141
+ */
1142
+ async appealThesisDecision(csidId, notificationId, message) {
1143
+ assertRequired(csidId, "csid_id");
1144
+ assertRequired(notificationId, "notification_id");
1145
+ assertRequired(message, "message");
1146
+ return this.post("/theses/appeal", { csid_id: csidId, notification_id: notificationId, message });
1147
+ }
1148
+ /** POST /v1/theses/delete — deletes the user's own thesis or reply. */
1149
+ async deleteThesis(params) {
1150
+ assertRequired(params.thesis_id, "thesis_id");
1151
+ assertRequired(params.csid_id, "csid_id");
1152
+ return this.post("/theses/delete", { ...params });
1153
+ }
1154
+ // ── stream ────────────────────────────────────────────────────────────────
1155
+ /**
1156
+ * Live trades of your users over WebSocket (Pro plan and up).
1157
+ *
1158
+ * const stream = csid.stream({ blockchain: 'solana' });
1159
+ * stream.on('trade', (trade) => console.log(trade));
1160
+ * await stream.connect();
1161
+ */
1162
+ stream(options = {}) {
1163
+ if (!this.apiKey) {
1164
+ throw new AuthenticationError("The stream needs an API key.", { status: 401, code: "missing_api_key" });
1165
+ }
1166
+ return new CabalSpyIDStream(this.streamUrl, this.apiKey, { instance: this.instance, ...options });
1167
+ }
1168
+ /**
1169
+ * Live theses for the tokens you subscribe to (every plan), e.g. for a chart.
1170
+ *
1171
+ * const ts = csid.thesesStream();
1172
+ * ts.on('thesis', (t) => addMarker(t));
1173
+ * await ts.connect();
1174
+ * ts.subscribe([{ blockchain: 'solana', mint }]);
1175
+ */
1176
+ thesesStream(options = {}) {
1177
+ if (!this.apiKey) {
1178
+ throw new AuthenticationError("The theses stream needs an API key.", { status: 401, code: "missing_api_key" });
1179
+ }
1180
+ return new ThesesStream(this.thesesStreamUrl, this.apiKey, { instance: this.instance, ...options });
1181
+ }
1182
+ };
1183
+ function stripQuery(path) {
1184
+ const i = path.indexOf("?");
1185
+ return i === -1 ? path : path.slice(0, i);
1186
+ }
1187
+
1188
+ // src/webhooks.ts
1189
+ function header(headers, name) {
1190
+ if (typeof headers.get === "function") {
1191
+ return headers.get(name) ?? void 0;
1192
+ }
1193
+ const rec = headers;
1194
+ const key = Object.keys(rec).find((k) => k.toLowerCase() === name);
1195
+ const v = key === void 0 ? void 0 : rec[key];
1196
+ return Array.isArray(v) ? v[0] : v;
1197
+ }
1198
+ function toBytes(payload) {
1199
+ if (typeof payload === "string") return utf8(payload);
1200
+ return new Uint8Array(payload instanceof Uint8Array ? payload : new Uint8Array(payload));
1201
+ }
1202
+ function secretKey(secret) {
1203
+ try {
1204
+ return base64ToBytes(secret.startsWith("whsec_") ? secret.slice(6) : secret);
1205
+ } catch {
1206
+ throw new WebhookVerificationError("The webhook secret is not valid base64 (expected whsec_\u2026)", { code: "invalid_secret" });
1207
+ }
1208
+ }
1209
+ async function hmac(secret, content) {
1210
+ const c = await getCrypto();
1211
+ const key = await c.subtle.importKey("raw", secretKey(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1212
+ return bytesToBase64(new Uint8Array(await c.subtle.sign("HMAC", key, content)));
1213
+ }
1214
+ function signedContent(id, timestamp, body) {
1215
+ const prefix = utf8(`${id}.${timestamp}.`);
1216
+ const out = new Uint8Array(prefix.length + body.length);
1217
+ out.set(prefix, 0);
1218
+ out.set(body, prefix.length);
1219
+ return out;
1220
+ }
1221
+ async function signWebhook(secret, id, timestamp, payload) {
1222
+ return `v1,${await hmac(secret, signedContent(id, timestamp, toBytes(payload)))}`;
1223
+ }
1224
+ async function verifyWebhook(options) {
1225
+ const { headers, secret } = options;
1226
+ if (!secret) throw new WebhookVerificationError("Missing webhook secret", { code: "invalid_secret" });
1227
+ const id = header(headers, "webhook-id");
1228
+ const ts = header(headers, "webhook-timestamp");
1229
+ const sigs = header(headers, "webhook-signature");
1230
+ if (!id || !ts || !sigs) {
1231
+ throw new WebhookVerificationError("Missing webhook-id, webhook-timestamp or webhook-signature header", {
1232
+ code: "missing_headers"
1233
+ });
1234
+ }
1235
+ const timestamp = Number(ts);
1236
+ if (!/^\d+$/.test(ts) || !Number.isSafeInteger(timestamp)) {
1237
+ throw new WebhookVerificationError("Invalid webhook-timestamp", { code: "invalid_timestamp" });
1238
+ }
1239
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1240
+ const tolerance = options.toleranceSeconds ?? 300;
1241
+ if (now - timestamp > tolerance) throw new WebhookVerificationError("Webhook timestamp is too old", { code: "timestamp_too_old" });
1242
+ if (timestamp - now > tolerance) throw new WebhookVerificationError("Webhook timestamp is in the future", { code: "timestamp_too_new" });
1243
+ const body = toBytes(options.payload);
1244
+ const expected = await hmac(secret, signedContent(id, ts, body));
1245
+ const match = sigs.split(" ").map((s) => s.trim()).filter(Boolean).some((s) => {
1246
+ const [version, sig] = s.split(",", 2);
1247
+ return version === "v1" && sig !== void 0 && timingSafeEqual(sig, expected);
1248
+ });
1249
+ if (!match) throw new WebhookVerificationError("No matching webhook signature", { code: "invalid_signature" });
1250
+ try {
1251
+ return JSON.parse(fromUtf8(body));
1252
+ } catch {
1253
+ throw new WebhookVerificationError("Webhook body is not valid JSON", { code: "invalid_payload" });
1254
+ }
1255
+ }
1256
+
1257
+ // src/users.ts
1258
+ function handleKind(handle) {
1259
+ if (handle.startsWith("tg.")) return "telegram";
1260
+ if (handle.startsWith("id.")) return "google";
1261
+ return "x";
1262
+ }
1263
+ function hasX(user) {
1264
+ if (!user) return false;
1265
+ if (user.x !== void 0) return user.x !== null;
1266
+ if (user.twitter !== void 0) return Boolean(user.twitter);
1267
+ const handle = user.preferred_username ?? user.username;
1268
+ return typeof handle === "string" && handle.length > 0 && handleKind(handle) === "x";
1269
+ }
1270
+
1271
+ export {
1272
+ CHAINS,
1273
+ PERIODS,
1274
+ LEADERBOARD_SORTS,
1275
+ CURRENCY_BY_CHAIN,
1276
+ THESIS_EMOJIS,
1277
+ THESIS_REPORT_REASONS,
1278
+ THESIS_MAX_BODY,
1279
+ CabalSpyIDStream,
1280
+ MAX_THESES_STREAM_TOKENS,
1281
+ ThesesStream,
1282
+ SDK_VERSION,
1283
+ USER_AGENT,
1284
+ utf8,
1285
+ fromUtf8,
1286
+ base64UrlDecode,
1287
+ getCrypto,
1288
+ randomToken,
1289
+ sha256Base64Url,
1290
+ timingSafeEqual,
1291
+ env,
1292
+ DEFAULT_BASE_URL,
1293
+ DEFAULT_STREAM_URL,
1294
+ DEFAULT_THESES_STREAM_URL,
1295
+ CabalSpyID,
1296
+ signWebhook,
1297
+ verifyWebhook,
1298
+ handleKind,
1299
+ hasX
1300
+ };
1301
+ //# sourceMappingURL=chunk-XLBO2RSL.js.map