kicksdk-js 0.2.0 → 0.3.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/README.md +79 -3
- package/index.d.ts +13 -2
- package/index.js +51 -6
- package/package.json +1 -1
- package/pkg/kicksdk.d.ts +541 -11
- package/pkg/kicksdk.js +1488 -229
- package/pkg/kicksdk_bg.wasm +0 -0
- package/pkg/kicksdk_bg.wasm.d.ts +49 -1
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@ limit: `terminal: true`) it never gives up, but waits long - 5, 10, 20, then
|
|
|
36
36
|
comes back as a new guest client (a new client id, so a new token). The chat
|
|
37
37
|
feed subscribing again ends the streak. `docs/KICK.md` §6.1 has the rules.
|
|
38
38
|
A callback that throws is reported to `onError` (or `console.error`) without
|
|
39
|
-
disturbing the connection; none fires after `close()
|
|
39
|
+
disturbing the connection; none fires after `close()`, and what throws after
|
|
40
|
+
it goes to `console.error`. A panic in the WebAssembly is reported too, and
|
|
41
|
+
ends that connection; the next one starts on a new session.
|
|
40
42
|
|
|
41
43
|
A mistake in the options throws from `connectChat` at once: a bad id or
|
|
42
44
|
endpoint, a `timeoutMs` that is not a number of ms above 0 and at most
|
|
@@ -59,12 +61,72 @@ relay through a server.
|
|
|
59
61
|
Everything else - `channelRequest` / `parseChannel`, `historyRequest`,
|
|
60
62
|
`livestreamsRequests`, `verifyWebhook`, `parseEvent`, `parseContent`, … -
|
|
61
63
|
returns plain request objects for your `fetch` and parses what comes back.
|
|
62
|
-
Parsers throw an `Error` with a `status` when Kick answered with an error
|
|
64
|
+
Parsers throw an `Error` with a `status` when Kick answered with an error,
|
|
65
|
+
and an `excerpt`: the start of Kick's answer, which often says why
|
|
66
|
+
(`CHANNEL_IS_NOT_LIVE`). The message ends with it too.
|
|
63
67
|
Every number must be a JS number - a numeric string or a boolean throws a
|
|
64
68
|
`TypeError` rather than being converted - and ids, statuses, times and
|
|
65
69
|
counts whole ones in range (ids at most `Number.MAX_SAFE_INTEGER`), or a
|
|
66
70
|
`RangeError` is thrown rather than the number being rounded into another
|
|
67
|
-
id.
|
|
71
|
+
id. The same holds for results: one that would carry an integer above
|
|
72
|
+
`Number.MAX_SAFE_INTEGER` (an id Kick sent that large) throws rather than
|
|
73
|
+
coming back rounded.
|
|
74
|
+
|
|
75
|
+
**User tokens.** To act as a Kick user (send chat, moderate, edit their
|
|
76
|
+
channel, manage rewards), get a user token with OAuth's authorization code
|
|
77
|
+
flow and PKCE. The core draws no random numbers: `newPkce()` and `newState()`
|
|
78
|
+
take them from `crypto.getRandomValues`.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const pkce = kick.newPkce(); // { verifier, challenge }
|
|
82
|
+
const state = kick.newState();
|
|
83
|
+
// keep pkce.verifier and state (a session) until the user comes back
|
|
84
|
+
location = kick.oauthAuthorizeUrl(clientId, redirectUri, ['user:read', 'chat:write'], state, pkce.verifier);
|
|
85
|
+
|
|
86
|
+
// on the redirect: check ?state= is yours, then
|
|
87
|
+
const token = kick.oauthParseUserToken(...await call(
|
|
88
|
+
kick.oauthCodeRequest(clientId, clientSecret, redirectUri, code, pkce.verifier)));
|
|
89
|
+
// later: oauthRefreshRequest(clientId, clientSecret, token.refresh_token) -> a new pair
|
|
90
|
+
```
|
|
91
|
+
`oauthRevokeRequest(token, 'access_token' | 'refresh_token')` /
|
|
92
|
+
`oauthParseRevoke`, `oauthIntrospectRequest(token)` / `oauthParseIntrospect`
|
|
93
|
+
(`{active, client_id, exp, scopes, token_type}`), and `oauthScopes()`, the 13
|
|
94
|
+
scopes Kick documents, complete the set; `oauthPkce(bytes)` and
|
|
95
|
+
`oauthPkceFromVerifier(verifier)` make a pair from your own 32 to 96 random
|
|
96
|
+
bytes or a kept verifier. **The verifier, the client secret and both tokens
|
|
97
|
+
are secrets: never log them**, nor a request carrying them
|
|
98
|
+
(`oauthRevokeRequest` puts the token in its URL).
|
|
99
|
+
|
|
100
|
+
With the token (`call` being your `fetch` that returns `[status, text]`):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const sent = kick.chatParseSend(...await call(kick.chatSendRequest(token, { user: broadcasterUserId }, 'hi')));
|
|
104
|
+
kick.parseDone(...await call(kick.moderationTimeoutRequest(token, broadcasterUserId, userId, 10, 'spam')));
|
|
105
|
+
```
|
|
106
|
+
- chat: `chatSendRequest(token, {user} | 'bot', content, replyToMessageId?)` /
|
|
107
|
+
`chatParseSend`, `chatDeleteRequest(token, messageId)`;
|
|
108
|
+
- moderation: `moderationBanRequest(token, broadcasterUserId, userId, reason?)`,
|
|
109
|
+
`moderationTimeoutRequest(…, minutes, reason?)`, `moderationUnbanRequest`;
|
|
110
|
+
- the token's channel: `channelsUpdateRequest(token, categoryId?, customTags?, streamTitle?)`;
|
|
111
|
+
- rewards: `rewardsListRequest` / `rewardsParseRewards`,
|
|
112
|
+
`rewardsCreateRequest(token, fields)` and `rewardsUpdateRequest(token, id, fields)` /
|
|
113
|
+
`rewardsParseReward` (`fields` as Kick names them: `{title, cost, is_enabled, …}`),
|
|
114
|
+
`rewardsDeleteRequest`, `rewardsRedemptionsRequest(token, rewardId?, status?, cursor?)`
|
|
115
|
+
and `rewardsRedemptionsByIdsRequest(token, ids, cursor?)` / `rewardsParseRedemptions`,
|
|
116
|
+
`rewardsAcceptRequest` and `rewardsRejectRequest(token, ids)` / `rewardsParseFailures`;
|
|
117
|
+
- ads: `adsAdBreakRequest(token, crypto.randomUUID(), seconds)` / `adsParseAdBreak`,
|
|
118
|
+
`adsStatusRequest` / `adsParseStatus`, `adsEnrollRequest`;
|
|
119
|
+
- drops (an app token): `dropsClaimsRequest(token, {campaign_id, user_id, limit, cursor, …})` /
|
|
120
|
+
`dropsParseClaims`, `dropsUpdateClaimsRequest(token, [{claim_id, external_status}])`;
|
|
121
|
+
- reads, an app or a user token: `usersRequest(token, userIds?)` / `parseUsers`,
|
|
122
|
+
`categoriesRequest(token, names?, tags?, ids?, limit?, cursor?)` / `parseCategories`,
|
|
123
|
+
`livestreamStatsRequest` / `parseLivestreamStats`, `kicksLeaderboardRequest(token, top?)` /
|
|
124
|
+
`parseKicksLeaderboard`.
|
|
125
|
+
|
|
126
|
+
Calls whose answer carries nothing are read with `parseDone(status, body)`,
|
|
127
|
+
which throws with the `status` unless it is a 2xx. Option objects (`fields`,
|
|
128
|
+
the drops filter, the claim updates, `{user}`) refuse a key they do not know.
|
|
129
|
+
`index.d.ts` types every result (`UserToken`, `Reward`, `Page<Category>`, …).
|
|
68
130
|
|
|
69
131
|
**Webhooks.** Read Kick's key once and check each delivery against the raw
|
|
70
132
|
body (text or bytes), its headers (pairs, a fetch `Headers`, or an object),
|
|
@@ -83,3 +145,17 @@ the key): deduplicate on `headers.message_id`. See `docs/KICK.md` §4.3.
|
|
|
83
145
|
|
|
84
146
|
See the repository's `docs/KICK.md` for everything known about Kick, with
|
|
85
147
|
sources. MIT OR Apache-2.0. Not affiliated with Kick.
|
|
148
|
+
|
|
149
|
+
## Live API coverage
|
|
150
|
+
|
|
151
|
+
The shared core was checked against Kick on 2026-11-15: OAuth code exchange
|
|
152
|
+
with PKCE, refresh/revocation/introspection, app and user reads, chat
|
|
153
|
+
send/reply/delete, timeout/ban/unban and reward create/update/delete.
|
|
154
|
+
Bot send returned 404; channel editing was skipped because no original title
|
|
155
|
+
was available to restore. Ad breaks, enrollment, drops and redemption
|
|
156
|
+
accept/reject remain documentation-based. See the repository `docs/KICK.md`
|
|
157
|
+
and `fixtures/{public_api,id}/README.md` for evidence and limits.
|
|
158
|
+
|
|
159
|
+
Check granted OAuth scopes before protected calls: Kick can grant fewer than
|
|
160
|
+
requested. Never log tokens, client secrets, authorization codes, PKCE
|
|
161
|
+
verifiers or requests carrying them.
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
export * from './pkg/kicksdk.js';
|
|
2
|
-
import type { Endpoints } from './pkg/kicksdk.js';
|
|
2
|
+
import type { Endpoints, Pkce } from './pkg/kicksdk.js';
|
|
3
3
|
|
|
4
4
|
/** The Origin Kick's realtime server accepts. */
|
|
5
5
|
export const REQUIRED_ORIGIN: 'https://kick.com';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* A new PKCE pair from 32 bytes of `crypto.getRandomValues`, for
|
|
9
|
+
* `oauthAuthorizeUrl` and `oauthCodeRequest`. The verifier is a secret: keep
|
|
10
|
+
* it until the user comes back, never log it.
|
|
11
|
+
*/
|
|
12
|
+
export function newPkce(): Pkce;
|
|
13
|
+
|
|
14
|
+
/** A new OAuth `state`: 32 bytes of `crypto.getRandomValues`, base64url without padding. */
|
|
15
|
+
export function newState(): string;
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Loads the WebAssembly: a URL, a Response (or a promise of one), the .wasm
|
|
9
19
|
* bytes or a compiled module. Safe to call more than once; after a failure,
|
|
@@ -100,7 +110,8 @@ export interface ChatOptions {
|
|
|
100
110
|
onDrop?: ((reason: string, drop: ChatDrop) => void) | undefined;
|
|
101
111
|
/**
|
|
102
112
|
* A callback above threw, or `now` gave a reading it cannot use. Default:
|
|
103
|
-
* console.error. The connection carries on either way.
|
|
113
|
+
* console.error. The connection carries on either way. Not called after
|
|
114
|
+
* `close()`: what throws then goes to console.error.
|
|
104
115
|
*/
|
|
105
116
|
onError?: ((error: unknown) => void) | undefined;
|
|
106
117
|
/** The fetch to make Kick's requests with. Default: the global fetch. */
|
package/index.js
CHANGED
|
@@ -20,13 +20,35 @@ import initWasm, {
|
|
|
20
20
|
parseConnection,
|
|
21
21
|
tokenRequest,
|
|
22
22
|
parseToken,
|
|
23
|
-
retryAfterMs
|
|
23
|
+
retryAfterMs,
|
|
24
|
+
oauthPkce
|
|
24
25
|
} from './pkg/kicksdk.js';
|
|
25
26
|
|
|
26
27
|
export * from './pkg/kicksdk.js';
|
|
27
28
|
|
|
28
29
|
export const REQUIRED_ORIGIN = 'https://kick.com';
|
|
29
30
|
|
|
31
|
+
// The core draws no random numbers: these two take them from the platform's
|
|
32
|
+
// CSPRNG (crypto.getRandomValues, in browsers and Node alike).
|
|
33
|
+
|
|
34
|
+
const randomBytes = (n) => crypto.getRandomValues(new Uint8Array(n));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A new PKCE pair, from 32 random bytes. The verifier is a secret: keep it
|
|
38
|
+
* until the user comes back, never log it.
|
|
39
|
+
*/
|
|
40
|
+
export function newPkce() {
|
|
41
|
+
return oauthPkce(randomBytes(32));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A new OAuth `state`: 32 random bytes, base64url without padding. */
|
|
45
|
+
export function newState() {
|
|
46
|
+
return btoa(String.fromCharCode(...randomBytes(32)))
|
|
47
|
+
.replace(/\+/g, '-')
|
|
48
|
+
.replace(/\//g, '_')
|
|
49
|
+
.replace(/=+$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
let ready = null;
|
|
31
53
|
|
|
32
54
|
/**
|
|
@@ -149,7 +171,9 @@ function socketFactory(o) {
|
|
|
149
171
|
*
|
|
150
172
|
* Callbacks are isolated: one that throws is reported to `onError` (or
|
|
151
173
|
* console.error) and the connection carries on. None is called after
|
|
152
|
-
* close(), which may be called from inside one
|
|
174
|
+
* close(), which may be called from inside one; what throws after it goes
|
|
175
|
+
* to console.error. A panic in the WebAssembly ends the connection, and the
|
|
176
|
+
* reconnect starts on a new Session.
|
|
153
177
|
*
|
|
154
178
|
* @param {object} o ChatOptions, in index.d.ts
|
|
155
179
|
* @returns {{ close(): void }}
|
|
@@ -192,7 +216,8 @@ export function connectChat(o) {
|
|
|
192
216
|
let retry = null;
|
|
193
217
|
|
|
194
218
|
function report(error) {
|
|
195
|
-
|
|
219
|
+
// After close() no callback runs, onError included.
|
|
220
|
+
if (o.onError && !closed) {
|
|
196
221
|
try {
|
|
197
222
|
o.onError(error);
|
|
198
223
|
return;
|
|
@@ -371,9 +396,22 @@ export function connectChat(o) {
|
|
|
371
396
|
}
|
|
372
397
|
}
|
|
373
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Frees `x`, if any. After a panic in the WebAssembly (a trap) a Session
|
|
401
|
+
* stays borrowed and throws on every call, free included: it is let go
|
|
402
|
+
* all the same.
|
|
403
|
+
*/
|
|
404
|
+
function free(x) {
|
|
405
|
+
try {
|
|
406
|
+
x?.free();
|
|
407
|
+
} catch {
|
|
408
|
+
// Unusable; nothing more to do with it.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
374
412
|
function release() {
|
|
375
|
-
|
|
376
|
-
|
|
413
|
+
free(session);
|
|
414
|
+
free(backoff);
|
|
377
415
|
session = backoff = null;
|
|
378
416
|
}
|
|
379
417
|
|
|
@@ -397,7 +435,14 @@ export function connectChat(o) {
|
|
|
397
435
|
function end(reason, code, retryAfterMs, terminal) {
|
|
398
436
|
detach();
|
|
399
437
|
if (closed) return;
|
|
400
|
-
|
|
438
|
+
try {
|
|
439
|
+
session.closed();
|
|
440
|
+
} catch {
|
|
441
|
+
// A panic (a trap, reported where it happened) left the session
|
|
442
|
+
// unusable: start over with a new one, which the reconnect opens.
|
|
443
|
+
free(session);
|
|
444
|
+
session = new Session(channels);
|
|
445
|
+
}
|
|
401
446
|
state();
|
|
402
447
|
drop(reason, code, retryAfterMs, terminal);
|
|
403
448
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kicksdk-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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" },
|