kicksdk-js 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // cannot be fetched beside this file, pass its bytes: `init(bytes)`).
7
7
  //
8
8
  // Where it works: Kick's realtime server only takes a socket whose Origin is
9
- // https://kick.com. Node sets that header itself (see `createSocket` below); a
9
+ // https://kick.com. Node sets that header itself (see `socketFactory` below); a
10
10
  // browser cannot, so in a browser connectChat only works on kick.com - a page
11
11
  // there, or an extension's content script. Elsewhere, relay through a server.
12
12
 
@@ -19,7 +19,8 @@ import initWasm, {
19
19
  connectionRequest,
20
20
  parseConnection,
21
21
  tokenRequest,
22
- parseToken
22
+ parseToken,
23
+ retryAfterMs
23
24
  } from './pkg/kicksdk.js';
24
25
 
25
26
  export * from './pkg/kicksdk.js';
@@ -28,14 +29,34 @@ export const REQUIRED_ORIGIN = 'https://kick.com';
28
29
 
29
30
  let ready = null;
30
31
 
31
- /** Loads the WebAssembly. Safe to call more than once. */
32
+ /**
33
+ * Loads the WebAssembly. Safe to call more than once; if loading fails, the
34
+ * next call tries again.
35
+ */
32
36
  export function init(input) {
33
37
  if (!ready) {
34
- if (input instanceof Uint8Array || input instanceof ArrayBuffer) {
35
- initSync({ module: input });
38
+ const bytes =
39
+ input instanceof Uint8Array ||
40
+ input instanceof ArrayBuffer ||
41
+ (typeof WebAssembly !== 'undefined' && input instanceof WebAssembly.Module);
42
+ if (bytes) {
43
+ try {
44
+ initSync({ module: input });
45
+ } catch (e) {
46
+ return Promise.reject(e);
47
+ }
36
48
  ready = Promise.resolve();
37
49
  } else {
38
- ready = initWasm(input === undefined ? undefined : { module_or_path: input });
50
+ // A failed load (a fetch that did not get through) is not kept: a
51
+ // later init() starts over instead of returning the same rejection.
52
+ const loading = initWasm(input === undefined ? undefined : { module_or_path: input }).then(
53
+ () => undefined,
54
+ (e) => {
55
+ if (ready === loading) ready = null;
56
+ throw e;
57
+ }
58
+ );
59
+ ready = loading;
39
60
  }
40
61
  }
41
62
  return ready;
@@ -43,100 +64,267 @@ export function init(input) {
43
64
 
44
65
  const isNode = typeof process !== 'undefined' && !!(process.versions && process.versions.node);
45
66
 
46
- function defaultSocket(url) {
47
- // Node's WebSocket (undici) takes headers; a browser's ignores the argument
48
- // and sends its own page's origin.
49
- return isNode ? new WebSocket(url, { headers: { Origin: REQUIRED_ORIGIN } }) : new WebSocket(url);
67
+ /** How long one step of connecting (a fetch, the WebSocket handshake) may take. */
68
+ const DEFAULT_TIMEOUT_MS = 10_000;
69
+
70
+ /** The longest wait setTimeout holds; a longer one fires at once. */
71
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
72
+
73
+ const NO_WEBSOCKET =
74
+ 'no WebSocket in this runtime (Node 22 and later have one): pass connectChat a `WebSocket` ' +
75
+ "constructor (the `ws` package's will do) or `createSocket`";
76
+
77
+ const noop = () => {};
78
+
79
+ const message = (e) => String((e && e.message) || e);
80
+
81
+ /** A clock that only goes forward, so a wall-clock jump cannot fake silence. */
82
+ function monotonicClock() {
83
+ const p = globalThis.performance;
84
+ return p && typeof p.now === 'function' ? () => p.now() : () => Date.now();
85
+ }
86
+
87
+ /**
88
+ * The session's clock, whole ms: the caller's `now`, or performance.now().
89
+ * A reading of the caller's that is not a number from 0 to
90
+ * Number.MAX_SAFE_INTEGER (or a `now` that throws) is reported once, and the
91
+ * clock goes on with performance.now() from the last good reading: the
92
+ * session could do nothing with it, and every attempt would fail the same way.
93
+ */
94
+ function sessionClock(given, report) {
95
+ const mono = monotonicClock();
96
+ if (!given) return () => Math.floor(mono());
97
+ let last = 0;
98
+ let fallback = null;
99
+ return () => {
100
+ if (!fallback) {
101
+ let bad;
102
+ try {
103
+ const t = given();
104
+ if (typeof t === 'number' && t >= 0 && t <= Number.MAX_SAFE_INTEGER) return (last = Math.floor(t));
105
+ bad = new RangeError(`connectChat: now() returned ${String(t)}, not milliseconds; using performance.now()`);
106
+ } catch (e) {
107
+ bad = e;
108
+ }
109
+ report(bad);
110
+ const base = last;
111
+ const start = mono();
112
+ fallback = () => base + Math.floor(mono() - start);
113
+ }
114
+ return fallback();
115
+ };
116
+ }
117
+
118
+ /** A guest client id, a v4 UUID. Not a secret: it names the connection. */
119
+ function guestId() {
120
+ const c = globalThis.crypto;
121
+ // randomUUID is missing outside secure contexts, and Node 18 has no
122
+ // global crypto at all.
123
+ if (c && typeof c.randomUUID === 'function') return c.randomUUID();
124
+ const b = new Uint8Array(16);
125
+ if (c && typeof c.getRandomValues === 'function') c.getRandomValues(b);
126
+ else for (let i = 0; i < b.length; i++) b[i] = Math.floor(Math.random() * 256);
127
+ b[6] = (b[6] & 0x0f) | 0x40;
128
+ b[8] = (b[8] & 0x3f) | 0x80;
129
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
130
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
50
131
  }
51
132
 
52
- async function send(fetchImpl, req) {
53
- // A browser drops the headers it forbids (origin, referer) and sends its own.
54
- const res = await fetchImpl(req.url, {
55
- method: req.method,
56
- headers: Object.fromEntries(req.headers),
57
- body: req.body ?? undefined,
58
- credentials: 'omit'
59
- });
60
- return { status: res.status, body: await res.text() };
133
+ function socketFactory(o) {
134
+ if (o.createSocket) return (url) => o.createSocket(url);
135
+ const WS = o.WebSocket || globalThis.WebSocket;
136
+ if (typeof WS !== 'function') return null;
137
+ // Node's WebSocket (undici) and the `ws` package take headers; a browser's
138
+ // would read the object as a subprotocol, and sends its page's origin
139
+ // whatever it is given.
140
+ return isNode ? (url) => new WS(url, { headers: { Origin: REQUIRED_ORIGIN } }) : (url) => new WS(url);
61
141
  }
62
142
 
63
143
  /**
64
144
  * Connects to one channel's chat and keeps the connection up until close().
145
+ * Whatever ends a connection, connectChat waits and connects again; after one
146
+ * of Kick's terminal closes (a refused token, a ban, a connection limit) the
147
+ * wait is long - 5, 10, 20, then 30 minutes in a row, see KICK.md §6.1 - and
148
+ * each new connection is a new guest client.
149
+ *
150
+ * Callbacks are isolated: one that throws is reported to `onError` (or
151
+ * console.error) and the connection carries on. None is called after
152
+ * close(), which may be called from inside one.
65
153
  *
66
- * @param {object} o
67
- * @param {number} o.chatroomId the chatroom (website API `chatroom.id`)
68
- * @param {number} [o.channelId] the channel; also locates the chat's region
69
- * @param {string[]} [o.channels] feeds to subscribe to; chatChannels() by default
70
- * @param {(e: {channel, name, data, event}) => void} o.onEvent
71
- * @param {(s: {connected: boolean, subscribed: string[]}) => void} [o.onState]
72
- * @param {(reason: string) => void} [o.onDrop] a connection ended (a new one follows)
73
- * @param {typeof fetch} [o.fetch]
74
- * @param {(url: string) => WebSocket} [o.createSocket]
75
- * @param {object} [o.endpoints] overrides of Kick's URLs
76
- * @param {() => number} [o.now]
154
+ * @param {object} o ChatOptions, in index.d.ts
77
155
  * @returns {{ close(): void }}
78
156
  */
79
157
  export function connectChat(o) {
80
- const fetchImpl = o.fetch || ((...a) => fetch(...a));
81
- const createSocket = o.createSocket || defaultSocket;
82
- const now = o.now || (() => Date.now());
83
- const channels = o.channels || chatChannels(o.chatroomId, o.channelId);
84
- const backoff = new Backoff();
85
- let closed = false;
158
+ if (!o || typeof o.onEvent !== 'function') throw new TypeError('connectChat: onEvent is required');
159
+ for (const name of ['onState', 'onDrop', 'onError', 'fetch', 'createSocket', 'WebSocket', 'now']) {
160
+ if (o[name] != null && typeof o[name] !== 'function') {
161
+ throw new TypeError(`connectChat: ${name} must be a function`);
162
+ }
163
+ }
164
+ const timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
165
+ // setTimeout fires a longer wait (Infinity among them) at once: every
166
+ // request would time out before it began, and nothing would connect.
167
+ if (typeof timeoutMs !== 'number' || !(timeoutMs > 0 && timeoutMs <= MAX_TIMEOUT_MS)) {
168
+ throw new RangeError(`connectChat: timeoutMs must be a number of ms above 0, at most ${MAX_TIMEOUT_MS}`);
169
+ }
170
+ // Ids and endpoints are checked here, where a mistake throws, rather than
171
+ // failing every reconnect.
172
+ // A feed listed twice is subscribed once, and reported once.
173
+ const channels = [...new Set(o.channels ?? chatChannels(o.chatroomId, o.channelId))];
174
+ const hasChannel = o.channelId !== undefined && o.channelId !== null;
175
+ if (hasChannel) chatConnectionRequest(o.channelId, 'x', o.endpoints);
176
+ else connectionRequest('x', o.endpoints);
177
+
178
+ const fetchImpl = o.fetch || ((...a) => globalThis.fetch(...a));
179
+ const createSocket = socketFactory(o);
180
+ // It would fail the same way on every attempt.
181
+ if (!createSocket) throw new TypeError(`connectChat: ${NO_WEBSOCKET}`);
182
+ const now = sessionClock(o.now, (e) => report(e));
183
+
184
+ let session = new Session(channels);
185
+ let backoff = new Backoff();
186
+ let closed = false; // close() was called
86
187
  let ws = null;
87
- let session = null;
88
- let timer = null;
89
- let retry = null;
188
+ let attempt = null; // aborts the current connection's fetches
90
189
  let clientId = null;
190
+ let timer = null; // session.deadline()
191
+ let handshake = null;
192
+ let retry = null;
91
193
 
92
- const state = () => {
93
- if (o.onState && session) {
94
- o.onState({ connected: session.isConnected(), subscribed: channels.filter((c) => session.isSubscribed(c)) });
194
+ function report(error) {
195
+ if (o.onError) {
196
+ try {
197
+ o.onError(error);
198
+ return;
199
+ } catch (e) {
200
+ error = e;
201
+ }
95
202
  }
96
- };
203
+ console.error('kicksdk connectChat:', error);
204
+ }
205
+
206
+ /** A user callback, by name: never after close(), and never fatal. */
207
+ function callback(name, ...args) {
208
+ const fn = o[name];
209
+ if (closed || typeof fn !== 'function') return;
210
+ try {
211
+ fn.apply(o, args);
212
+ } catch (e) {
213
+ report(e);
214
+ }
215
+ }
216
+
217
+ /** An entry point (a socket event, a timer): a failure ends the connection, not the driver. */
218
+ function guard(fn) {
219
+ return (...args) => {
220
+ try {
221
+ return fn(...args);
222
+ } catch (e) {
223
+ report(e);
224
+ if (!closed) end(`kicksdk: ${message(e)}`, 0, 0, false);
225
+ }
226
+ };
227
+ }
228
+
229
+ /** A fetch that gives up after timeoutMs, or when `signal` aborts. */
230
+ function request(req, what, signal) {
231
+ return new Promise((resolve, reject) => {
232
+ const ctrl = new AbortController();
233
+ const cleanup = () => {
234
+ clearTimeout(timeout);
235
+ signal.removeEventListener('abort', onAbort);
236
+ };
237
+ const fail = (e) => {
238
+ cleanup();
239
+ ctrl.abort(e);
240
+ reject(e);
241
+ };
242
+ const onAbort = () => fail(new Error(`${what}: cancelled`));
243
+ const timeout = setTimeout(() => fail(new Error(`${what}: no answer in ${timeoutMs} ms`)), timeoutMs);
244
+ if (signal.aborted) return onAbort();
245
+ signal.addEventListener('abort', onAbort);
246
+ (async () => {
247
+ // A browser drops the headers it forbids (origin, referer) and sends its own.
248
+ const res = await fetchImpl(req.url, {
249
+ method: req.method,
250
+ headers: Object.fromEntries(req.headers),
251
+ body: req.body ?? undefined,
252
+ credentials: 'omit',
253
+ signal: ctrl.signal
254
+ });
255
+ return { status: res.status, body: await res.text() };
256
+ })().then(
257
+ (r) => {
258
+ cleanup();
259
+ resolve(r);
260
+ },
261
+ (e) => {
262
+ cleanup();
263
+ reject(e);
264
+ }
265
+ );
266
+ });
267
+ }
268
+
269
+ function state() {
270
+ if (closed) return;
271
+ callback('onState', {
272
+ connected: session.isConnected(),
273
+ subscribed: channels.filter((c) => session.isSubscribed(c))
274
+ });
275
+ }
97
276
 
98
277
  function arm() {
99
278
  clearTimeout(timer);
100
- const at = session && session.deadline();
101
- if (at !== undefined && at !== null) timer = setTimeout(() => apply(session.tick(now())), Math.max(0, at - now()));
279
+ timer = null;
280
+ if (closed) return;
281
+ const at = session.deadline();
282
+ if (at === undefined || at === null) return;
283
+ timer = setTimeout(
284
+ guard(() => {
285
+ timer = null;
286
+ if (!closed) apply(session.tick(now()));
287
+ }),
288
+ // A clock that jumped could ask for more than setTimeout holds; a tick
289
+ // before the deadline does nothing, and arms the rest.
290
+ Math.min(MAX_TIMEOUT_MS, Math.max(0, at - now()))
291
+ );
102
292
  }
103
293
 
104
294
  function apply(actions) {
105
- // Anything still in flight once close() has run - a timer, a token
106
- // arriving - finds nothing left to act on.
107
- if (closed || !session) return;
108
295
  let changed = false;
109
296
  for (const a of actions) {
297
+ // A callback may have called close().
298
+ if (closed) return;
110
299
  switch (a.action) {
111
300
  case 'send':
112
301
  if (ws && ws.readyState === 1) ws.send(a.text);
113
302
  break;
114
303
  case 'close':
115
- if (o.onDrop) o.onDrop(a.reason);
116
- if (ws) ws.close();
117
- break;
304
+ // Kick said goodbye, or went quiet. The socket is finished now: a
305
+ // dead connection may not deliver its close event for minutes.
306
+ end(a.reason, a.code, a.retry_after_ms, a.terminal);
307
+ return;
118
308
  case 'refresh_token':
119
- send(fetchImpl, tokenRequest(clientId, o.endpoints))
120
- .then((r) => parseToken(r.status, r.body))
121
- .then((token) => {
122
- if (!closed && session) apply(session.refresh(token, now()));
123
- })
124
- .catch(() => {
125
- // Kick ends the connection when the old token runs out; the
126
- // reconnect fetches a new one.
127
- });
309
+ refresh();
128
310
  break;
129
311
  case 'subscribed':
312
+ // The chat feed (the first listed) is up: back to short waits, and
313
+ // a terminal streak is over.
130
314
  if (a.channel === channels[0]) backoff.reset();
131
315
  changed = true;
132
316
  break;
133
317
  case 'connected':
318
+ // With no feed to subscribe to, the connection is all there is.
319
+ if (channels.length === 0) backoff.reset();
320
+ changed = true;
321
+ break;
134
322
  case 'unsubscribed':
135
323
  case 'subscribe_failed':
136
324
  changed = true;
137
325
  break;
138
326
  case 'event':
139
- o.onEvent({ channel: a.channel, name: a.name, data: a.data, event: a.event });
327
+ callback('onEvent', { channel: a.channel, name: a.name, data: a.data, event: a.event });
140
328
  break;
141
329
  }
142
330
  }
@@ -144,77 +332,168 @@ export function connectChat(o) {
144
332
  arm();
145
333
  }
146
334
 
147
- function again() {
335
+ function refresh() {
336
+ const ctrl = attempt;
337
+ if (!ctrl) return;
338
+ request(tokenRequest(clientId, o.endpoints), 'token refresh', ctrl.signal)
339
+ .then((r) => parseToken(r.status, r.body))
340
+ .then(
341
+ guard((token) => {
342
+ if (!closed && attempt === ctrl) apply(session.refresh(token, now()));
343
+ }),
344
+ // Kick ends the connection when the old token runs out; the
345
+ // reconnect fetches a new one.
346
+ noop
347
+ );
348
+ }
349
+
350
+ /** Lets go of the socket, without waiting for it, and of what hangs on it. */
351
+ function detach() {
352
+ clearTimeout(timer);
353
+ clearTimeout(handshake);
354
+ timer = handshake = null;
355
+ if (attempt) {
356
+ attempt.abort();
357
+ attempt = null;
358
+ }
359
+ const s = ws;
360
+ ws = null;
361
+ if (s) {
362
+ s.onopen = s.onmessage = s.onclose = null;
363
+ // The `ws` package emits an error on closing a socket still
364
+ // connecting, and throws one that nobody listens to.
365
+ s.onerror = noop;
366
+ try {
367
+ s.close();
368
+ } catch {
369
+ // Closed already.
370
+ }
371
+ }
372
+ }
373
+
374
+ function release() {
375
+ if (session) session.free();
376
+ if (backoff) backoff.free();
377
+ session = backoff = null;
378
+ }
379
+
380
+ /**
381
+ * A connection, or an attempt at one, ended: wait, then connect again.
382
+ * `retryAfterMs` is the least wait the end asked for (0: the ordinary
383
+ * backoff); `terminal`, that Kick will not take this client back.
384
+ */
385
+ function drop(reason, code, retryAfterMs, terminal) {
386
+ if (closed) return;
387
+ const waitMs = Math.min(backoff.nextDelayFor(retryAfterMs, Math.random()), MAX_TIMEOUT_MS);
388
+ callback('onDrop', reason, { code, terminal, waitMs });
148
389
  if (closed) return;
149
390
  clearTimeout(retry);
150
- retry = setTimeout(connect, backoff.nextDelay());
391
+ // connect() makes a new client id (so a new guest token) every time,
392
+ // which a terminal close needs; after any other end it does no harm.
393
+ retry = setTimeout(start, waitMs);
394
+ }
395
+
396
+ /** The socket is finished, however it ended. */
397
+ function end(reason, code, retryAfterMs, terminal) {
398
+ detach();
399
+ if (closed) return;
400
+ session.closed();
401
+ state();
402
+ drop(reason, code, retryAfterMs, terminal);
403
+ }
404
+
405
+ function start() {
406
+ retry = null;
407
+ connect().catch((e) => {
408
+ report(e);
409
+ if (!closed) end(`kicksdk: ${message(e)}`, 0, 0, false);
410
+ });
151
411
  }
152
412
 
153
413
  async function connect() {
154
414
  if (closed) return;
155
- // A guest identity: the token is issued to this id, and so is every
156
- // refreshed one for this connection.
157
- clientId = crypto.randomUUID();
415
+ const ctrl = new AbortController();
416
+ attempt = ctrl;
417
+ const mine = () => !closed && attempt === ctrl;
158
418
  let url;
159
419
  let token;
160
420
  try {
161
- const where = o.channelId
421
+ // A guest identity: the token is issued to this id, and so is every
422
+ // refreshed one for this connection.
423
+ clientId = guestId();
424
+ const where = hasChannel
162
425
  ? chatConnectionRequest(o.channelId, clientId, o.endpoints)
163
426
  : connectionRequest(clientId, o.endpoints);
164
- const c = await send(fetchImpl, where);
427
+ const c = await request(where, 'connection', ctrl.signal);
428
+ if (!mine()) return;
165
429
  const found = parseConnection(c.status, c.body).find((x) => x.provider === 'centrifugo');
166
430
  if (!found) throw new Error('Kick offered no Centrifugo server');
167
431
  url = found.url;
168
- const t = await send(fetchImpl, tokenRequest(clientId, o.endpoints));
432
+ const t = await request(tokenRequest(clientId, o.endpoints), 'token', ctrl.signal);
433
+ if (!mine()) return;
169
434
  token = parseToken(t.status, t.body);
170
435
  } catch (e) {
171
- if (o.onDrop) o.onDrop(String((e && e.message) || e));
172
- again();
436
+ // Closed meanwhile: nobody is left to tell.
437
+ if (!mine()) return;
438
+ attempt = null;
439
+ drop(message(e), 0, 0, false);
173
440
  return;
174
441
  }
175
- if (closed) return;
176
442
 
177
- if (!session) session = new Session(channels);
178
443
  let socket;
179
444
  try {
180
445
  socket = createSocket(url);
181
446
  } catch (e) {
182
- again();
447
+ attempt = null;
448
+ drop(`could not open a WebSocket: ${message(e)}`, 0, 0, false);
183
449
  return;
184
450
  }
185
451
  ws = socket;
186
- socket.onopen = () => apply(session.open(token, now()));
187
- socket.onmessage = (e) => {
452
+ handshake = setTimeout(
453
+ guard(() => {
454
+ handshake = null;
455
+ if (ws === socket) end(`WebSocket handshake: no answer in ${timeoutMs} ms`, 0, 0, false);
456
+ }),
457
+ timeoutMs
458
+ );
459
+ socket.onopen = guard(() => {
460
+ if (ws !== socket) return;
461
+ clearTimeout(handshake);
462
+ handshake = null;
463
+ apply(session.open(token, now()));
464
+ });
465
+ socket.onmessage = guard((e) => {
188
466
  if (ws === socket) apply(session.receive(String(e.data), now()));
189
- };
190
- socket.onclose = () => {
467
+ });
468
+ socket.onclose = guard((e) => {
191
469
  if (ws !== socket) return;
192
- ws = null;
193
- clearTimeout(timer);
194
- apply(session.closed());
195
- state();
196
- again();
197
- };
198
- socket.onerror = () => {};
470
+ // A refused handshake ends here too, with no open before it. A close
471
+ // without a code a peer can send (1000-4999) is 1006, abnormal
472
+ // closure: Node 22's WebSocket reports a refused handshake as 0.
473
+ const code = e && Number.isInteger(e.code) && e.code >= 1000 && e.code <= 4999 ? e.code : 1006;
474
+ const why = e && e.reason ? `: ${e.reason}` : '';
475
+ // 3500-3999 and 4500-4999 are terminal: a long wait, then a new client.
476
+ const after = retryAfterMs(code);
477
+ end(`WebSocket closed (${code})${why}`, code, after, after > 0);
478
+ });
479
+ // After the handshake a close event follows an error. A refused
480
+ // handshake may end with the error alone (Node 22's WebSocket fires no
481
+ // close then), so an error before the open ends the socket here, as a
482
+ // close would have, rather than at the handshake timeout.
483
+ socket.onerror = guard(() => {
484
+ if (ws === socket && handshake !== null) end('WebSocket handshake failed (1006)', 1006, 0, false);
485
+ });
199
486
  }
200
487
 
201
- connect();
488
+ retry = setTimeout(start, 0);
202
489
  return {
203
490
  close() {
491
+ if (closed) return;
204
492
  closed = true;
205
- clearTimeout(timer);
206
493
  clearTimeout(retry);
207
- if (ws) {
208
- const s = ws;
209
- ws = null;
210
- s.onclose = null;
211
- s.close();
212
- }
213
- if (session) {
214
- session.free();
215
- session = null;
216
- }
217
- backoff.free();
494
+ retry = null;
495
+ detach();
496
+ release();
218
497
  }
219
498
  };
220
499
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kicksdk-js",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Kick (kick.com) without the I/O: requests, parsers, the realtime chat protocol and webhook verification, from one Rust core",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/AnisAbdellatif/kicksdk.git", "directory": "bindings/js" },
@@ -12,11 +12,14 @@
12
12
  ".": { "types": "./index.d.ts", "default": "./index.js" },
13
13
  "./kicksdk_bg.wasm": "./pkg/kicksdk_bg.wasm"
14
14
  },
15
- "files": ["index.js", "index.d.ts", "pkg/", "README.md"],
15
+ "files": ["index.js", "index.d.ts", "pkg/", "README.md", "LICENSE-MIT", "LICENSE-APACHE"],
16
16
  "engines": { "node": ">=22" },
17
17
  "sideEffects": false,
18
18
  "scripts": {
19
19
  "build": "../../scripts/build-js.sh",
20
- "test": "node --test test/*.test.js"
20
+ "prepack": "node -e \"for (const f of ['LICENSE-MIT', 'LICENSE-APACHE']) require('fs').copyFileSync('../../' + f, f)\"",
21
+ "postpack": "node -e \"for (const f of ['LICENSE-MIT', 'LICENSE-APACHE']) require('fs').rmSync(f, { force: true })\"",
22
+ "test": "node --test test/*.test.js",
23
+ "typecheck": "tsc -p test/types/tsconfig.json"
21
24
  }
22
25
  }