@quizbase/client 0.1.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 Maciej Dzierżek
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,151 @@
1
+ # @quizbase/client
2
+
3
+ TypeScript SDK for the [QuizBase API](https://quizbase.runriva.com) — multilingual trivia API with 1.4M+ quiz-ready questions blended from 11 open-licensed sources, English and Polish at launch.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@quizbase/client.svg)](https://www.npmjs.com/package/@quizbase/client)
6
+ [![types](https://img.shields.io/npm/types/@quizbase/client.svg)](https://www.npmjs.com/package/@quizbase/client)
7
+ [![license](https://img.shields.io/npm/l/@quizbase/client.svg)](LICENSE)
8
+
9
+ > **0.x — API may change.** Generated from the live OpenAPI 3.1 spec, but the wrapper ergonomics may shift before 1.0. We aim to lock the surface ~6–8 weeks after public launch based on user feedback.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @quizbase/client
15
+ # or: npm install @quizbase/client
16
+ # or: yarn add @quizbase/client
17
+ ```
18
+
19
+ Requires Node ≥20 (or any modern browser with `fetch` + `AbortController`).
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ import { createClient } from '@quizbase/client';
25
+
26
+ const client = createClient({
27
+ apiKey: process.env.QUIZBASE_API_KEY! // qb_test_pk_… or qb_live_pk_…
28
+ });
29
+
30
+ const random = await client.questions.random({ category: 'science', lang: 'en' });
31
+ console.log(random.data.question);
32
+ ```
33
+
34
+ Get a key at [quizbase.runriva.com](https://quizbase.runriva.com). `qb_test_*` keys are free and unmetered for development.
35
+
36
+ ## Resources
37
+
38
+ ```ts
39
+ client.questions.list({ category, tags_any, topics_any, subcategory, lang, cursor, limit, ... });
40
+ client.questions.random({ category, tags, lang, ... });
41
+ client.questions.get(id, { lang });
42
+
43
+ client.categories.list({ lang });
44
+ client.languages.list();
45
+ client.topics.list({ lang });
46
+ client.topics.get(slug, { lang });
47
+ client.tags.list({ lang });
48
+ client.subcategories.list({ lang });
49
+
50
+ client.stats.get();
51
+ client.me.get();
52
+ client.usage.get({ from, to });
53
+
54
+ client.report.create({ questionId, kind, comment });
55
+ ```
56
+
57
+ Full parameter docs: [docs.quizbase.runriva.com/docs](https://quizbase.runriva.com/docs) and the interactive [API Reference](https://quizbase.runriva.com/docs/api-reference).
58
+
59
+ ## Errors
60
+
61
+ Every non-2xx response throws `QuizbaseError` carrying the parsed [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457):
62
+
63
+ ```ts
64
+ import { createClient, QuizbaseError } from '@quizbase/client';
65
+
66
+ try {
67
+ await client.questions.random({ category: 'unknown' });
68
+ } catch (err) {
69
+ if (err instanceof QuizbaseError) {
70
+ console.error(err.status); // 400
71
+ console.error(err.problem.code); // "invalid_query_param"
72
+ console.error(err.problem.detail); // human-readable message
73
+ console.error(err.requestId); // for support requests
74
+ if (err.isRateLimited) console.error(err.retryAfter); // seconds
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Retries
80
+
81
+ By default, the SDK retries 2× (3 attempts total) on `429` and `5xx` responses with exponential backoff + jitter. Server-issued `Retry-After` is honored. `4xx` (other than 429) is not retried.
82
+
83
+ ```ts
84
+ createClient({ apiKey, retries: 5 }); // tune
85
+ createClient({ apiKey, retries: 0 }); // disable
86
+ ```
87
+
88
+ ## Performance-aware timeouts
89
+
90
+ Per-endpoint defaults are tuned against [the public performance baseline](https://quizbase.runriva.com/docs/performance):
91
+
92
+ | Endpoint | Default timeout |
93
+ |-------------------------------------------|-----------------|
94
+ | `questions.list`, `topics.list`, `tags.list`, `subcategories.list`, `report.create` | 15 s |
95
+ | `questions.random`, `questions.get`, `categories.list`, `languages.list`, `topics.get`, `stats.get`, `me.get`, `usage.get` | 10 s |
96
+ | Global default | 30 s |
97
+
98
+ Override per-endpoint:
99
+
100
+ ```ts
101
+ createClient({
102
+ apiKey,
103
+ timeout: 30_000,
104
+ timeouts: {
105
+ 'questions.random': 5_000 // narrow filters → fail fast
106
+ }
107
+ });
108
+ ```
109
+
110
+ ## Telemetry hook
111
+
112
+ Wire every HTTP attempt (including retries) to your observability stack:
113
+
114
+ ```ts
115
+ import { posthog } from 'posthog-js';
116
+
117
+ const client = createClient({
118
+ apiKey,
119
+ onRequest: ({ method, endpoint, duration, status, requestId, retryCount, final, error }) => {
120
+ posthog.capture('quizbase_api_call', {
121
+ method, endpoint, duration, status, requestId, retryCount, final,
122
+ error: error?.message
123
+ });
124
+ }
125
+ });
126
+ ```
127
+
128
+ `final: false` means another retry will follow. The hook is async-safe; thrown errors inside it are swallowed so telemetry can never break the caller.
129
+
130
+ ## Custom `fetch`
131
+
132
+ Pass a `fetch` implementation for testing or to use `undici`/`node-fetch`:
133
+
134
+ ```ts
135
+ createClient({ apiKey, fetch: customFetch });
136
+ ```
137
+
138
+ ## Type-safe responses
139
+
140
+ All responses are typed from the OpenAPI spec. Hover over `client.questions.random(...)` in your IDE for autocomplete on every parameter and field.
141
+
142
+ ## Source & releases
143
+
144
+ - **GitHub:** [maciejdzierzek/quizbase-sdk-ts](https://github.com/maciejdzierzek/quizbase-sdk-ts)
145
+ - **Issues / feedback:** [GitHub Issues](https://github.com/maciejdzierzek/quizbase-sdk-ts/issues)
146
+ - **Releases:** automated by [release-please](https://github.com/googleapis/release-please) from conventional commits
147
+ - **Drift guard:** the main repo [`tests/api/sdk-contract.spec.ts`](https://github.com/maciejdzierzek/quizbase) verifies the latest published SDK still matches `/openapi.json`
148
+
149
+ ## License
150
+
151
+ MIT © Maciej Dzierżek
package/dist/index.cjs ADDED
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var QuizbaseError = class extends Error {
5
+ status;
6
+ problem;
7
+ requestId;
8
+ retryAfter;
9
+ url;
10
+ method;
11
+ constructor(opts) {
12
+ const title = opts.problem.title ?? `HTTP ${opts.status}`;
13
+ const detail = opts.problem.detail ? ` \u2014 ${opts.problem.detail}` : "";
14
+ super(`QuizBase ${opts.method} ${opts.url} \u2192 ${opts.status} ${title}${detail}`);
15
+ this.name = "QuizbaseError";
16
+ this.status = opts.status;
17
+ this.problem = opts.problem;
18
+ this.requestId = opts.requestId;
19
+ this.retryAfter = opts.retryAfter;
20
+ this.url = opts.url;
21
+ this.method = opts.method;
22
+ }
23
+ get type() {
24
+ return this.problem.type;
25
+ }
26
+ get isRateLimited() {
27
+ return this.status === 429;
28
+ }
29
+ get isAuthError() {
30
+ return this.status === 401 || this.status === 403;
31
+ }
32
+ get isServerError() {
33
+ return this.status >= 500;
34
+ }
35
+ };
36
+
37
+ // src/client.ts
38
+ var DEFAULT_TIMEOUTS = {
39
+ "questions.list": 15e3,
40
+ "questions.random": 1e4,
41
+ "questions.get": 1e4,
42
+ "categories.list": 1e4,
43
+ "languages.list": 1e4,
44
+ "topics.list": 15e3,
45
+ "topics.get": 1e4,
46
+ "tags.list": 15e3,
47
+ "subcategories.list": 15e3,
48
+ "stats.get": 1e4,
49
+ "me.get": 1e4,
50
+ "usage.get": 1e4,
51
+ "report.create": 15e3
52
+ };
53
+ var DEFAULT_BASE_URL = "https://quizbase.runriva.com";
54
+ var SDK_VERSION = "0.1.0";
55
+ function createClient(options) {
56
+ if (!options.apiKey) {
57
+ throw new Error("createClient: `apiKey` is required.");
58
+ }
59
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
60
+ const defaultTimeout = options.timeout ?? 3e4;
61
+ const timeouts = { ...DEFAULT_TIMEOUTS };
62
+ if (options.timeouts) {
63
+ for (const [key, value] of Object.entries(options.timeouts)) {
64
+ if (typeof value === "number") timeouts[key] = value;
65
+ }
66
+ }
67
+ const retries = Math.max(0, options.retries ?? 2);
68
+ const doFetch = options.fetch ?? globalThis.fetch;
69
+ if (typeof doFetch !== "function") {
70
+ throw new Error(
71
+ "createClient: global `fetch` is unavailable. Pass `fetch` option (Node \u226520 or polyfill)."
72
+ );
73
+ }
74
+ const userAgent = `quizbase-client/${SDK_VERSION}${options.userAgent ? ` ${options.userAgent}` : ""}`;
75
+ async function request(params) {
76
+ const timeoutMs = timeouts[params.endpoint] ?? defaultTimeout;
77
+ const url = buildUrl(baseUrl, params.path, params.query);
78
+ const requestId = generateRequestId();
79
+ const headers = {
80
+ Accept: "application/json",
81
+ Authorization: `Bearer ${options.apiKey}`,
82
+ "X-Request-Id": requestId,
83
+ "User-Agent": userAgent
84
+ };
85
+ let body;
86
+ if (params.body !== void 0) {
87
+ headers["Content-Type"] = "application/json";
88
+ body = JSON.stringify(params.body);
89
+ }
90
+ let attempt = 0;
91
+ let lastError;
92
+ while (attempt <= retries) {
93
+ const startedAt = Date.now();
94
+ const controller = new AbortController();
95
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
96
+ let status = 0;
97
+ let response;
98
+ let attemptError;
99
+ try {
100
+ response = await doFetch(url, {
101
+ method: params.method,
102
+ headers,
103
+ body,
104
+ signal: controller.signal
105
+ });
106
+ status = response.status;
107
+ if (response.ok) {
108
+ const data = await response.json();
109
+ await emit(options.onRequest, {
110
+ method: params.method,
111
+ endpoint: params.endpoint,
112
+ url,
113
+ duration: Date.now() - startedAt,
114
+ status,
115
+ requestId: response.headers.get("x-request-id") ?? requestId,
116
+ retryCount: attempt,
117
+ final: true
118
+ });
119
+ return data;
120
+ }
121
+ const problem = await safeProblem(response);
122
+ const retryAfterHeader = response.headers.get("retry-after");
123
+ const retryAfter = retryAfterHeader ? parseRetryAfter(retryAfterHeader) : null;
124
+ const err = new QuizbaseError({
125
+ status,
126
+ problem,
127
+ requestId: response.headers.get("x-request-id") ?? requestId,
128
+ retryAfter,
129
+ url,
130
+ method: params.method
131
+ });
132
+ attemptError = err;
133
+ if (shouldRetry(status) && attempt < retries) {
134
+ await emit(options.onRequest, {
135
+ method: params.method,
136
+ endpoint: params.endpoint,
137
+ url,
138
+ duration: Date.now() - startedAt,
139
+ status,
140
+ requestId: err.requestId,
141
+ retryCount: attempt,
142
+ final: false,
143
+ error: err
144
+ });
145
+ await sleep(backoffMs(attempt, retryAfter));
146
+ attempt += 1;
147
+ lastError = err;
148
+ continue;
149
+ }
150
+ await emit(options.onRequest, {
151
+ method: params.method,
152
+ endpoint: params.endpoint,
153
+ url,
154
+ duration: Date.now() - startedAt,
155
+ status,
156
+ requestId: err.requestId,
157
+ retryCount: attempt,
158
+ final: true,
159
+ error: err
160
+ });
161
+ throw err;
162
+ } catch (err) {
163
+ if (err instanceof QuizbaseError) throw err;
164
+ const networkErr = err instanceof Error ? err : new Error(String(err));
165
+ attemptError = networkErr;
166
+ if (attempt < retries) {
167
+ await emit(options.onRequest, {
168
+ method: params.method,
169
+ endpoint: params.endpoint,
170
+ url,
171
+ duration: Date.now() - startedAt,
172
+ status,
173
+ requestId: null,
174
+ retryCount: attempt,
175
+ final: false,
176
+ error: networkErr
177
+ });
178
+ await sleep(backoffMs(attempt, null));
179
+ attempt += 1;
180
+ lastError = networkErr;
181
+ continue;
182
+ }
183
+ await emit(options.onRequest, {
184
+ method: params.method,
185
+ endpoint: params.endpoint,
186
+ url,
187
+ duration: Date.now() - startedAt,
188
+ status,
189
+ requestId: null,
190
+ retryCount: attempt,
191
+ final: true,
192
+ error: networkErr
193
+ });
194
+ throw networkErr;
195
+ } finally {
196
+ clearTimeout(timer);
197
+ }
198
+ }
199
+ throw lastError ?? new Error("quizbase-client: retry loop exhausted unexpectedly");
200
+ }
201
+ return {
202
+ questions: {
203
+ list: (params) => request({
204
+ endpoint: "questions.list",
205
+ method: "GET",
206
+ path: "/api/v1/questions",
207
+ query: params
208
+ }),
209
+ random: (params) => request({
210
+ endpoint: "questions.random",
211
+ method: "GET",
212
+ path: "/api/v1/questions/random",
213
+ query: params
214
+ }),
215
+ get: (id, params) => request({
216
+ endpoint: "questions.get",
217
+ method: "GET",
218
+ path: `/api/v1/questions/${encodeURIComponent(id)}`,
219
+ query: params
220
+ })
221
+ },
222
+ categories: {
223
+ list: (params) => request({
224
+ endpoint: "categories.list",
225
+ method: "GET",
226
+ path: "/api/v1/categories",
227
+ query: params
228
+ })
229
+ },
230
+ languages: {
231
+ list: () => request({
232
+ endpoint: "languages.list",
233
+ method: "GET",
234
+ path: "/api/v1/languages"
235
+ })
236
+ },
237
+ topics: {
238
+ list: (params) => request({
239
+ endpoint: "topics.list",
240
+ method: "GET",
241
+ path: "/api/v1/topics",
242
+ query: params
243
+ }),
244
+ get: (slug, params) => request({
245
+ endpoint: "topics.get",
246
+ method: "GET",
247
+ path: `/api/v1/topics/${encodeURIComponent(slug)}`,
248
+ query: params
249
+ })
250
+ },
251
+ tags: {
252
+ list: (params) => request({
253
+ endpoint: "tags.list",
254
+ method: "GET",
255
+ path: "/api/v1/tags",
256
+ query: params
257
+ })
258
+ },
259
+ subcategories: {
260
+ list: (params) => request({
261
+ endpoint: "subcategories.list",
262
+ method: "GET",
263
+ path: "/api/v1/subcategories",
264
+ query: params
265
+ })
266
+ },
267
+ stats: {
268
+ get: () => request({
269
+ endpoint: "stats.get",
270
+ method: "GET",
271
+ path: "/api/v1/stats"
272
+ })
273
+ },
274
+ me: {
275
+ get: () => request({
276
+ endpoint: "me.get",
277
+ method: "GET",
278
+ path: "/api/v1/me"
279
+ })
280
+ },
281
+ usage: {
282
+ get: (params) => request({
283
+ endpoint: "usage.get",
284
+ method: "GET",
285
+ path: "/api/v1/usage",
286
+ query: params
287
+ })
288
+ },
289
+ report: {
290
+ create: (body) => request({
291
+ endpoint: "report.create",
292
+ method: "POST",
293
+ path: "/api/v1/report",
294
+ body
295
+ })
296
+ }
297
+ };
298
+ }
299
+ function buildUrl(baseUrl, path, query) {
300
+ const url = new URL(baseUrl + path);
301
+ if (query) {
302
+ for (const [key, value] of Object.entries(query)) {
303
+ if (value === void 0 || value === null) continue;
304
+ if (Array.isArray(value)) {
305
+ for (const item of value) {
306
+ if (item !== void 0 && item !== null) {
307
+ url.searchParams.append(key, String(item));
308
+ }
309
+ }
310
+ } else {
311
+ url.searchParams.append(key, String(value));
312
+ }
313
+ }
314
+ }
315
+ return url.toString();
316
+ }
317
+ function generateRequestId() {
318
+ const cryptoApi = globalThis.crypto;
319
+ if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
320
+ return cryptoApi.randomUUID();
321
+ }
322
+ return `req-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
323
+ }
324
+ async function safeProblem(response) {
325
+ try {
326
+ const data = await response.json();
327
+ if (data && typeof data === "object") return data;
328
+ } catch {
329
+ }
330
+ return {
331
+ type: "about:blank",
332
+ title: response.statusText || `HTTP ${response.status}`,
333
+ status: response.status,
334
+ detail: "",
335
+ instance: "",
336
+ code: "unknown"
337
+ };
338
+ }
339
+ function shouldRetry(status) {
340
+ return status === 429 || status >= 500 && status < 600;
341
+ }
342
+ function parseRetryAfter(value) {
343
+ const seconds = Number.parseInt(value, 10);
344
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds;
345
+ const date = Date.parse(value);
346
+ if (Number.isFinite(date)) {
347
+ const diff = Math.max(0, Math.ceil((date - Date.now()) / 1e3));
348
+ return diff;
349
+ }
350
+ return null;
351
+ }
352
+ function backoffMs(attempt, retryAfterSeconds) {
353
+ if (retryAfterSeconds !== null) return retryAfterSeconds * 1e3;
354
+ const base = 250;
355
+ const exp = base * 2 ** attempt;
356
+ const jitter = Math.random() * 100;
357
+ return Math.min(exp + jitter, 5e3);
358
+ }
359
+ function sleep(ms) {
360
+ return new Promise((resolve) => setTimeout(resolve, ms));
361
+ }
362
+ async function emit(hook, event) {
363
+ if (!hook) return;
364
+ try {
365
+ await hook(event);
366
+ } catch {
367
+ }
368
+ }
369
+
370
+ exports.QuizbaseError = QuizbaseError;
371
+ exports.createClient = createClient;
372
+ //# sourceMappingURL=index.cjs.map
373
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { components } from './types.gen.js';\n\nexport type ProblemDetails = components['schemas']['ProblemDetails'];\n\nexport interface QuizbaseErrorOptions {\n\tstatus: number;\n\tproblem: ProblemDetails;\n\trequestId: string | null;\n\tretryAfter: number | null;\n\turl: string;\n\tmethod: string;\n}\n\n/**\n * Thrown for any non-2xx response from the QuizBase API.\n * Carries the parsed RFC 9457 Problem Details body, X-Request-Id for support,\n * and a parsed retry-after (seconds) for 429s.\n */\nexport class QuizbaseError extends Error {\n\treadonly status: number;\n\treadonly problem: ProblemDetails;\n\treadonly requestId: string | null;\n\treadonly retryAfter: number | null;\n\treadonly url: string;\n\treadonly method: string;\n\n\tconstructor(opts: QuizbaseErrorOptions) {\n\t\tconst title = opts.problem.title ?? `HTTP ${opts.status}`;\n\t\tconst detail = opts.problem.detail ? ` — ${opts.problem.detail}` : '';\n\t\tsuper(`QuizBase ${opts.method} ${opts.url} → ${opts.status} ${title}${detail}`);\n\t\tthis.name = 'QuizbaseError';\n\t\tthis.status = opts.status;\n\t\tthis.problem = opts.problem;\n\t\tthis.requestId = opts.requestId;\n\t\tthis.retryAfter = opts.retryAfter;\n\t\tthis.url = opts.url;\n\t\tthis.method = opts.method;\n\t}\n\n\tget type(): string | undefined {\n\t\treturn this.problem.type;\n\t}\n\n\tget isRateLimited(): boolean {\n\t\treturn this.status === 429;\n\t}\n\n\tget isAuthError(): boolean {\n\t\treturn this.status === 401 || this.status === 403;\n\t}\n\n\tget isServerError(): boolean {\n\t\treturn this.status >= 500;\n\t}\n}\n","import { QuizbaseError, type ProblemDetails } from './errors.js';\nimport type { OnRequestHook } from './telemetry.js';\nimport type { components, paths } from './types.gen.js';\n\ntype Schemas = components['schemas'];\n\n/** Logical endpoint identifiers — stable across path-param values, used for telemetry + per-endpoint timeouts. */\nexport type EndpointKey =\n\t| 'questions.list'\n\t| 'questions.random'\n\t| 'questions.get'\n\t| 'categories.list'\n\t| 'languages.list'\n\t| 'topics.list'\n\t| 'topics.get'\n\t| 'tags.list'\n\t| 'subcategories.list'\n\t| 'stats.get'\n\t| 'me.get'\n\t| 'usage.get'\n\t| 'report.create';\n\nconst DEFAULT_TIMEOUTS: Record<EndpointKey, number> = {\n\t'questions.list': 15_000,\n\t'questions.random': 10_000,\n\t'questions.get': 10_000,\n\t'categories.list': 10_000,\n\t'languages.list': 10_000,\n\t'topics.list': 15_000,\n\t'topics.get': 10_000,\n\t'tags.list': 15_000,\n\t'subcategories.list': 15_000,\n\t'stats.get': 10_000,\n\t'me.get': 10_000,\n\t'usage.get': 10_000,\n\t'report.create': 15_000\n};\n\nexport interface ClientOptions {\n\t/** API key. Use `qb_test_pk_*` / `qb_test_sk_*` for development (free, unmetered) and `qb_live_*` in production. */\n\tapiKey: string;\n\t/** Override base URL. Defaults to `https://quizbase.runriva.com`. */\n\tbaseUrl?: string;\n\t/** Default request timeout in ms. Defaults to 30_000. Per-endpoint overrides take precedence. */\n\ttimeout?: number;\n\t/** Per-endpoint timeout overrides keyed by `EndpointKey`. */\n\ttimeouts?: Partial<Record<EndpointKey, number>>;\n\t/** Number of retries for 429 / 5xx / network errors. Defaults to 2 (3 total attempts). */\n\tretries?: number;\n\t/** Optional `fetch` implementation. Defaults to global `fetch`. */\n\tfetch?: typeof fetch;\n\t/** Telemetry hook fired after every HTTP attempt (including retries). */\n\tonRequest?: OnRequestHook;\n\t/** User-Agent suffix appended to the SDK identifier. */\n\tuserAgent?: string;\n}\n\nexport interface QuizbaseClient {\n\tquestions: {\n\t\tlist(\n\t\t\tparams?: paths['/api/v1/questions']['get']['parameters']['query']\n\t\t): Promise<Schemas['QuestionsListResponse']>;\n\t\trandom(\n\t\t\tparams?: paths['/api/v1/questions/random']['get']['parameters']['query']\n\t\t): Promise<Schemas['QuestionsRandomResponse']>;\n\t\tget(\n\t\t\tid: string,\n\t\t\tparams?: paths['/api/v1/questions/{id}']['get']['parameters']['query']\n\t\t): Promise<Schemas['QuestionByIdResponse']>;\n\t};\n\tcategories: {\n\t\tlist(\n\t\t\tparams?: paths['/api/v1/categories']['get']['parameters']['query']\n\t\t): Promise<Schemas['CategoriesResponse']>;\n\t};\n\tlanguages: {\n\t\tlist(): Promise<Schemas['LanguagesResponse']>;\n\t};\n\ttopics: {\n\t\tlist(\n\t\t\tparams?: paths['/api/v1/topics']['get']['parameters']['query']\n\t\t): Promise<Schemas['TopicsListResponse']>;\n\t\tget(\n\t\t\tslug: string,\n\t\t\tparams?: paths['/api/v1/topics/{slug}']['get']['parameters']['query']\n\t\t): Promise<Schemas['TopicDetailResponse']>;\n\t};\n\ttags: {\n\t\tlist(\n\t\t\tparams?: paths['/api/v1/tags']['get']['parameters']['query']\n\t\t): Promise<Schemas['TagsListResponse']>;\n\t};\n\tsubcategories: {\n\t\tlist(\n\t\t\tparams?: paths['/api/v1/subcategories']['get']['parameters']['query']\n\t\t): Promise<Schemas['SubcategoriesListResponse']>;\n\t};\n\tstats: {\n\t\tget(): Promise<Schemas['StatsResponse']>;\n\t};\n\tme: {\n\t\tget(): Promise<Schemas['MeResponse']>;\n\t};\n\tusage: {\n\t\tget(\n\t\t\tparams?: paths['/api/v1/usage']['get']['parameters']['query']\n\t\t): Promise<Schemas['UsageResponse']>;\n\t};\n\treport: {\n\t\tcreate(\n\t\t\tbody: NonNullable<\n\t\t\t\tpaths['/api/v1/report']['post']['requestBody']\n\t\t\t>['content']['application/json']\n\t\t): Promise<Schemas['ReportAcceptedResponse']>;\n\t};\n}\n\nconst DEFAULT_BASE_URL = 'https://quizbase.runriva.com';\nconst SDK_VERSION = '0.1.0';\n\ninterface RequestParams {\n\tendpoint: EndpointKey;\n\tmethod: 'GET' | 'POST';\n\tpath: string;\n\tquery?: Record<string, unknown> | undefined;\n\tbody?: unknown;\n}\n\nexport function createClient(options: ClientOptions): QuizbaseClient {\n\tif (!options.apiKey) {\n\t\tthrow new Error('createClient: `apiKey` is required.');\n\t}\n\tconst baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n\tconst defaultTimeout = options.timeout ?? 30_000;\n\tconst timeouts: Record<EndpointKey, number> = { ...DEFAULT_TIMEOUTS };\n\tif (options.timeouts) {\n\t\tfor (const [key, value] of Object.entries(options.timeouts) as [\n\t\t\tEndpointKey,\n\t\t\tnumber | undefined\n\t\t][]) {\n\t\t\tif (typeof value === 'number') timeouts[key] = value;\n\t\t}\n\t}\n\tconst retries = Math.max(0, options.retries ?? 2);\n\tconst doFetch = options.fetch ?? globalThis.fetch;\n\tif (typeof doFetch !== 'function') {\n\t\tthrow new Error(\n\t\t\t'createClient: global `fetch` is unavailable. Pass `fetch` option (Node ≥20 or polyfill).'\n\t\t);\n\t}\n\tconst userAgent = `quizbase-client/${SDK_VERSION}${options.userAgent ? ` ${options.userAgent}` : ''}`;\n\n\tasync function request<T>(params: RequestParams): Promise<T> {\n\t\tconst timeoutMs = timeouts[params.endpoint] ?? defaultTimeout;\n\t\tconst url = buildUrl(baseUrl, params.path, params.query);\n\t\tconst requestId = generateRequestId();\n\t\tconst headers: Record<string, string> = {\n\t\t\tAccept: 'application/json',\n\t\t\tAuthorization: `Bearer ${options.apiKey}`,\n\t\t\t'X-Request-Id': requestId,\n\t\t\t'User-Agent': userAgent\n\t\t};\n\t\tlet body: string | undefined;\n\t\tif (params.body !== undefined) {\n\t\t\theaders['Content-Type'] = 'application/json';\n\t\t\tbody = JSON.stringify(params.body);\n\t\t}\n\n\t\tlet attempt = 0;\n\t\tlet lastError: unknown;\n\t\twhile (attempt <= retries) {\n\t\t\tconst startedAt = Date.now();\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\t\t\tlet status = 0;\n\t\t\tlet response: Response | undefined;\n\t\t\tlet attemptError: Error | undefined;\n\t\t\ttry {\n\t\t\t\tresponse = await doFetch(url, {\n\t\t\t\t\tmethod: params.method,\n\t\t\t\t\theaders,\n\t\t\t\t\tbody,\n\t\t\t\t\tsignal: controller.signal\n\t\t\t\t});\n\t\t\t\tstatus = response.status;\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = (await response.json()) as T;\n\t\t\t\t\tawait emit(options.onRequest, {\n\t\t\t\t\t\tmethod: params.method,\n\t\t\t\t\t\tendpoint: params.endpoint,\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tduration: Date.now() - startedAt,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\trequestId: response.headers.get('x-request-id') ?? requestId,\n\t\t\t\t\t\tretryCount: attempt,\n\t\t\t\t\t\tfinal: true\n\t\t\t\t\t});\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\t\t\t\tconst problem = await safeProblem(response);\n\t\t\t\tconst retryAfterHeader = response.headers.get('retry-after');\n\t\t\t\tconst retryAfter = retryAfterHeader ? parseRetryAfter(retryAfterHeader) : null;\n\t\t\t\tconst err = new QuizbaseError({\n\t\t\t\t\tstatus,\n\t\t\t\t\tproblem,\n\t\t\t\t\trequestId: response.headers.get('x-request-id') ?? requestId,\n\t\t\t\t\tretryAfter,\n\t\t\t\t\turl,\n\t\t\t\t\tmethod: params.method\n\t\t\t\t});\n\t\t\t\tattemptError = err;\n\t\t\t\tif (shouldRetry(status) && attempt < retries) {\n\t\t\t\t\tawait emit(options.onRequest, {\n\t\t\t\t\t\tmethod: params.method,\n\t\t\t\t\t\tendpoint: params.endpoint,\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tduration: Date.now() - startedAt,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\trequestId: err.requestId,\n\t\t\t\t\t\tretryCount: attempt,\n\t\t\t\t\t\tfinal: false,\n\t\t\t\t\t\terror: err\n\t\t\t\t\t});\n\t\t\t\t\tawait sleep(backoffMs(attempt, retryAfter));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tlastError = err;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tawait emit(options.onRequest, {\n\t\t\t\t\tmethod: params.method,\n\t\t\t\t\tendpoint: params.endpoint,\n\t\t\t\t\turl,\n\t\t\t\t\tduration: Date.now() - startedAt,\n\t\t\t\t\tstatus,\n\t\t\t\t\trequestId: err.requestId,\n\t\t\t\t\tretryCount: attempt,\n\t\t\t\t\tfinal: true,\n\t\t\t\t\terror: err\n\t\t\t\t});\n\t\t\t\tthrow err;\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof QuizbaseError) throw err;\n\t\t\t\tconst networkErr = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tattemptError = networkErr;\n\t\t\t\tif (attempt < retries) {\n\t\t\t\t\tawait emit(options.onRequest, {\n\t\t\t\t\t\tmethod: params.method,\n\t\t\t\t\t\tendpoint: params.endpoint,\n\t\t\t\t\t\turl,\n\t\t\t\t\t\tduration: Date.now() - startedAt,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\trequestId: null,\n\t\t\t\t\t\tretryCount: attempt,\n\t\t\t\t\t\tfinal: false,\n\t\t\t\t\t\terror: networkErr\n\t\t\t\t\t});\n\t\t\t\t\tawait sleep(backoffMs(attempt, null));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tlastError = networkErr;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tawait emit(options.onRequest, {\n\t\t\t\t\tmethod: params.method,\n\t\t\t\t\tendpoint: params.endpoint,\n\t\t\t\t\turl,\n\t\t\t\t\tduration: Date.now() - startedAt,\n\t\t\t\t\tstatus,\n\t\t\t\t\trequestId: null,\n\t\t\t\t\tretryCount: attempt,\n\t\t\t\t\tfinal: true,\n\t\t\t\t\terror: networkErr\n\t\t\t\t});\n\t\t\t\tthrow networkErr;\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tvoid attemptError;\n\t\t\t\tvoid response;\n\t\t\t}\n\t\t}\n\t\tthrow lastError ?? new Error('quizbase-client: retry loop exhausted unexpectedly');\n\t}\n\n\treturn {\n\t\tquestions: {\n\t\t\tlist: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'questions.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/questions',\n\t\t\t\t\tquery: params\n\t\t\t\t}),\n\t\t\trandom: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'questions.random',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/questions/random',\n\t\t\t\t\tquery: params\n\t\t\t\t}),\n\t\t\tget: (id, params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'questions.get',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: `/api/v1/questions/${encodeURIComponent(id)}`,\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\tcategories: {\n\t\t\tlist: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'categories.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/categories',\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\tlanguages: {\n\t\t\tlist: () =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'languages.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/languages'\n\t\t\t\t})\n\t\t},\n\t\ttopics: {\n\t\t\tlist: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'topics.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/topics',\n\t\t\t\t\tquery: params\n\t\t\t\t}),\n\t\t\tget: (slug, params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'topics.get',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: `/api/v1/topics/${encodeURIComponent(slug)}`,\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\ttags: {\n\t\t\tlist: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'tags.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/tags',\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\tsubcategories: {\n\t\t\tlist: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'subcategories.list',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/subcategories',\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\tstats: {\n\t\t\tget: () =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'stats.get',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/stats'\n\t\t\t\t})\n\t\t},\n\t\tme: {\n\t\t\tget: () =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'me.get',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/me'\n\t\t\t\t})\n\t\t},\n\t\tusage: {\n\t\t\tget: (params) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'usage.get',\n\t\t\t\t\tmethod: 'GET',\n\t\t\t\t\tpath: '/api/v1/usage',\n\t\t\t\t\tquery: params\n\t\t\t\t})\n\t\t},\n\t\treport: {\n\t\t\tcreate: (body) =>\n\t\t\t\trequest({\n\t\t\t\t\tendpoint: 'report.create',\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tpath: '/api/v1/report',\n\t\t\t\t\tbody\n\t\t\t\t})\n\t\t}\n\t};\n}\n\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, unknown> | undefined): string {\n\tconst url = new URL(baseUrl + path);\n\tif (query) {\n\t\tfor (const [key, value] of Object.entries(query)) {\n\t\t\tif (value === undefined || value === null) continue;\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\tfor (const item of value) {\n\t\t\t\t\tif (item !== undefined && item !== null) {\n\t\t\t\t\t\turl.searchParams.append(key, String(item));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\turl.searchParams.append(key, String(value));\n\t\t\t}\n\t\t}\n\t}\n\treturn url.toString();\n}\n\nfunction generateRequestId(): string {\n\tconst cryptoApi: Crypto | undefined = globalThis.crypto;\n\tif (cryptoApi && typeof cryptoApi.randomUUID === 'function') {\n\t\treturn cryptoApi.randomUUID();\n\t}\n\treturn `req-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;\n}\n\nasync function safeProblem(response: Response): Promise<ProblemDetails> {\n\ttry {\n\t\tconst data = (await response.json()) as ProblemDetails;\n\t\tif (data && typeof data === 'object') return data;\n\t} catch {\n\t\t// fall through\n\t}\n\treturn {\n\t\ttype: 'about:blank',\n\t\ttitle: response.statusText || `HTTP ${response.status}`,\n\t\tstatus: response.status,\n\t\tdetail: '',\n\t\tinstance: '',\n\t\tcode: 'unknown'\n\t} satisfies ProblemDetails;\n}\n\nfunction shouldRetry(status: number): boolean {\n\treturn status === 429 || (status >= 500 && status < 600);\n}\n\nfunction parseRetryAfter(value: string): number | null {\n\tconst seconds = Number.parseInt(value, 10);\n\tif (Number.isFinite(seconds) && seconds >= 0) return seconds;\n\tconst date = Date.parse(value);\n\tif (Number.isFinite(date)) {\n\t\tconst diff = Math.max(0, Math.ceil((date - Date.now()) / 1000));\n\t\treturn diff;\n\t}\n\treturn null;\n}\n\nfunction backoffMs(attempt: number, retryAfterSeconds: number | null): number {\n\tif (retryAfterSeconds !== null) return retryAfterSeconds * 1000;\n\tconst base = 250;\n\tconst exp = base * 2 ** attempt;\n\tconst jitter = Math.random() * 100;\n\treturn Math.min(exp + jitter, 5_000);\n}\n\nfunction sleep(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function emit(\n\thook: OnRequestHook | undefined,\n\tevent: Parameters<OnRequestHook>[0]\n): Promise<void> {\n\tif (!hook) return;\n\ttry {\n\t\tawait hook(event);\n\t} catch {\n\t\t// telemetry must never break the caller\n\t}\n}\n"]}