@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 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}