@clovnet/casino-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,403 @@
1
+ // src/realtime/transport.ts
2
+ function resolveWebSocket(injected) {
3
+ if (injected) return injected;
4
+ const g = globalThis;
5
+ if (g.WebSocket) return g.WebSocket;
6
+ throw new Error(
7
+ "No WebSocket implementation available. In Node, pass `WebSocketImpl` (e.g. `import WebSocket from 'ws'`) in the realtime options."
8
+ );
9
+ }
10
+ var WS_OPEN = 1;
11
+
12
+ // src/realtime/client.ts
13
+ var DEFAULTS = { baseMs: 500, maxMs: 15e3, factor: 2, dedupeWindow: 512, heartbeatMs: 25e3 };
14
+ var MAX_PENDING_CONNECT_ATTEMPTS = 5;
15
+ var PONG_DEADLINE_INTERVALS = 2;
16
+ var READY_DEADLINE_MS = 1e4;
17
+ function createRealtimeClient(options) {
18
+ return new RealtimeClientImpl(options);
19
+ }
20
+ var RealtimeClientImpl = class {
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.seen = new SeenSet(options.dedupeWindow ?? DEFAULTS.dedupeWindow);
24
+ }
25
+ options;
26
+ state = "idle";
27
+ socket = null;
28
+ handlers = /* @__PURE__ */ new Map();
29
+ active = /* @__PURE__ */ new Set();
30
+ /**
31
+ * Channels the app declared via `subscribe()` (as opposed to interest implied by
32
+ * an `on()` handler). A channel is torn down only when BOTH interests are gone:
33
+ * `unsubscribe()` must not kill a channel that still has a handler, and removing
34
+ * the last handler must not kill a channel the app explicitly subscribed.
35
+ */
36
+ explicit = /* @__PURE__ */ new Set();
37
+ seen;
38
+ stateListeners = /* @__PURE__ */ new Set();
39
+ errorListeners = /* @__PURE__ */ new Set();
40
+ attempt = 0;
41
+ lastPongAt = 0;
42
+ heartbeatTimer = null;
43
+ reconnectTimer = null;
44
+ readyDeadlineTimer = null;
45
+ intentionalClose = false;
46
+ /**
47
+ * Teardown epoch. `disconnect()` bumps it; an `open()` that suspended (awaiting
48
+ * the auth credential) before the bump must abandon its cycle when it resumes —
49
+ * otherwise it resurrects an authenticated "zombie" socket after logout.
50
+ */
51
+ epoch = 0;
52
+ lastOccurredAt;
53
+ idCounter = 0;
54
+ connectResolve = null;
55
+ connectReject = null;
56
+ connectPromise = null;
57
+ connect() {
58
+ if (this.state === "ready") return Promise.resolve();
59
+ if (this.connectPromise) return this.connectPromise;
60
+ this.intentionalClose = false;
61
+ const pending = new Promise((resolve, reject) => {
62
+ this.connectResolve = resolve;
63
+ this.connectReject = reject;
64
+ });
65
+ const tracked = pending.finally(() => {
66
+ if (this.connectPromise === tracked) this.connectPromise = null;
67
+ });
68
+ this.connectPromise = tracked;
69
+ if (this.state !== "reconnecting") void this.open();
70
+ return tracked;
71
+ }
72
+ async disconnect() {
73
+ this.intentionalClose = true;
74
+ this.epoch++;
75
+ this.clearTimers();
76
+ this.failConnect(new Error("realtime: disconnect() before the connection became ready"));
77
+ if (this.socket) {
78
+ try {
79
+ this.socket.close(1e3, "client disconnect");
80
+ } catch {
81
+ }
82
+ this.socket = null;
83
+ }
84
+ this.setState("closed");
85
+ }
86
+ on(channel, handler) {
87
+ let set = this.handlers.get(channel);
88
+ if (!set) {
89
+ set = /* @__PURE__ */ new Set();
90
+ this.handlers.set(channel, set);
91
+ }
92
+ set.add(handler);
93
+ if (!this.active.has(channel)) this.markActive([channel]);
94
+ return () => {
95
+ const handlers = this.handlers.get(channel);
96
+ handlers?.delete(handler);
97
+ if (handlers && handlers.size === 0) {
98
+ this.handlers.delete(channel);
99
+ if (!this.explicit.has(channel)) this.teardown([channel]);
100
+ }
101
+ };
102
+ }
103
+ async subscribe(channels) {
104
+ for (const c of channels) this.explicit.add(c);
105
+ this.markActive(channels);
106
+ }
107
+ async unsubscribe(channels) {
108
+ for (const c of channels) this.explicit.delete(c);
109
+ this.teardown(channels.filter((c) => !this.handlers.get(c)?.size));
110
+ }
111
+ async withSubscription(channels, scope) {
112
+ await this.subscribe(channels);
113
+ try {
114
+ return await scope();
115
+ } finally {
116
+ await this.unsubscribe(channels);
117
+ }
118
+ }
119
+ activeChannels() {
120
+ return [...this.active];
121
+ }
122
+ /** Register interest and subscribe on the wire when connected. */
123
+ markActive(channels) {
124
+ for (const c of channels) this.active.add(c);
125
+ if (this.state === "ready") {
126
+ this.send({ type: "subscribe", channels: [...channels], id: this.nextId() });
127
+ }
128
+ }
129
+ /** Drop interest and unsubscribe on the wire when connected. */
130
+ teardown(channels) {
131
+ if (channels.length === 0) return;
132
+ for (const c of channels) this.active.delete(c);
133
+ if (this.state === "ready") {
134
+ this.send({ type: "unsubscribe", channels: [...channels], id: this.nextId() });
135
+ }
136
+ }
137
+ onStateChange(handler) {
138
+ this.stateListeners.add(handler);
139
+ return () => this.stateListeners.delete(handler);
140
+ }
141
+ onError(handler) {
142
+ this.errorListeners.add(handler);
143
+ return () => this.errorListeners.delete(handler);
144
+ }
145
+ // ── internals ────────────────────────────────────────────────────────────────
146
+ async open() {
147
+ const epoch = this.epoch;
148
+ this.setState(this.attempt === 0 ? "connecting" : "reconnecting");
149
+ let ticket;
150
+ try {
151
+ const cred = await this.options.getAuthCredential?.();
152
+ if (cred && "ticket" in cred) ticket = cred.ticket;
153
+ } catch (err) {
154
+ if (epoch !== this.epoch) return;
155
+ this.emitError({ code: "REALTIME_AUTH", message: err.message });
156
+ this.failConnect(err instanceof Error ? err : new Error(String(err)));
157
+ this.scheduleReconnect();
158
+ return;
159
+ }
160
+ if (epoch !== this.epoch) return;
161
+ let socket;
162
+ try {
163
+ const WebSocketImpl = resolveWebSocket(this.options.WebSocketImpl);
164
+ socket = new WebSocketImpl(this.options.url);
165
+ } catch (err) {
166
+ this.failConnect(err);
167
+ this.scheduleReconnect();
168
+ return;
169
+ }
170
+ this.socket = socket;
171
+ this.armReadyDeadline(socket);
172
+ socket.onopen = () => {
173
+ if (socket !== this.socket) return;
174
+ if (ticket) this.send({ type: "auth", token: ticket });
175
+ };
176
+ socket.onmessage = (ev) => {
177
+ if (socket !== this.socket) return;
178
+ this.onMessage(ev.data);
179
+ };
180
+ socket.onerror = () => {
181
+ if (socket !== this.socket) return;
182
+ this.emitError({ code: "REALTIME_SOCKET", message: "socket error" });
183
+ };
184
+ socket.onclose = (ev) => this.onClose(socket, ev?.code);
185
+ }
186
+ onMessage(raw) {
187
+ if (typeof raw !== "string") return;
188
+ let frame;
189
+ try {
190
+ frame = JSON.parse(raw);
191
+ } catch {
192
+ return;
193
+ }
194
+ switch (frame.type) {
195
+ case "ready":
196
+ this.onReady(frame);
197
+ break;
198
+ case "event":
199
+ this.onEvent(frame);
200
+ break;
201
+ case "error":
202
+ this.emitError({
203
+ code: frame.code,
204
+ message: frame.message,
205
+ ...isRecord(frame.details) ? { details: frame.details } : {}
206
+ });
207
+ break;
208
+ case "reconnect":
209
+ try {
210
+ this.socket?.close(1e3, frame.reason);
211
+ } catch {
212
+ }
213
+ break;
214
+ case "pong":
215
+ this.lastPongAt = Date.now();
216
+ break;
217
+ }
218
+ }
219
+ onReady(frame) {
220
+ this.attempt = 0;
221
+ this.clearReadyDeadline();
222
+ this.setState("ready");
223
+ this.startHeartbeat(frame.heartbeatMs || this.options.heartbeatMs || DEFAULTS.heartbeatMs);
224
+ if (this.active.size > 0) {
225
+ this.send({ type: "subscribe", channels: [...this.active], id: this.nextId() });
226
+ }
227
+ const ctx = this.lastOccurredAt !== void 0 ? { since: this.lastOccurredAt } : {};
228
+ void Promise.resolve(this.options.resync?.(ctx)).catch(() => {
229
+ });
230
+ this.connectResolve?.();
231
+ this.connectResolve = null;
232
+ this.connectReject = null;
233
+ }
234
+ onEvent(frame) {
235
+ if (this.seen.has(frame.eventId)) return;
236
+ this.seen.add(frame.eventId);
237
+ this.lastOccurredAt = frame.occurredAt;
238
+ const handlers = this.handlers.get(frame.channel);
239
+ if (!handlers) return;
240
+ const event = frame;
241
+ for (const handler of handlers) {
242
+ try {
243
+ handler(event);
244
+ } catch (err) {
245
+ this.emitError({ code: "REALTIME_HANDLER", message: err.message });
246
+ }
247
+ }
248
+ }
249
+ onClose(socket, code) {
250
+ if (socket !== this.socket) return;
251
+ this.clearTimers();
252
+ this.socket = null;
253
+ if (this.intentionalClose) {
254
+ this.setState("closed");
255
+ return;
256
+ }
257
+ this.scheduleReconnect(code);
258
+ }
259
+ scheduleReconnect(_code) {
260
+ if (this.intentionalClose) return;
261
+ this.setState("reconnecting");
262
+ if (this.connectReject && this.attempt + 1 >= MAX_PENDING_CONNECT_ATTEMPTS) {
263
+ this.failConnect(
264
+ new Error(
265
+ `realtime: connection not ready after ${MAX_PENDING_CONNECT_ATTEMPTS} attempts (still retrying in the background)`
266
+ )
267
+ );
268
+ }
269
+ const delay = this.backoffDelay(this.attempt++);
270
+ this.reconnectTimer = setTimeout(() => void this.open(), delay);
271
+ }
272
+ backoffDelay(attempt) {
273
+ const { baseMs, maxMs, factor } = {
274
+ baseMs: this.options.backoff?.baseMs ?? DEFAULTS.baseMs,
275
+ maxMs: this.options.backoff?.maxMs ?? DEFAULTS.maxMs,
276
+ factor: this.options.backoff?.factor ?? DEFAULTS.factor
277
+ };
278
+ const ceiling = Math.min(maxMs, baseMs * factor ** attempt);
279
+ return Math.floor(Math.random() * ceiling);
280
+ }
281
+ startHeartbeat(intervalMs) {
282
+ this.clearHeartbeat();
283
+ this.lastPongAt = Date.now();
284
+ this.heartbeatTimer = setInterval(() => {
285
+ const socket = this.socket;
286
+ if (!socket || socket.readyState !== WS_OPEN) return;
287
+ if (Date.now() - this.lastPongAt >= intervalMs * PONG_DEADLINE_INTERVALS) {
288
+ try {
289
+ socket.close(4e3, "pong deadline");
290
+ } catch {
291
+ }
292
+ this.onClose(socket);
293
+ return;
294
+ }
295
+ this.send({ type: "ping" });
296
+ }, intervalMs);
297
+ }
298
+ clearHeartbeat() {
299
+ if (this.heartbeatTimer) {
300
+ clearInterval(this.heartbeatTimer);
301
+ this.heartbeatTimer = null;
302
+ }
303
+ }
304
+ /** Reap a socket that upgrades but never becomes `ready` (see READY_DEADLINE_MS). */
305
+ armReadyDeadline(socket) {
306
+ this.clearReadyDeadline();
307
+ const deadlineMs = this.options.heartbeatMs ?? READY_DEADLINE_MS;
308
+ this.readyDeadlineTimer = setTimeout(() => {
309
+ if (socket !== this.socket || this.state === "ready") return;
310
+ try {
311
+ socket.close(4e3, "ready deadline");
312
+ } catch {
313
+ }
314
+ this.onClose(socket);
315
+ }, deadlineMs);
316
+ }
317
+ clearReadyDeadline() {
318
+ if (this.readyDeadlineTimer) {
319
+ clearTimeout(this.readyDeadlineTimer);
320
+ this.readyDeadlineTimer = null;
321
+ }
322
+ }
323
+ clearTimers() {
324
+ this.clearHeartbeat();
325
+ this.clearReadyDeadline();
326
+ if (this.reconnectTimer) {
327
+ clearTimeout(this.reconnectTimer);
328
+ this.reconnectTimer = null;
329
+ }
330
+ }
331
+ send(frame) {
332
+ if (!this.socket || this.socket.readyState !== WS_OPEN) return;
333
+ try {
334
+ this.socket.send(JSON.stringify(frame));
335
+ } catch (err) {
336
+ this.emitError({ code: "REALTIME_SEND", message: err.message });
337
+ }
338
+ }
339
+ setState(state) {
340
+ if (this.state === state) return;
341
+ this.state = state;
342
+ for (const l of this.stateListeners) {
343
+ try {
344
+ l(state);
345
+ } catch {
346
+ }
347
+ }
348
+ }
349
+ emitError(error) {
350
+ for (const l of this.errorListeners) {
351
+ try {
352
+ l(error);
353
+ } catch {
354
+ }
355
+ }
356
+ }
357
+ failConnect(err) {
358
+ this.connectReject?.(err);
359
+ this.connectResolve = null;
360
+ this.connectReject = null;
361
+ this.connectPromise = null;
362
+ }
363
+ nextId() {
364
+ return `c${++this.idCounter}`;
365
+ }
366
+ };
367
+ var SeenSet = class {
368
+ constructor(max) {
369
+ this.max = max;
370
+ }
371
+ max;
372
+ set = /* @__PURE__ */ new Set();
373
+ has(id) {
374
+ return this.set.has(id);
375
+ }
376
+ add(id) {
377
+ this.set.add(id);
378
+ if (this.set.size > this.max) {
379
+ const oldest = this.set.values().next().value;
380
+ if (oldest !== void 0) this.set.delete(oldest);
381
+ }
382
+ }
383
+ };
384
+ function isRecord(v) {
385
+ return typeof v === "object" && v !== null;
386
+ }
387
+
388
+ // src/realtime/contract.ts
389
+ function isExtChannel(value) {
390
+ return value.startsWith("ext.") && value.length > 4;
391
+ }
392
+ var REALTIME_CHANNELS = [
393
+ "wallet.balance",
394
+ "wallet.deposit",
395
+ "wallet.withdrawal",
396
+ "gaming",
397
+ "bonus",
398
+ "player"
399
+ ];
400
+
401
+ export { REALTIME_CHANNELS, createRealtimeClient, isExtChannel };
402
+ //# sourceMappingURL=index.js.map
403
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/realtime/transport.ts","../../src/realtime/client.ts","../../src/realtime/contract.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * WebSocket transport resolution.\n *\n * The realtime client is isomorphic: in the browser it uses the global\n * `WebSocket`; in Node it uses an injected `ws` implementation (peer dependency).\n * It never imports `ws` statically so the core stays dependency-free and\n * tree-shakeable.\n */\nimport type { WebSocketImpl } from \"./contract.js\";\n\n/** Resolve a WebSocket constructor: explicit injection → browser global. */\nexport function resolveWebSocket(injected?: WebSocketImpl): WebSocketImpl {\n if (injected) return injected;\n const g = globalThis as { WebSocket?: WebSocketImpl };\n if (g.WebSocket) return g.WebSocket;\n throw new Error(\n \"No WebSocket implementation available. In Node, pass `WebSocketImpl` \" +\n \"(e.g. `import WebSocket from 'ws'`) in the realtime options.\",\n );\n}\n\n/** Standard ready-state constants (browser + `ws` agree on these values). */\nexport const WS_OPEN = 1;\n","/**\n * The realtime client: a single socket per player that hides the wire protocol,\n * reconnection, heartbeat, eventId dedupe, and post-reconnect re-sync.\n *\n * Wire protocol (mirrors `packages/realtime/src/protocol.ts` in the runtime):\n * - client → server: `auth`, `subscribe`, `unsubscribe`, `ping`\n * - server → client: `ready`, `subscribed`, `unsubscribed`, `event`, `error`,\n * `reconnect`, `pong`\n *\n * Push-only: this client never moves money. Correctness comes from `resync()`,\n * which re-reads authoritative state over REST after every (re)connect.\n */\nimport { resolveWebSocket, WS_OPEN } from \"./transport.js\";\nimport type {\n ChannelEventMap,\n ConnectionState,\n RealtimeChannel,\n RealtimeClient,\n RealtimeClientOptions,\n RealtimeError,\n Unsubscribe,\n WebSocketLike,\n} from \"./contract.js\";\n\n// ── Server frame shapes (decode side) ──────────────────────────────────────────\n\ninterface ReadyFrame {\n type: \"ready\";\n connectionId: string;\n heartbeatMs: number;\n resumeToken: string;\n}\ninterface EventFrame {\n type: \"event\";\n channel: RealtimeChannel;\n event: string;\n eventId: string;\n v: number;\n occurredAt: string;\n tenantId: string;\n data: unknown;\n}\ninterface ErrorFrame {\n type: \"error\";\n code: string;\n message: string;\n details?: unknown;\n id?: string;\n}\ninterface ReconnectFrame {\n type: \"reconnect\";\n reason: \"draining\" | \"slow_consumer\" | \"auth_expired\";\n}\ntype ServerFrame =\n | ReadyFrame\n | EventFrame\n | ErrorFrame\n | ReconnectFrame\n | { type: \"subscribed\" | \"unsubscribed\"; id?: string; channels: RealtimeChannel[] }\n | { type: \"pong\" };\n\ntype Handler = (event: ChannelEventMap[RealtimeChannel]) => void;\n\nconst DEFAULTS = { baseMs: 500, maxMs: 15_000, factor: 2, dedupeWindow: 512, heartbeatMs: 25_000 };\n\n/**\n * A caller awaiting `connect()` is rejected after this many failed connection\n * cycles. Background reconnection keeps going (with capped backoff) — observe\n * `onStateChange` for a later recovery.\n */\nconst MAX_PENDING_CONNECT_ATTEMPTS = 5;\n\n/** Close the socket when no `pong` arrives within this many heartbeat intervals. */\nconst PONG_DEADLINE_INTERVALS = 2;\n\n/**\n * A socket that upgrades but never delivers the `ready` frame (dropped auth\n * ticket, WS-speaking non-gateway proxy, hung gateway) is closed after this long\n * so the cycle fails, retries, and the pending `connect()` stays bounded — the\n * pong deadline can't reap it because the heartbeat only starts on `ready`.\n * `options.heartbeatMs` (documented test-only) overrides it in tests.\n */\nconst READY_DEADLINE_MS = 10_000;\n\nexport function createRealtimeClient(options: RealtimeClientOptions): RealtimeClient {\n return new RealtimeClientImpl(options);\n}\n\nclass RealtimeClientImpl implements RealtimeClient {\n state: ConnectionState = \"idle\";\n\n private socket: WebSocketLike | null = null;\n\n private readonly handlers = new Map<RealtimeChannel, Set<Handler>>();\n private readonly active = new Set<RealtimeChannel>();\n /**\n * Channels the app declared via `subscribe()` (as opposed to interest implied by\n * an `on()` handler). A channel is torn down only when BOTH interests are gone:\n * `unsubscribe()` must not kill a channel that still has a handler, and removing\n * the last handler must not kill a channel the app explicitly subscribed.\n */\n private readonly explicit = new Set<RealtimeChannel>();\n private readonly seen: SeenSet;\n\n private readonly stateListeners = new Set<(s: ConnectionState) => void>();\n private readonly errorListeners = new Set<(e: RealtimeError) => void>();\n\n private attempt = 0;\n private lastPongAt = 0;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private readyDeadlineTimer: ReturnType<typeof setTimeout> | null = null;\n private intentionalClose = false;\n /**\n * Teardown epoch. `disconnect()` bumps it; an `open()` that suspended (awaiting\n * the auth credential) before the bump must abandon its cycle when it resumes —\n * otherwise it resurrects an authenticated \"zombie\" socket after logout.\n */\n private epoch = 0;\n private lastOccurredAt: string | undefined;\n private idCounter = 0;\n private connectResolve: (() => void) | null = null;\n private connectReject: ((err: Error) => void) | null = null;\n private connectPromise: Promise<void> | null = null;\n\n constructor(private readonly options: RealtimeClientOptions) {\n this.seen = new SeenSet(options.dedupeWindow ?? DEFAULTS.dedupeWindow);\n }\n\n connect(): Promise<void> {\n if (this.state === \"ready\") return Promise.resolve();\n // Single-flight: while a connect/reconnect cycle is pending, every caller gets\n // the SAME promise — a second connect() (React StrictMode's double mount) must\n // not open a second socket or clobber the pending resolve/reject pair.\n if (this.connectPromise) return this.connectPromise;\n this.intentionalClose = false;\n const pending = new Promise<void>((resolve, reject) => {\n this.connectResolve = resolve;\n this.connectReject = reject;\n });\n const tracked: Promise<void> = pending.finally(() => {\n if (this.connectPromise === tracked) this.connectPromise = null;\n });\n this.connectPromise = tracked;\n // While auto-reconnect is already cycling (timer pending or open() in flight),\n // don't open a competing socket — the cycle settles this promise instead.\n if (this.state !== \"reconnecting\") void this.open();\n return tracked;\n }\n\n async disconnect(): Promise<void> {\n this.intentionalClose = true;\n this.epoch++; // any open() suspended mid-await must abandon its cycle\n this.clearTimers();\n // A caller still awaiting connect() settles now — it will never become ready.\n this.failConnect(new Error(\"realtime: disconnect() before the connection became ready\"));\n if (this.socket) {\n try {\n this.socket.close(1000, \"client disconnect\");\n } catch {\n /* ignore */\n }\n this.socket = null;\n }\n this.setState(\"closed\");\n }\n\n on<C extends RealtimeChannel>(\n channel: C,\n handler: (event: ChannelEventMap[C]) => void,\n ): Unsubscribe {\n let set = this.handlers.get(channel);\n if (!set) {\n set = new Set();\n this.handlers.set(channel, set);\n }\n set.add(handler as Handler);\n // Mark active now so the channel is (re)subscribed on the next `ready` frame —\n // whether we're connected yet or not (subscribe-before-connect is the norm).\n // Handler interest does NOT mark the channel `explicit`.\n if (!this.active.has(channel)) this.markActive([channel]);\n\n return () => {\n const handlers = this.handlers.get(channel);\n handlers?.delete(handler as Handler);\n if (handlers && handlers.size === 0) {\n this.handlers.delete(channel);\n if (!this.explicit.has(channel)) this.teardown([channel]);\n }\n };\n }\n\n async subscribe(channels: readonly RealtimeChannel[]): Promise<void> {\n for (const c of channels) this.explicit.add(c);\n this.markActive(channels);\n }\n\n async unsubscribe(channels: readonly RealtimeChannel[]): Promise<void> {\n for (const c of channels) this.explicit.delete(c);\n // Keep any channel a live on() handler still needs (L-SDK6).\n this.teardown(channels.filter((c) => !this.handlers.get(c)?.size));\n }\n\n async withSubscription<T>(\n channels: readonly RealtimeChannel[],\n scope: () => Promise<T>,\n ): Promise<T> {\n await this.subscribe(channels);\n try {\n return await scope();\n } finally {\n await this.unsubscribe(channels);\n }\n }\n\n activeChannels(): readonly RealtimeChannel[] {\n return [...this.active];\n }\n\n /** Register interest and subscribe on the wire when connected. */\n private markActive(channels: readonly RealtimeChannel[]): void {\n for (const c of channels) this.active.add(c);\n if (this.state === \"ready\") {\n this.send({ type: \"subscribe\", channels: [...channels], id: this.nextId() });\n }\n }\n\n /** Drop interest and unsubscribe on the wire when connected. */\n private teardown(channels: readonly RealtimeChannel[]): void {\n if (channels.length === 0) return;\n for (const c of channels) this.active.delete(c);\n if (this.state === \"ready\") {\n this.send({ type: \"unsubscribe\", channels: [...channels], id: this.nextId() });\n }\n }\n\n onStateChange(handler: (state: ConnectionState) => void): Unsubscribe {\n this.stateListeners.add(handler);\n return () => this.stateListeners.delete(handler);\n }\n\n onError(handler: (error: RealtimeError) => void): Unsubscribe {\n this.errorListeners.add(handler);\n return () => this.errorListeners.delete(handler);\n }\n\n // ── internals ────────────────────────────────────────────────────────────────\n\n private async open(): Promise<void> {\n const epoch = this.epoch;\n this.setState(this.attempt === 0 ? \"connecting\" : \"reconnecting\");\n let ticket: string | undefined;\n try {\n const cred = await this.options.getAuthCredential?.();\n if (cred && \"ticket\" in cred) ticket = cred.ticket;\n } catch (err) {\n if (epoch !== this.epoch) return; // torn down while fetching the credential\n this.emitError({ code: \"REALTIME_AUTH\", message: (err as Error).message });\n // Auth failures are deterministic (usually: not logged in) — settle the\n // pending connect() now instead of hanging it across the retry loop.\n this.failConnect(err instanceof Error ? err : new Error(String(err)));\n this.scheduleReconnect();\n return;\n }\n // disconnect() ran while we awaited the ticket: the pending connect() is\n // already settled and state is \"closed\" — do NOT resurrect a socket for the\n // dead cycle (it would come up authenticated as the logged-out user).\n if (epoch !== this.epoch) return;\n\n let socket: WebSocketLike;\n try {\n // resolveWebSocket inside the try: a missing WebSocket implementation must\n // reject the pending connect(), not escape `void this.open()` as an\n // unhandled rejection while connect() hangs forever.\n const WebSocketImpl = resolveWebSocket(this.options.WebSocketImpl);\n socket = new WebSocketImpl(this.options.url);\n } catch (err) {\n this.failConnect(err as Error);\n this.scheduleReconnect();\n return;\n }\n this.socket = socket;\n this.armReadyDeadline(socket);\n\n // Every handler is tagged to ITS socket: events from a socket that is no longer\n // `this.socket` (replaced during a reconnect race) must not touch live state —\n // a stale onclose used to null the current socket, orphaning it mid-heartbeat.\n socket.onopen = () => {\n if (socket !== this.socket) return;\n // Ticket path: authenticate in-band. Cookie path: the upgrade already carried it.\n if (ticket) this.send({ type: \"auth\", token: ticket });\n };\n socket.onmessage = (ev) => {\n if (socket !== this.socket) return;\n this.onMessage(ev.data);\n };\n socket.onerror = () => {\n if (socket !== this.socket) return;\n this.emitError({ code: \"REALTIME_SOCKET\", message: \"socket error\" });\n };\n socket.onclose = (ev) => this.onClose(socket, ev?.code);\n }\n\n private onMessage(raw: unknown): void {\n if (typeof raw !== \"string\") return;\n let frame: ServerFrame;\n try {\n frame = JSON.parse(raw) as ServerFrame;\n } catch {\n return;\n }\n\n switch (frame.type) {\n case \"ready\":\n this.onReady(frame);\n break;\n case \"event\":\n this.onEvent(frame);\n break;\n case \"error\":\n this.emitError({\n code: frame.code,\n message: frame.message,\n ...(isRecord(frame.details) ? { details: frame.details } : {}),\n });\n break;\n case \"reconnect\":\n // Server asked us to cycle; close and let auto-reconnect take over.\n try {\n this.socket?.close(1000, frame.reason);\n } catch {\n /* ignore */\n }\n break;\n case \"pong\":\n this.lastPongAt = Date.now();\n break;\n case \"subscribed\":\n case \"unsubscribed\":\n break;\n }\n }\n\n private onReady(frame: ReadyFrame): void {\n this.attempt = 0;\n this.clearReadyDeadline();\n this.setState(\"ready\");\n this.startHeartbeat(frame.heartbeatMs || this.options.heartbeatMs || DEFAULTS.heartbeatMs);\n\n // Replay the currently-active channels (subscribe-once-and-forget).\n if (this.active.size > 0) {\n this.send({ type: \"subscribe\", channels: [...this.active], id: this.nextId() });\n }\n\n // Reconcile any state missed while disconnected. REST is the source of truth.\n const ctx = this.lastOccurredAt !== undefined ? { since: this.lastOccurredAt } : {};\n void Promise.resolve(this.options.resync?.(ctx)).catch(() => {\n /* app-supplied; swallow */\n });\n\n this.connectResolve?.();\n this.connectResolve = null;\n this.connectReject = null;\n }\n\n private onEvent(frame: EventFrame): void {\n if (this.seen.has(frame.eventId)) return; // dedupe across reconnect/resume overlap\n this.seen.add(frame.eventId);\n this.lastOccurredAt = frame.occurredAt;\n\n const handlers = this.handlers.get(frame.channel);\n if (!handlers) return;\n const event = frame as unknown as ChannelEventMap[RealtimeChannel];\n for (const handler of handlers) {\n try {\n handler(event);\n } catch (err) {\n this.emitError({ code: \"REALTIME_HANDLER\", message: (err as Error).message });\n }\n }\n }\n\n private onClose(socket: WebSocketLike, code?: number): void {\n // A close from a socket we already replaced is stale — ignoring it keeps the\n // live socket's heartbeat and reconnect schedule intact.\n if (socket !== this.socket) return;\n this.clearTimers();\n this.socket = null;\n if (this.intentionalClose) {\n this.setState(\"closed\");\n return;\n }\n // 4401 = unauthorized: getAuthCredential() will refresh on the next open().\n this.scheduleReconnect(code);\n }\n\n private scheduleReconnect(_code?: number): void {\n if (this.intentionalClose) return;\n this.setState(\"reconnecting\");\n // Bounded wait for callers: after enough failed cycles, reject the pending\n // connect() promise. Reconnection itself keeps cycling in the background.\n if (this.connectReject && this.attempt + 1 >= MAX_PENDING_CONNECT_ATTEMPTS) {\n this.failConnect(\n new Error(\n `realtime: connection not ready after ${MAX_PENDING_CONNECT_ATTEMPTS} attempts (still retrying in the background)`,\n ),\n );\n }\n const delay = this.backoffDelay(this.attempt++);\n this.reconnectTimer = setTimeout(() => void this.open(), delay);\n }\n\n private backoffDelay(attempt: number): number {\n const { baseMs, maxMs, factor } = {\n baseMs: this.options.backoff?.baseMs ?? DEFAULTS.baseMs,\n maxMs: this.options.backoff?.maxMs ?? DEFAULTS.maxMs,\n factor: this.options.backoff?.factor ?? DEFAULTS.factor,\n };\n const ceiling = Math.min(maxMs, baseMs * factor ** attempt);\n return Math.floor(Math.random() * ceiling); // full jitter\n }\n\n private startHeartbeat(intervalMs: number): void {\n this.clearHeartbeat();\n this.lastPongAt = Date.now();\n this.heartbeatTimer = setInterval(() => {\n const socket = this.socket;\n if (!socket || socket.readyState !== WS_OPEN) return;\n if (Date.now() - this.lastPongAt >= intervalMs * PONG_DEADLINE_INTERVALS) {\n // No pong across the deadline: the connection is dead (half-open TCP after\n // a network change, say) and would otherwise show stale state for minutes.\n // Close and run the close path NOW — reconnect + REST resync — rather than\n // waiting for a close event a dead socket may never fire.\n try {\n socket.close(4000, \"pong deadline\");\n } catch {\n /* ignore */\n }\n this.onClose(socket);\n return;\n }\n this.send({ type: \"ping\" });\n }, intervalMs);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /** Reap a socket that upgrades but never becomes `ready` (see READY_DEADLINE_MS). */\n private armReadyDeadline(socket: WebSocketLike): void {\n this.clearReadyDeadline();\n const deadlineMs = this.options.heartbeatMs ?? READY_DEADLINE_MS;\n this.readyDeadlineTimer = setTimeout(() => {\n if (socket !== this.socket || this.state === \"ready\") return;\n try {\n socket.close(4000, \"ready deadline\");\n } catch {\n /* ignore */\n }\n this.onClose(socket); // idempotent via the socket tag\n }, deadlineMs);\n }\n\n private clearReadyDeadline(): void {\n if (this.readyDeadlineTimer) {\n clearTimeout(this.readyDeadlineTimer);\n this.readyDeadlineTimer = null;\n }\n }\n\n private clearTimers(): void {\n this.clearHeartbeat();\n this.clearReadyDeadline();\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private send(frame: Record<string, unknown>): void {\n if (!this.socket || this.socket.readyState !== WS_OPEN) return;\n try {\n this.socket.send(JSON.stringify(frame));\n } catch (err) {\n this.emitError({ code: \"REALTIME_SEND\", message: (err as Error).message });\n }\n }\n\n private setState(state: ConnectionState): void {\n if (this.state === state) return;\n this.state = state;\n // Listener guards mirror onEvent's: an app listener throwing must not abort\n // the lifecycle transition that invoked it (e.g. wedge a reconnect cycle).\n for (const l of this.stateListeners) {\n try {\n l(state);\n } catch {\n /* app-supplied; swallow */\n }\n }\n }\n\n private emitError(error: RealtimeError): void {\n for (const l of this.errorListeners) {\n try {\n l(error);\n } catch {\n /* app-supplied; swallow */\n }\n }\n }\n\n private failConnect(err: Error): void {\n this.connectReject?.(err);\n this.connectResolve = null;\n this.connectReject = null;\n // Clear synchronously (not just in the tracked promise's finally microtask):\n // a connect() issued in the same tick as disconnect() must start a fresh\n // cycle, not adopt the already-rejected promise.\n this.connectPromise = null;\n }\n\n private nextId(): string {\n return `c${++this.idCounter}`;\n }\n}\n\n/** Bounded insertion-ordered set for eventId dedupe (LRU-ish via FIFO eviction). */\nclass SeenSet {\n private readonly set = new Set<string>();\n constructor(private readonly max: number) {}\n has(id: string): boolean {\n return this.set.has(id);\n }\n add(id: string): void {\n this.set.add(id);\n if (this.set.size > this.max) {\n const oldest = this.set.values().next().value;\n if (oldest !== undefined) this.set.delete(oldest);\n }\n }\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n","/**\n * Canonical realtime surface, aligned with the runtime's\n * `docs/realtime-sdk-contract.ts` (the agreed contract; verbatim copy kept in\n * `contract.vendored.ts` for diffing — `pnpm sync:contract` refreshes it). We\n * ALIGN with that file — do not fork it. Naming note: the runtime doc calls the\n * core-channel union `RealtimeChannel` and the core+ext union\n * `AnyRealtimeChannel`; here they are {@link CoreRealtimeChannel} and\n * {@link RealtimeChannel} respectively. The plugin `ext.*` channel family\n * ({@link ExtChannel}, {@link ExtPluginEvent}) is part of the runtime contract.\n * SDK-local additions: {@link WebSocketImpl}, `dedupeWindow`,\n * {@link REALTIME_CHANNELS}, {@link isExtChannel}.\n *\n * Money amounts are scale-4 integer minor units (see `core/money.ts`).\n */\n\n/** Statically-known core channels. Strings are stable, versioned, and authz-checked server-side. */\nexport type CoreRealtimeChannel =\n \"wallet.balance\" | \"wallet.deposit\" | \"wallet.withdrawal\" | \"gaming\" | \"bonus\" | \"player\";\n\n/**\n * Channel family published by tenant-enabled plugins, e.g. `\"ext.cashback\"`.\n * Which ones exist comes from the ext catalog (`ExtCatalogPlugin.channels`);\n * subscribing for a non-enabled plugin is rejected by the gateway exactly like\n * an unknown channel.\n */\nexport type ExtChannel = `ext.${string}`;\n\n/** Logical channels: the static core set plus the dynamic plugin family. */\nexport type RealtimeChannel = CoreRealtimeChannel | ExtChannel;\n\n/**\n * Narrow a plain string (e.g. an entry of `ExtCatalogPlugin.channels`, which the\n * wire contract types as `string[]`) to a subscribable {@link ExtChannel}.\n */\nexport function isExtChannel(value: string): value is ExtChannel {\n return value.startsWith(\"ext.\") && value.length > 4;\n}\n\n/**\n * All statically-known subscribable channels, for runtime validation/iteration.\n * Plugin `ext.*` channels are dynamic (discovered via the ext catalog) and\n * intentionally not listed.\n */\nexport const REALTIME_CHANNELS: readonly CoreRealtimeChannel[] = [\n \"wallet.balance\",\n \"wallet.deposit\",\n \"wallet.withdrawal\",\n \"gaming\",\n \"bonus\",\n \"player\",\n];\n\nexport type WalletBucket = \"cash\" | \"bonus\" | \"locked\";\n\n/** Minor units (scale-4 integer). Never do money math on the client beyond display. */\nexport type MinorUnits = number;\n\n/** Common envelope present on every server → client business event. */\nexport interface RealtimeEventMeta {\n readonly event: string;\n readonly eventId: string;\n readonly v: number;\n readonly occurredAt: string;\n readonly tenantId: string;\n}\n\nexport interface WalletBalanceEvent extends RealtimeEventMeta {\n readonly channel: \"wallet.balance\";\n readonly data: {\n readonly currency: string;\n readonly balances: Readonly<Record<WalletBucket, MinorUnits>>;\n readonly change: {\n readonly bucket: WalletBucket;\n readonly direction: \"credit\" | \"debit\";\n readonly amount: MinorUnits;\n readonly reason: string;\n };\n };\n}\n\nexport type DepositStatus = \"pending\" | \"processing\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface WalletDepositEvent extends RealtimeEventMeta {\n readonly channel: \"wallet.deposit\";\n readonly data: {\n readonly depositId: string;\n readonly status: DepositStatus;\n readonly amount: MinorUnits;\n readonly currency: string;\n };\n}\n\nexport type WithdrawalStatus =\n \"requested\" | \"approved\" | \"rejected\" | \"processing\" | \"completed\" | \"reversed\" | \"cancelled\";\n\nexport interface WalletWithdrawalEvent extends RealtimeEventMeta {\n readonly channel: \"wallet.withdrawal\";\n readonly data: {\n readonly withdrawalId: string;\n readonly status: WithdrawalStatus;\n readonly amount: MinorUnits;\n readonly currency: string;\n };\n}\n\nexport interface GamingEvent extends RealtimeEventMeta {\n readonly channel: \"gaming\";\n readonly data: {\n readonly roundId: string;\n readonly betId?: string;\n readonly status: \"placed\" | \"settled\" | \"rolled_back\" | \"round_closed\";\n /** Internal catalog game id — map to a display name from your catalog. */\n readonly gameId?: string;\n /** Provider key that produced the round (e.g. `\"slotserv\"`). */\n readonly provider?: string;\n /** Wager amount, present on `placed`. */\n readonly betAmount?: MinorUnits;\n /** Payout, present on `settled`. */\n readonly winAmount?: MinorUnits;\n readonly currency?: string;\n };\n}\n\nexport interface BonusEvent extends RealtimeEventMeta {\n readonly channel: \"bonus\";\n readonly data: {\n readonly bonusId: string;\n readonly status: \"granted\" | \"revoked\";\n readonly amount?: MinorUnits;\n readonly currency?: string;\n readonly reason?: string;\n };\n}\n\nexport interface PlayerEvent extends RealtimeEventMeta {\n readonly channel: \"player\";\n // Client-safe account-change hints: never emails/IPs/hashes — a coarse `change`\n // kind plus explicitly-safe scalars in `data` for changes that carry context.\n readonly data: {\n readonly change:\n | \"password_changed\"\n | \"social_linked\"\n | \"social_unlinked\"\n | \"account_locked\"\n | \"email_verified\"\n | \"phone_verified\"\n | \"session_revoked\"\n | \"kyc_status_changed\"\n | \"kyc_documents_requested\"\n | \"limit_changed\"\n | \"self_excluded\"\n | \"reality_check\";\n readonly data?: Readonly<Record<string, string | number | boolean | null>>;\n };\n}\n\n/**\n * An event published by a plugin on its `ext.<pluginKey>` channel. The standard\n * realtime envelope applies (incl. `eventId` dedupe); `data.type` is the\n * plugin-defined event type (e.g. `\"cashback.claimed\"`) and `data.payload` is\n * opaque to the SDK.\n */\nexport interface ExtPluginEvent extends RealtimeEventMeta {\n readonly channel: ExtChannel;\n readonly data: {\n readonly type: string;\n readonly payload: unknown;\n };\n}\n\n/** Maps a channel name to its event payload type, for type-safe `on(...)`. */\nexport interface ChannelEventMap {\n \"wallet.balance\": WalletBalanceEvent;\n \"wallet.deposit\": WalletDepositEvent;\n \"wallet.withdrawal\": WalletWithdrawalEvent;\n gaming: GamingEvent;\n bonus: BonusEvent;\n player: PlayerEvent;\n [channel: `ext.${string}`]: ExtPluginEvent;\n}\n\nexport type ConnectionState = \"idle\" | \"connecting\" | \"ready\" | \"reconnecting\" | \"closed\";\n\nexport interface RealtimeError {\n readonly code: string;\n readonly message: string;\n readonly details?: Record<string, unknown>;\n}\n\nexport interface RealtimeClientOptions {\n /** wss URL of the gateway, e.g. `wss://grandbet.example/realtime`. */\n readonly url: string;\n /**\n * How the SDK obtains a handshake credential. Return `{ ticket }` (fetched from\n * `POST /realtime/ticket`) or nothing to rely on a same-site HttpOnly cookie.\n * Called again on every reconnect so an expiring credential never drops the socket.\n */\n readonly getAuthCredential?: () => Promise<{ ticket: string } | void>;\n /**\n * Snapshot read run after every (re)connect to reconcile missed state. The socket\n * accelerates; REST is the truth. The SDK calls this; the app supplies the reads.\n */\n readonly resync?: (ctx: { since?: string }) => Promise<void>;\n /** Reconnect backoff. Defaults: base 500ms, factor 2, max 15s, full jitter. */\n readonly backoff?: {\n readonly baseMs?: number;\n readonly maxMs?: number;\n readonly factor?: number;\n };\n /** Override only for testing; the server dictates heartbeat in the `ready` frame. */\n readonly heartbeatMs?: number;\n /** Inject a WebSocket implementation (Node `ws`); defaults to the platform global. */\n readonly WebSocketImpl?: WebSocketImpl;\n /** Max remembered eventIds for dedupe. Default 512. */\n readonly dedupeWindow?: number;\n}\n\nexport type Unsubscribe = () => void;\n\nexport interface RealtimeClient {\n readonly state: ConnectionState;\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n on<C extends RealtimeChannel>(\n channel: C,\n handler: (event: ChannelEventMap[C]) => void,\n ): Unsubscribe;\n subscribe(channels: readonly RealtimeChannel[]): Promise<void>;\n unsubscribe(channels: readonly RealtimeChannel[]): Promise<void>;\n withSubscription<T>(channels: readonly RealtimeChannel[], scope: () => Promise<T>): Promise<T>;\n activeChannels(): readonly RealtimeChannel[];\n onStateChange(handler: (state: ConnectionState) => void): Unsubscribe;\n onError(handler: (error: RealtimeError) => void): Unsubscribe;\n}\n\nexport type CreateRealtimeClient = (options: RealtimeClientOptions) => RealtimeClient;\n\n// ── Minimal structural WebSocket type (browser global or Node `ws`) ─────────────\n\nexport interface WebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n onopen: ((ev: unknown) => void) | null;\n onclose: ((ev: { code?: number; reason?: string }) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n}\n\nexport type WebSocketImpl = new (url: string) => WebSocketLike;\n"]}
package/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "@clovnet/casino-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Player / casino frontend SDK for the CasinoWebEngine Runtime Core \u2014 auth, cashier, wallet, catalog, game launch, and realtime, fully typed and isomorphic.",
5
+ "keywords": [
6
+ "casino",
7
+ "igaming",
8
+ "sdk",
9
+ "casinowebengine",
10
+ "realtime",
11
+ "typescript"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Clovnet",
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ },
37
+ "./react": {
38
+ "types": "./dist/react/index.d.ts",
39
+ "import": "./dist/react/index.js",
40
+ "require": "./dist/react/index.cjs"
41
+ },
42
+ "./realtime": {
43
+ "types": "./dist/realtime/index.d.ts",
44
+ "import": "./dist/realtime/index.js",
45
+ "require": "./dist/realtime/index.cjs"
46
+ },
47
+ "./package.json": "./package.json"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch --onSuccess \"yalc push --no-scripts\"",
52
+ "clean": "rm -rf dist",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:watch": "vitest",
57
+ "test:contract": "CWE_CONTRACT_TEST=1 vitest run contract",
58
+ "lint": "eslint .",
59
+ "format": "prettier --write .",
60
+ "format:check": "prettier --check .",
61
+ "docs": "typedoc",
62
+ "sync:contract": "tsx scripts/sync-contract.ts",
63
+ "audit:contract": "./scripts/audit-runtime-drift.sh",
64
+ "audit:contract:fix": "./scripts/audit-runtime-drift.sh --fix",
65
+ "changeset": "changeset",
66
+ "release": "pnpm build && changeset publish",
67
+ "prepack": "pnpm build"
68
+ },
69
+ "peerDependencies": {
70
+ "react": ">=18",
71
+ "ws": ">=8"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "react": {
75
+ "optional": true
76
+ },
77
+ "ws": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@changesets/cli": "^2.27.10",
83
+ "@eslint/js": "^9.15.0",
84
+ "@testing-library/react": "^16.3.2",
85
+ "@types/node": "^20.17.6",
86
+ "@types/react": "^18.3.12",
87
+ "@types/ws": "^8.5.13",
88
+ "@typescript-eslint/eslint-plugin": "^8.15.0",
89
+ "@typescript-eslint/parser": "^8.15.0",
90
+ "@vitest/coverage-v8": "2.1.9",
91
+ "eslint": "^9.15.0",
92
+ "jsdom": "^29.1.1",
93
+ "prettier": "^3.4.1",
94
+ "react": "^18.3.1",
95
+ "react-dom": "^18.3.1",
96
+ "tsup": "^8.3.5",
97
+ "tsx": "^4.19.2",
98
+ "typedoc": "^0.27.2",
99
+ "typescript": "^5.7.2",
100
+ "vitest": "^2.1.6",
101
+ "ws": "^8.18.0"
102
+ },
103
+ "repository": {
104
+ "type": "git",
105
+ "url": "git+https://github.com/Clovnet/casino-sdk.git"
106
+ },
107
+ "homepage": "https://clovnet.com",
108
+ "bugs": {
109
+ "url": "https://github.com/Clovnet/casino-sdk/issues"
110
+ }
111
+ }