@toktikhq/sdk-js 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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/http.js +117 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +1754 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/realtime.js +197 -0
- package/dist/realtime.js.map +1 -0
- package/dist/resources.js +208 -0
- package/dist/resources.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TokTik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# TokTik JavaScript SDK
|
|
2
|
+
|
|
3
|
+
Typed client for the TokTik Developer API — REST data-plane + realtime LIVE event stream.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i @toktikhq/sdk-js
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { TokTikClient } from "@toktikhq/sdk-js";
|
|
13
|
+
|
|
14
|
+
const client = new TokTikClient({ apiKey: process.env.TOKTIK_API_KEY! });
|
|
15
|
+
|
|
16
|
+
// REST — provenance-enveloped responses expose `{ data, provenance }`, unchanged.
|
|
17
|
+
const creators = await client.creators.list({ q: "cooking", limit: 10 });
|
|
18
|
+
console.log(creators.provenance.freshness, creators.data.results);
|
|
19
|
+
|
|
20
|
+
const board = await client.rankings.official({ board: "hourly", region: "VN" });
|
|
21
|
+
console.log(board.provenance.freshness, board.data.board.entries);
|
|
22
|
+
|
|
23
|
+
// Realtime — three lines to a live event stream.
|
|
24
|
+
const stream = await client.live.stream(["@some.creator"], {
|
|
25
|
+
onEvent: (frame) => console.log(frame.event, frame.data),
|
|
26
|
+
onStatus: (frame) => console.log(frame.creatorId, "→", frame.status)
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The client stays thin: it sends your bearer key only to the configured API origin and leaves REST
|
|
31
|
+
retries, caching, and key storage to the calling application. (The realtime stream is the one
|
|
32
|
+
exception — reconnection there is not optional, see below.)
|
|
33
|
+
|
|
34
|
+
## Provenance is part of the answer
|
|
35
|
+
|
|
36
|
+
Most data methods return `{ data, provenance }` and the SDK never strips the envelope. Much of this
|
|
37
|
+
data is **observed**, not officially published, so `freshness` (`near_realtime` / `stale` /
|
|
38
|
+
`historical`), `source` and `coverageStatus` tell you how much to trust a given answer.
|
|
39
|
+
|
|
40
|
+
A few shapes deliberately differ, matching the server: **exports** return the job / job list directly,
|
|
41
|
+
**`account.usage()`** returns its own `{ period, balance, items }` shape, and **`exports.download()`**
|
|
42
|
+
returns the CSV as a **string**, not JSON. Everything else is enveloped.
|
|
43
|
+
|
|
44
|
+
## Realtime
|
|
45
|
+
|
|
46
|
+
`client.live.stream(creatorIds, handlers)` handles the parts that are easy to get wrong:
|
|
47
|
+
|
|
48
|
+
- **Token lifetime** — mints a short-lived handshake token per connection (via
|
|
49
|
+
`POST /v1/live/stream/token`), so an expiry mid-stream costs a reconnect, not a dead socket.
|
|
50
|
+
- **Credential transport** — the token travels in the `Sec-WebSocket-Protocol` subprotocol, never the
|
|
51
|
+
URL, where it would leak into access logs and browser history.
|
|
52
|
+
- **Reconnect** — exponential backoff with full jitter and a bounded ceiling, so a fleet does not
|
|
53
|
+
stampede a restarted gateway.
|
|
54
|
+
- **Resume** — the gateway keeps no per-connection memory, so the SDK re-subscribes everything after
|
|
55
|
+
a reconnect. Subscribe/unsubscribe issued while disconnected is replayed on the next open.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const stream = await client.live.stream(["creator.one"], {
|
|
59
|
+
onEvent: (f) => console.log(f.event, f.sequence, f.data),
|
|
60
|
+
onStatus: (f) => console.log(f.status, f.reason), // queued | active | offline | unavailable
|
|
61
|
+
onError: (e) => console.error(e),
|
|
62
|
+
onReconnect: () => console.log("resumed")
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
stream.subscribe("creator.two");
|
|
66
|
+
stream.unsubscribe("creator.one");
|
|
67
|
+
stream.close(); // final — stops reconnecting
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Statuses are honest: a creator whose room is still being admitted/acquired reports `queued`, not
|
|
71
|
+
`active`. `active` means events are actually flowing.
|
|
72
|
+
|
|
73
|
+
### Node
|
|
74
|
+
|
|
75
|
+
Node 22+ has a global `WebSocket`, so nothing extra is needed. On older runtimes pass a factory:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import WebSocket from "ws";
|
|
79
|
+
client.live.stream(ids, handlers, {
|
|
80
|
+
socketFactory: (url, token) => new WebSocket(url, ["bearer", token]) as any
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The two subprotocol values must be the literal `bearer` followed by the token — the gateway selects
|
|
85
|
+
and echoes the marker, and a client that offers subprotocols closes the socket if the server selects
|
|
86
|
+
none.
|
|
87
|
+
|
|
88
|
+
## Errors
|
|
89
|
+
|
|
90
|
+
Failures throw `TokTikApiError`, which keeps the status distinguishable rather than collapsing it:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
try {
|
|
94
|
+
await client.creators.get("someone");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof TokTikApiError) {
|
|
97
|
+
error.isUnauthorized; // 401 — bad or expired key
|
|
98
|
+
error.isForbidden; // 403 — key lacks the scope this resource is sold under
|
|
99
|
+
error.isPaymentRequired; // 402 — out of credits; retrying will not help
|
|
100
|
+
error.isRateLimited; // 429
|
|
101
|
+
error.retryable; // 429 + 5xx only
|
|
102
|
+
error.requestId; // quote this to support
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Coverage
|
|
108
|
+
|
|
109
|
+
| Namespace | Scope | Endpoints |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `client.live` | `live:read`, `live:stream` | sessions, session by id, creator performance, stream token, `stream()` |
|
|
112
|
+
| `client.rankings` | `rank:read` | `official({ board })`, `movers({ board })`, `history({ board })`, `regions()`, `games({ region })` |
|
|
113
|
+
| `client.creators` | `creator:read` | list, get, changes, analysis, following, followers |
|
|
114
|
+
| `client.content` | `content:read` | creator videos (no params), video, video comments |
|
|
115
|
+
| `client.gifters` | `gifter:read` | list, get, for creator |
|
|
116
|
+
| `client.trends` | `trend:read` | list |
|
|
117
|
+
| `client.exports` | `export` | list, create, get, download |
|
|
118
|
+
| `client.account` | `keys:manage` | entitlements, usage |
|
|
119
|
+
|
|
120
|
+
Option names mirror the server schemas exactly. Those routes declare `additionalProperties: false`,
|
|
121
|
+
so an invented parameter is a hard `400` — not an ignored extra.
|
|
122
|
+
|
|
123
|
+
The pre-D3.1 methods (`listLiveSessions`, `getLiveSession`, `listCreatorPerformance`) still work and
|
|
124
|
+
are marked deprecated; they forward to `client.live.*`.
|
|
125
|
+
|
|
126
|
+
## Publishing
|
|
127
|
+
|
|
128
|
+
The npm artifact is self-contained: `@v2/contracts` is a build-only workspace dependency and the
|
|
129
|
+
public declaration graph is bundled into `dist/index.d.ts`. `npm run test:package` imports the built
|
|
130
|
+
ESM entry point and type-checks a clean consumer whose only dependency is this SDK.
|
|
131
|
+
|
|
132
|
+
Releases are tag-driven through `.github/workflows/sdk-release.yml`:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git tag sdk-js-v0.2.0
|
|
136
|
+
git push origin sdk-js-v0.2.0
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The npm package must have a Trusted Publisher configured for this repository and that workflow. The
|
|
140
|
+
package remains ESM-only (`"type": "module"`); a dual ESM/CJS build is not currently provided.
|
|
141
|
+
|
|
142
|
+
## Known scope limits
|
|
143
|
+
|
|
144
|
+
- Alert-rule (`webhook:manage`) and API-key (`keys:manage`) *management* endpoints are sellable but
|
|
145
|
+
intentionally out of scope here; only `usage`/`entitlements` are exposed.
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A non-2xx answer from the API.
|
|
3
|
+
*
|
|
4
|
+
* The status is deliberately surfaced rather than folded into a generic failure: the four the API
|
|
5
|
+
* uses to say something actionable each need a different reaction from the caller, and swallowing
|
|
6
|
+
* them is the failure mode this SDK exists to prevent.
|
|
7
|
+
*
|
|
8
|
+
* - `401` bad/expired key · `403` the key lacks the scope this resource is sold under
|
|
9
|
+
* - `402` out of credits — buying more is the only fix; retrying will not help
|
|
10
|
+
* - `429` rate limited — retry after backing off
|
|
11
|
+
*/
|
|
12
|
+
export class TokTikApiError extends Error {
|
|
13
|
+
status;
|
|
14
|
+
code;
|
|
15
|
+
requestId;
|
|
16
|
+
constructor(status, code, requestId, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.requestId = requestId;
|
|
21
|
+
this.name = "TokTikApiError";
|
|
22
|
+
}
|
|
23
|
+
/** True when retrying the identical request could plausibly succeed. */
|
|
24
|
+
get retryable() {
|
|
25
|
+
return this.status === 429 || this.status >= 500;
|
|
26
|
+
}
|
|
27
|
+
/** Out of credits. Distinct from `403`: the key is valid and scoped, the wallet is empty. */
|
|
28
|
+
get isPaymentRequired() {
|
|
29
|
+
return this.status === 402;
|
|
30
|
+
}
|
|
31
|
+
/** The key is missing the scope this endpoint is sold under (or is not a data-plane key at all). */
|
|
32
|
+
get isForbidden() {
|
|
33
|
+
return this.status === 403;
|
|
34
|
+
}
|
|
35
|
+
get isUnauthorized() {
|
|
36
|
+
return this.status === 401;
|
|
37
|
+
}
|
|
38
|
+
get isRateLimited() {
|
|
39
|
+
return this.status === 429;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function defaultFetch(url, init) {
|
|
43
|
+
const fetchImpl = globalThis.fetch;
|
|
44
|
+
if (!fetchImpl)
|
|
45
|
+
throw new Error("No fetch implementation is available; pass TokTikClientOptions.fetch.");
|
|
46
|
+
return fetchImpl(url, init);
|
|
47
|
+
}
|
|
48
|
+
export function buildQuery(parameters = {}) {
|
|
49
|
+
const values = new URLSearchParams();
|
|
50
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
51
|
+
if (value === undefined || value === null || value === "")
|
|
52
|
+
continue;
|
|
53
|
+
// Repeatable filters (e.g. `?type=a&type=b`) must not collapse into "a,b".
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
for (const item of value)
|
|
56
|
+
values.append(key, String(item));
|
|
57
|
+
else
|
|
58
|
+
values.set(key, String(value));
|
|
59
|
+
}
|
|
60
|
+
return values.size ? `?${values}` : "";
|
|
61
|
+
}
|
|
62
|
+
function safeJson(raw) {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Shared request plumbing. Deliberately has no retries, caching, or credential storage. */
|
|
71
|
+
export class HttpTransport {
|
|
72
|
+
baseUrl;
|
|
73
|
+
fetchImpl;
|
|
74
|
+
apiKey;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.apiKey = options.apiKey.trim();
|
|
77
|
+
if (!this.apiKey)
|
|
78
|
+
throw new Error("apiKey is required");
|
|
79
|
+
this.baseUrl = (options.baseUrl ?? "https://api.toktikhq.com").replace(/\/$/u, "");
|
|
80
|
+
this.fetchImpl = options.fetch ?? defaultFetch;
|
|
81
|
+
}
|
|
82
|
+
get(path, parameters = {}) {
|
|
83
|
+
return this.request("GET", `${path}${buildQuery(parameters)}`);
|
|
84
|
+
}
|
|
85
|
+
post(path, body) {
|
|
86
|
+
return this.request("POST", path, body);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* For endpoints that answer with something other than JSON (today: the CSV export download).
|
|
90
|
+
* Errors are still parsed as JSON, because a failure is always the JSON error envelope.
|
|
91
|
+
*/
|
|
92
|
+
getText(path, parameters = {}) {
|
|
93
|
+
return this.request("GET", `${path}${buildQuery(parameters)}`, undefined, "text");
|
|
94
|
+
}
|
|
95
|
+
async request(method, path, body, responseKind = "json") {
|
|
96
|
+
const headers = {
|
|
97
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
98
|
+
accept: "application/json"
|
|
99
|
+
};
|
|
100
|
+
if (body !== undefined)
|
|
101
|
+
headers["content-type"] = "application/json";
|
|
102
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
103
|
+
method,
|
|
104
|
+
headers,
|
|
105
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {})
|
|
106
|
+
});
|
|
107
|
+
const raw = await response.text();
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const error = (raw ? safeJson(raw) : undefined);
|
|
110
|
+
throw new TokTikApiError(response.status, error?.code, error?.requestId, error?.message ?? `TokTik API request failed (${response.status}).`);
|
|
111
|
+
}
|
|
112
|
+
if (responseKind === "text")
|
|
113
|
+
return raw;
|
|
114
|
+
return (raw ? safeJson(raw) : undefined);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|