@buun_group/gunspec-sdk 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/dist/index.js ADDED
@@ -0,0 +1,2307 @@
1
+ // src/core/auth.ts
2
+ function readEnvKey() {
3
+ try {
4
+ if (typeof process !== "undefined" && process.env) {
5
+ return process.env.GUNSPEC_API_KEY;
6
+ }
7
+ } catch {
8
+ }
9
+ try {
10
+ const g = globalThis;
11
+ if (typeof g.Deno !== "undefined" && g.Deno.env) {
12
+ return g.Deno.env.get("GUNSPEC_API_KEY");
13
+ }
14
+ } catch {
15
+ }
16
+ return void 0;
17
+ }
18
+ function resolveApiKey(config) {
19
+ if (config.apiKey !== void 0 && config.apiKey !== "") {
20
+ return config.apiKey;
21
+ }
22
+ return readEnvKey();
23
+ }
24
+ function buildAuthHeaders(apiKey) {
25
+ const headers = {};
26
+ if (apiKey !== void 0 && apiKey !== "") {
27
+ headers["X-API-Key"] = apiKey;
28
+ }
29
+ return headers;
30
+ }
31
+
32
+ // src/core/errors.ts
33
+ var GunSpecError = class extends Error {
34
+ name = "GunSpecError";
35
+ constructor(message) {
36
+ super(message);
37
+ Object.setPrototypeOf(this, new.target.prototype);
38
+ }
39
+ };
40
+ var APIError = class extends GunSpecError {
41
+ name = "APIError";
42
+ /** HTTP status code returned by the API. */
43
+ status;
44
+ /** Machine-readable error code from the response body (e.g. `"NOT_FOUND"`). */
45
+ code;
46
+ /** The `X-Request-Id` header value, useful for support requests. */
47
+ requestId;
48
+ /** Raw response headers for further inspection. */
49
+ headers;
50
+ constructor(status, code, message, requestId, headers) {
51
+ super(message);
52
+ this.status = status;
53
+ this.code = code;
54
+ this.requestId = requestId;
55
+ this.headers = headers;
56
+ }
57
+ };
58
+ var AuthenticationError = class extends APIError {
59
+ name = "AuthenticationError";
60
+ constructor(code, message, requestId, headers) {
61
+ super(401, code, message, requestId, headers);
62
+ }
63
+ };
64
+ var PermissionError = class extends APIError {
65
+ name = "PermissionError";
66
+ constructor(code, message, requestId, headers) {
67
+ super(403, code, message, requestId, headers);
68
+ }
69
+ };
70
+ var NotFoundError = class extends APIError {
71
+ name = "NotFoundError";
72
+ constructor(code, message, requestId, headers) {
73
+ super(404, code, message, requestId, headers);
74
+ }
75
+ };
76
+ var BadRequestError = class extends APIError {
77
+ name = "BadRequestError";
78
+ constructor(code, message, requestId, headers) {
79
+ super(400, code, message, requestId, headers);
80
+ }
81
+ };
82
+ var RateLimitError = class extends APIError {
83
+ name = "RateLimitError";
84
+ /**
85
+ * Number of seconds the client should wait before retrying, or `null` if
86
+ * the server did not provide a `Retry-After` header.
87
+ */
88
+ retryAfter;
89
+ constructor(code, message, requestId, headers, retryAfter) {
90
+ super(429, code, message, requestId, headers);
91
+ this.retryAfter = retryAfter;
92
+ }
93
+ };
94
+ var InternalServerError = class extends APIError {
95
+ name = "InternalServerError";
96
+ constructor(code, message, requestId, headers) {
97
+ super(500, code, message, requestId, headers);
98
+ }
99
+ };
100
+ var ConnectionError = class extends GunSpecError {
101
+ name = "ConnectionError";
102
+ /** The original error thrown by `fetch`. */
103
+ cause;
104
+ constructor(message, cause) {
105
+ super(message);
106
+ this.cause = cause;
107
+ }
108
+ };
109
+ var TimeoutError = class extends GunSpecError {
110
+ name = "TimeoutError";
111
+ /** The timeout duration in milliseconds that was exceeded. */
112
+ timeoutMs;
113
+ constructor(timeoutMs) {
114
+ super(`Request timed out after ${timeoutMs}ms`);
115
+ this.timeoutMs = timeoutMs;
116
+ }
117
+ };
118
+ function parseRetryAfter(headers) {
119
+ const raw = headers.get("Retry-After");
120
+ if (raw === null) return null;
121
+ const seconds = Number(raw);
122
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds;
123
+ const date = Date.parse(raw);
124
+ if (!Number.isNaN(date)) {
125
+ const delta = Math.ceil((date - Date.now()) / 1e3);
126
+ return delta > 0 ? delta : 0;
127
+ }
128
+ return null;
129
+ }
130
+ function createAPIError(status, body, requestId, headers) {
131
+ const code = body?.error?.code ?? `HTTP_${status}`;
132
+ const message = body?.error?.message ?? `Request failed with status ${status}`;
133
+ switch (status) {
134
+ case 400:
135
+ return new BadRequestError(code, message, requestId, headers);
136
+ case 401:
137
+ return new AuthenticationError(code, message, requestId, headers);
138
+ case 403:
139
+ return new PermissionError(code, message, requestId, headers);
140
+ case 404:
141
+ return new NotFoundError(code, message, requestId, headers);
142
+ case 429:
143
+ return new RateLimitError(code, message, requestId, headers, parseRetryAfter(headers));
144
+ case 500:
145
+ return new InternalServerError(code, message, requestId, headers);
146
+ default:
147
+ return new APIError(status, code, message, requestId, headers);
148
+ }
149
+ }
150
+
151
+ // src/core/retry.ts
152
+ var DEFAULTS = {
153
+ maxRetries: 2,
154
+ initialDelayMs: 500,
155
+ maxDelayMs: 8e3,
156
+ multiplier: 2
157
+ };
158
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
159
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
160
+ function resolveRetryConfig(config) {
161
+ return {
162
+ maxRetries: config?.maxRetries ?? DEFAULTS.maxRetries,
163
+ initialDelayMs: config?.initialDelayMs ?? DEFAULTS.initialDelayMs,
164
+ maxDelayMs: config?.maxDelayMs ?? DEFAULTS.maxDelayMs,
165
+ multiplier: config?.multiplier ?? DEFAULTS.multiplier
166
+ };
167
+ }
168
+ function isRetryable(error, method) {
169
+ if (!IDEMPOTENT_METHODS.has(method.toUpperCase())) {
170
+ return false;
171
+ }
172
+ if (error instanceof ConnectionError || error instanceof TimeoutError) {
173
+ return true;
174
+ }
175
+ if (error instanceof APIError) {
176
+ return RETRYABLE_STATUS_CODES.has(error.status);
177
+ }
178
+ return false;
179
+ }
180
+ function computeDelay(attempt, config, error) {
181
+ const base = config.initialDelayMs * Math.pow(config.multiplier, attempt);
182
+ const jitter = 1 + (Math.random() * 0.4 - 0.2);
183
+ let delay = Math.min(base * jitter, config.maxDelayMs);
184
+ if (error instanceof RateLimitError && error.retryAfter !== null) {
185
+ const retryAfterMs = error.retryAfter * 1e3;
186
+ delay = Math.max(delay, retryAfterMs);
187
+ }
188
+ return Math.round(delay);
189
+ }
190
+ function sleep(ms, signal) {
191
+ return new Promise((resolve, reject) => {
192
+ if (signal?.aborted) {
193
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
194
+ return;
195
+ }
196
+ const timer = setTimeout(resolve, ms);
197
+ signal?.addEventListener(
198
+ "abort",
199
+ () => {
200
+ clearTimeout(timer);
201
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
202
+ },
203
+ { once: true }
204
+ );
205
+ });
206
+ }
207
+ async function withRetry(fn, method, config, signal) {
208
+ let lastError;
209
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
210
+ try {
211
+ return await fn();
212
+ } catch (error) {
213
+ lastError = error;
214
+ const isLastAttempt = attempt === config.maxRetries;
215
+ if (isLastAttempt || !isRetryable(error, method)) {
216
+ throw error;
217
+ }
218
+ const delay = computeDelay(attempt, config, error);
219
+ await sleep(delay, signal);
220
+ }
221
+ }
222
+ throw lastError;
223
+ }
224
+
225
+ // src/core/http-client.ts
226
+ var DEFAULT_BASE_URL = "https://api.gunspec.io";
227
+ var DEFAULT_TIMEOUT_MS = 3e4;
228
+ var SDK_USER_AGENT = "@gunspec/sdk";
229
+ function serialiseQuery(params) {
230
+ const parts = [];
231
+ for (const [key, value] of Object.entries(params)) {
232
+ if (value === void 0) continue;
233
+ if (Array.isArray(value)) {
234
+ for (const item of value) {
235
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
236
+ }
237
+ } else {
238
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
239
+ }
240
+ }
241
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
242
+ }
243
+ function parseRateLimitHeaders(headers) {
244
+ const parse = (name) => {
245
+ const raw = headers.get(name);
246
+ if (raw === null) return null;
247
+ const num = Number(raw);
248
+ return Number.isNaN(num) ? null : num;
249
+ };
250
+ return {
251
+ limit: parse("X-RateLimit-Limit"),
252
+ remaining: parse("X-RateLimit-Remaining"),
253
+ reset: parse("X-RateLimit-Reset")
254
+ };
255
+ }
256
+ function extractRequestId(headers) {
257
+ return headers.get("X-Request-Id") ?? "";
258
+ }
259
+ function composeSignals(timeoutMs, externalSignal) {
260
+ const timeoutController = new AbortController();
261
+ const timer = setTimeout(() => timeoutController.abort(new TimeoutError(timeoutMs)), timeoutMs);
262
+ if (typeof AbortSignal !== "undefined" && "any" in AbortSignal) {
263
+ const signals = [timeoutController.signal];
264
+ if (externalSignal) signals.push(externalSignal);
265
+ const composed2 = AbortSignal.any(signals);
266
+ return {
267
+ signal: composed2,
268
+ cleanup: () => clearTimeout(timer)
269
+ };
270
+ }
271
+ const composed = new AbortController();
272
+ const onTimeoutAbort = () => composed.abort(timeoutController.signal.reason);
273
+ const onExternalAbort = () => {
274
+ if (externalSignal) composed.abort(externalSignal.reason);
275
+ };
276
+ timeoutController.signal.addEventListener("abort", onTimeoutAbort, { once: true });
277
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
278
+ return {
279
+ signal: composed.signal,
280
+ cleanup: () => {
281
+ clearTimeout(timer);
282
+ timeoutController.signal.removeEventListener("abort", onTimeoutAbort);
283
+ externalSignal?.removeEventListener("abort", onExternalAbort);
284
+ }
285
+ };
286
+ }
287
+ var HttpClient = class {
288
+ baseUrl;
289
+ defaultTimeout;
290
+ defaultHeaders;
291
+ apiKey;
292
+ retryConfig;
293
+ constructor(config = {}) {
294
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
295
+ this.defaultTimeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
296
+ this.apiKey = resolveApiKey(config.auth ?? {});
297
+ this.retryConfig = resolveRetryConfig(config.retry);
298
+ this.defaultHeaders = {
299
+ "Accept": "application/json",
300
+ "User-Agent": SDK_USER_AGENT,
301
+ ...config.headers
302
+ };
303
+ }
304
+ /**
305
+ * Execute an HTTP request and return the unwrapped response.
306
+ *
307
+ * The API's `{ success: true, data: T }` envelope is stripped so that
308
+ * callers receive `T` directly via {@link APIResponse.data}.
309
+ *
310
+ * @typeParam T - The expected type of the `data` field in the response.
311
+ * @param config - Request configuration.
312
+ * @returns The unwrapped API response.
313
+ * @throws {@link APIError} on non-2xx responses.
314
+ * @throws {@link ConnectionError} on network failures.
315
+ * @throws {@link TimeoutError} when the request exceeds the timeout.
316
+ */
317
+ async request(config) {
318
+ return withRetry(
319
+ () => this.executeRequest(config),
320
+ config.method,
321
+ this.retryConfig,
322
+ config.signal
323
+ );
324
+ }
325
+ /**
326
+ * Execute an HTTP request and return a paginated response.
327
+ *
328
+ * The API's `{ success: true, data: T[], pagination }` envelope is
329
+ * unwrapped into a {@link PaginatedResponse}.
330
+ *
331
+ * @typeParam T - The element type of the paginated list.
332
+ * @param config - Request configuration.
333
+ * @returns The unwrapped paginated response.
334
+ * @throws {@link APIError} on non-2xx responses.
335
+ * @throws {@link ConnectionError} on network failures.
336
+ * @throws {@link TimeoutError} when the request exceeds the timeout.
337
+ */
338
+ async requestPaginated(config) {
339
+ return withRetry(
340
+ () => this.executePaginatedRequest(config),
341
+ config.method,
342
+ this.retryConfig,
343
+ config.signal
344
+ );
345
+ }
346
+ /**
347
+ * Convenience wrapper for a GET request returning a single resource.
348
+ */
349
+ async get(path, query) {
350
+ return this.request({ method: "GET", path, query });
351
+ }
352
+ /**
353
+ * Convenience wrapper for a GET request returning a paginated list.
354
+ */
355
+ async getPaginated(path, query) {
356
+ return this.requestPaginated({ method: "GET", path, query });
357
+ }
358
+ /**
359
+ * Convenience wrapper for a POST request.
360
+ */
361
+ async post(path, body, query) {
362
+ return this.request({ method: "POST", path, body, query });
363
+ }
364
+ /**
365
+ * Convenience wrapper for a PUT request.
366
+ */
367
+ async put(path, body, query) {
368
+ return this.request({ method: "PUT", path, body, query });
369
+ }
370
+ /**
371
+ * Convenience wrapper for a DELETE request.
372
+ */
373
+ async delete(path, query) {
374
+ return this.request({ method: "DELETE", path, query });
375
+ }
376
+ // -----------------------------------------------------------------------
377
+ // Internal execution
378
+ // -----------------------------------------------------------------------
379
+ async executeRequest(config) {
380
+ const response = await this.doFetch(config);
381
+ const requestId = extractRequestId(response.headers);
382
+ const rateLimit = parseRateLimitHeaders(response.headers);
383
+ if (!response.ok) {
384
+ await this.throwAPIError(response, requestId);
385
+ }
386
+ const json = await response.json();
387
+ return {
388
+ data: json.data,
389
+ status: response.status,
390
+ headers: response.headers,
391
+ requestId,
392
+ rateLimit
393
+ };
394
+ }
395
+ async executePaginatedRequest(config) {
396
+ const response = await this.doFetch(config);
397
+ const requestId = extractRequestId(response.headers);
398
+ const rateLimit = parseRateLimitHeaders(response.headers);
399
+ if (!response.ok) {
400
+ await this.throwAPIError(response, requestId);
401
+ }
402
+ const json = await response.json();
403
+ return {
404
+ data: json.data,
405
+ pagination: json.pagination,
406
+ status: response.status,
407
+ headers: response.headers,
408
+ requestId,
409
+ rateLimit
410
+ };
411
+ }
412
+ /**
413
+ * Perform the raw `fetch` call with merged headers, query string, timeout,
414
+ * and body serialisation.
415
+ */
416
+ async doFetch(config) {
417
+ const url = this.buildUrl(config.path, config.query);
418
+ const timeoutMs = config.timeout ?? this.defaultTimeout;
419
+ const { signal, cleanup } = composeSignals(timeoutMs, config.signal);
420
+ const headers = {
421
+ ...this.defaultHeaders,
422
+ ...buildAuthHeaders(this.apiKey),
423
+ ...config.headers
424
+ };
425
+ if (config.body !== void 0) {
426
+ headers["Content-Type"] = "application/json";
427
+ }
428
+ try {
429
+ const response = await fetch(url, {
430
+ method: config.method,
431
+ headers,
432
+ body: config.body !== void 0 ? JSON.stringify(config.body) : void 0,
433
+ signal
434
+ });
435
+ return response;
436
+ } catch (error) {
437
+ if (error instanceof TimeoutError) {
438
+ throw error;
439
+ }
440
+ if (error instanceof DOMException && error.name === "AbortError") {
441
+ if (config.signal?.aborted) {
442
+ throw error;
443
+ }
444
+ throw new TimeoutError(timeoutMs);
445
+ }
446
+ throw new ConnectionError(
447
+ error instanceof Error ? error.message : "Network request failed",
448
+ error
449
+ );
450
+ } finally {
451
+ cleanup();
452
+ }
453
+ }
454
+ /**
455
+ * Build the full URL from base URL, path, and query parameters.
456
+ */
457
+ buildUrl(path, query) {
458
+ const qs = query ? serialiseQuery(query) : "";
459
+ return `${this.baseUrl}${path}${qs}`;
460
+ }
461
+ /**
462
+ * Parse an error response body and throw the appropriate {@link APIError}.
463
+ */
464
+ async throwAPIError(response, requestId) {
465
+ let body = null;
466
+ try {
467
+ body = await response.json();
468
+ } catch {
469
+ }
470
+ throw createAPIError(response.status, body, requestId, response.headers);
471
+ }
472
+ };
473
+
474
+ // src/resources/firearms.ts
475
+ var FirearmsResource = class {
476
+ constructor(client) {
477
+ this.client = client;
478
+ }
479
+ // ---------------------------------------------------------------------------
480
+ // Collection endpoints
481
+ // ---------------------------------------------------------------------------
482
+ /**
483
+ * List firearms with optional filters and pagination.
484
+ *
485
+ * @param params - Optional query parameters for filtering, sorting, and pagination.
486
+ * @returns A paginated list of firearms matching the given filters.
487
+ * @throws {BadRequestError} If any filter value is invalid.
488
+ * @throws {AuthenticationError} If the API key is missing or invalid.
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * const result = await client.firearms.list({
493
+ * manufacturer: 'beretta',
494
+ * status: 'in_production',
495
+ * sort: 'name',
496
+ * order: 'asc',
497
+ * page: 1,
498
+ * per_page: 25,
499
+ * });
500
+ * console.log(result.data); // Firearm[]
501
+ * console.log(result.pagination.totalPages);
502
+ * ```
503
+ */
504
+ async list(params) {
505
+ return this.client.getPaginated("/v1/firearms", params);
506
+ }
507
+ /**
508
+ * Auto-paginate through all firearms matching the given filters.
509
+ *
510
+ * Returns an async iterator that fetches pages on demand, yielding
511
+ * individual {@link Firearm} objects. Useful for processing large
512
+ * result sets without managing pagination manually.
513
+ *
514
+ * @param params - Optional query parameters for filtering and sorting.
515
+ * @returns An async iterable iterator yielding individual firearms.
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * for await (const firearm of client.firearms.listAutoPaging({ manufacturer: 'colt' })) {
520
+ * console.log(firearm.name);
521
+ * }
522
+ * ```
523
+ */
524
+ async *listAutoPaging(params) {
525
+ let page = params?.page ?? 1;
526
+ while (true) {
527
+ const result = await this.list({ ...params, page });
528
+ for (const item of result.data) {
529
+ yield item;
530
+ }
531
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
532
+ page++;
533
+ }
534
+ }
535
+ /**
536
+ * Full-text search across firearms.
537
+ *
538
+ * @param params - Search query and pagination parameters.
539
+ * @returns A paginated list of firearms matching the search query.
540
+ * @throws {BadRequestError} If the search query is empty or too long.
541
+ * @throws {AuthenticationError} If the API key is missing or invalid.
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * const result = await client.firearms.search({ q: '9mm compact' });
546
+ * console.log(result.data.length);
547
+ * ```
548
+ */
549
+ async search(params) {
550
+ return this.client.getPaginated("/v1/firearms/search", params);
551
+ }
552
+ /**
553
+ * Compare up to 5 firearms side by side.
554
+ *
555
+ * @param params - Object containing comma-separated firearm IDs.
556
+ * @returns An array of firearms with full details for comparison.
557
+ * @throws {BadRequestError} If more than 5 IDs are provided or any ID is invalid.
558
+ * @throws {NotFoundError} If any of the specified firearms do not exist.
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const { data } = await client.firearms.compare({ ids: 'glock-g17,sig-sauer-p320,beretta-92fs' });
563
+ * console.log(data.items.length); // 3
564
+ * ```
565
+ */
566
+ async compare(params) {
567
+ return this.client.get("/v1/firearms/compare", params);
568
+ }
569
+ /**
570
+ * Retrieve game metadata for firearms (archetypes, stat ranges, etc.).
571
+ *
572
+ * @param params - Optional archetype filter.
573
+ * @returns Game metadata including archetype definitions and stat ranges.
574
+ * @throws {AuthenticationError} If the API key is missing or invalid.
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * const { data } = await client.firearms.gameMeta({ archetype: 'sniper' });
579
+ * ```
580
+ */
581
+ async gameMeta(params) {
582
+ return this.client.get("/v1/firearms/game-meta", params);
583
+ }
584
+ /**
585
+ * List all known action types across the database.
586
+ *
587
+ * @returns An array of distinct action type strings.
588
+ * @throws {AuthenticationError} If the API key is missing or invalid.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * const { data } = await client.firearms.actionTypes();
593
+ * // ['Semi-automatic', 'Bolt action', 'Lever action', ...]
594
+ * ```
595
+ */
596
+ async actionTypes() {
597
+ return this.client.get("/v1/firearms/action-types");
598
+ }
599
+ /**
600
+ * Get available filter options for the firearms list endpoint.
601
+ *
602
+ * Returns distinct values for manufacturers, calibers, categories,
603
+ * action types, countries, and statuses that can be used as filter values.
604
+ *
605
+ * @returns An object mapping filter fields to their available values.
606
+ * @throws {AuthenticationError} If the API key is missing or invalid.
607
+ *
608
+ * @example
609
+ * ```typescript
610
+ * const { data } = await client.firearms.filterOptions();
611
+ * console.log(data.manufacturers); // ['beretta', 'colt', ...]
612
+ * console.log(data.categories); // ['pistol', 'rifle', ...]
613
+ * ```
614
+ */
615
+ async filterOptions() {
616
+ return this.client.get("/v1/firearms/filter-options");
617
+ }
618
+ /**
619
+ * Get a random firearm, optionally filtered by category or country.
620
+ *
621
+ * @param params - Optional category and country filters.
622
+ * @returns A single randomly selected firearm.
623
+ * @throws {AuthenticationError} If the API key is missing or invalid.
624
+ *
625
+ * @example
626
+ * ```typescript
627
+ * const { data } = await client.firearms.random({ category: 'pistol' });
628
+ * console.log(data.name);
629
+ * ```
630
+ */
631
+ async random(params) {
632
+ return this.client.get("/v1/firearms/random", params);
633
+ }
634
+ /**
635
+ * Get top firearms ranked by a specific statistic.
636
+ *
637
+ * @param params - The stat to rank by and optional category/limit filters.
638
+ * @returns An ordered array of firearms ranked by the specified stat.
639
+ * @throws {BadRequestError} If the stat value is not a recognized ranking metric.
640
+ * @throws {AuthenticationError} If the API key is missing or invalid.
641
+ *
642
+ * @example
643
+ * ```typescript
644
+ * const { data } = await client.firearms.top({
645
+ * stat: 'lightest',
646
+ * category: 'pistol',
647
+ * limit: 5,
648
+ * });
649
+ * ```
650
+ */
651
+ async top(params) {
652
+ return this.client.get("/v1/firearms/top", params);
653
+ }
654
+ /**
655
+ * Compare two firearms in a head-to-head matchup.
656
+ *
657
+ * @param params - The slugs of the two firearms to compare.
658
+ * @returns A detailed head-to-head comparison with per-stat breakdowns.
659
+ * @throws {NotFoundError} If either firearm does not exist.
660
+ * @throws {AuthenticationError} If the API key is missing or invalid.
661
+ *
662
+ * @example
663
+ * ```typescript
664
+ * const { data } = await client.firearms.headToHead({ a: 'glock-g17', b: 'sig-sauer-p320' });
665
+ * console.log(data.winner);
666
+ * ```
667
+ */
668
+ async headToHead(params) {
669
+ return this.client.get("/v1/firearms/head-to-head", params);
670
+ }
671
+ /**
672
+ * Filter firearms by a specific feature.
673
+ *
674
+ * @param params - The feature name and optional category filter with pagination.
675
+ * @returns A paginated list of firearms that have the specified feature.
676
+ * @throws {BadRequestError} If the feature name is empty.
677
+ * @throws {AuthenticationError} If the API key is missing or invalid.
678
+ *
679
+ * @example
680
+ * ```typescript
681
+ * const result = await client.firearms.byFeature({ feature: 'threaded-barrel' });
682
+ * ```
683
+ */
684
+ async byFeature(params) {
685
+ return this.client.getPaginated("/v1/firearms/by-feature", params);
686
+ }
687
+ /**
688
+ * Filter firearms by action type.
689
+ *
690
+ * @param params - The action type string with pagination.
691
+ * @returns A paginated list of firearms with the specified action type.
692
+ * @throws {BadRequestError} If the action type is empty.
693
+ * @throws {AuthenticationError} If the API key is missing or invalid.
694
+ *
695
+ * @example
696
+ * ```typescript
697
+ * const result = await client.firearms.byAction({ action: 'semi-automatic' });
698
+ * ```
699
+ */
700
+ async byAction(params) {
701
+ return this.client.getPaginated("/v1/firearms/by-action", params);
702
+ }
703
+ /**
704
+ * Filter firearms by frame/component material.
705
+ *
706
+ * @param params - The material name, component type, and pagination.
707
+ * @returns A paginated list of firearms using the specified material.
708
+ * @throws {BadRequestError} If the material or component is invalid.
709
+ * @throws {AuthenticationError} If the API key is missing or invalid.
710
+ *
711
+ * @example
712
+ * ```typescript
713
+ * const result = await client.firearms.byMaterial({
714
+ * material: 'polymer',
715
+ * component: 'frame',
716
+ * });
717
+ * ```
718
+ */
719
+ async byMaterial(params) {
720
+ return this.client.getPaginated("/v1/firearms/by-material", params);
721
+ }
722
+ /**
723
+ * Filter firearms by designer name.
724
+ *
725
+ * @param params - The designer name string with pagination.
726
+ * @returns A paginated list of firearms designed by the specified person.
727
+ * @throws {BadRequestError} If the designer name is empty.
728
+ * @throws {AuthenticationError} If the API key is missing or invalid.
729
+ *
730
+ * @example
731
+ * ```typescript
732
+ * const result = await client.firearms.byDesigner({ designer: 'John Browning' });
733
+ * ```
734
+ */
735
+ async byDesigner(params) {
736
+ return this.client.getPaginated("/v1/firearms/by-designer", params);
737
+ }
738
+ /**
739
+ * Get firearms ranked by computed power rating.
740
+ *
741
+ * @param params - Optional category filter with pagination.
742
+ * @returns A paginated list of firearms with their power ratings.
743
+ * @throws {AuthenticationError} If the API key is missing or invalid.
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * const result = await client.firearms.powerRating({ category: 'rifle' });
748
+ * for (const item of result.data) {
749
+ * console.log(`${item.name}: ${item.rating}`);
750
+ * }
751
+ * ```
752
+ */
753
+ async powerRating(params) {
754
+ return this.client.getPaginated("/v1/firearms/power-rating", params);
755
+ }
756
+ /**
757
+ * Get firearms arranged in a chronological timeline.
758
+ *
759
+ * @param params - Optional year range, category filter, and pagination.
760
+ * @returns A paginated list of firearms ordered by introduction date.
761
+ * @throws {AuthenticationError} If the API key is missing or invalid.
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * const result = await client.firearms.timeline({ from: 1900, to: 1950 });
766
+ * ```
767
+ */
768
+ async timeline(params) {
769
+ return this.client.getPaginated("/v1/firearms/timeline", params);
770
+ }
771
+ /**
772
+ * Filter firearms by military conflict.
773
+ *
774
+ * @param params - The conflict identifier string with pagination.
775
+ * @returns A paginated list of firearms used in the specified conflict.
776
+ * @throws {BadRequestError} If the conflict name is empty.
777
+ * @throws {AuthenticationError} If the API key is missing or invalid.
778
+ *
779
+ * @example
780
+ * ```typescript
781
+ * const result = await client.firearms.byConflict({ conflict: 'world-war-ii' });
782
+ * ```
783
+ */
784
+ async byConflict(params) {
785
+ return this.client.getPaginated("/v1/firearms/by-conflict", params);
786
+ }
787
+ // ---------------------------------------------------------------------------
788
+ // Single-resource endpoints
789
+ // ---------------------------------------------------------------------------
790
+ /**
791
+ * Get a single firearm by its slug or ID.
792
+ *
793
+ * @param id - The firearm slug (e.g. `"glock-g17"`) or numeric ID.
794
+ * @returns The full firearm record with all available fields.
795
+ * @throws {NotFoundError} If no firearm matches the given identifier.
796
+ * @throws {AuthenticationError} If the API key is missing or invalid.
797
+ *
798
+ * @example
799
+ * ```typescript
800
+ * const { data } = await client.firearms.get('beretta-92fs');
801
+ * console.log(data.name); // "Beretta 92FS"
802
+ * console.log(data.manufacturerId); // "beretta"
803
+ * ```
804
+ */
805
+ async get(id) {
806
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}`);
807
+ }
808
+ /**
809
+ * Get all variants of a firearm.
810
+ *
811
+ * @param id - The parent firearm slug or ID.
812
+ * @returns An array of variant firearms derived from the parent.
813
+ * @throws {NotFoundError} If the parent firearm does not exist.
814
+ * @throws {AuthenticationError} If the API key is missing or invalid.
815
+ *
816
+ * @example
817
+ * ```typescript
818
+ * const { data } = await client.firearms.getVariants('colt-1911');
819
+ * console.log(data.map(v => v.name));
820
+ * ```
821
+ */
822
+ async getVariants(id) {
823
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/variants`);
824
+ }
825
+ /**
826
+ * Get images for a firearm.
827
+ *
828
+ * @param id - The firearm slug or ID.
829
+ * @returns An array of image records with URLs, dimensions, and metadata.
830
+ * @throws {NotFoundError} If the firearm does not exist.
831
+ * @throws {AuthenticationError} If the API key is missing or invalid.
832
+ *
833
+ * @example
834
+ * ```typescript
835
+ * const { data } = await client.firearms.getImages('sig-sauer-p226');
836
+ * for (const img of data) {
837
+ * console.log(img.url, img.width, img.height);
838
+ * }
839
+ * ```
840
+ */
841
+ async getImages(id) {
842
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/images`);
843
+ }
844
+ /**
845
+ * Get computed game statistics for a firearm.
846
+ *
847
+ * @param id - The firearm slug or ID.
848
+ * @returns Game-balanced stats including damage, accuracy, range, etc.
849
+ * @throws {NotFoundError} If the firearm does not exist.
850
+ * @throws {AuthenticationError} If the API key is missing or invalid.
851
+ *
852
+ * @example
853
+ * ```typescript
854
+ * const { data } = await client.firearms.getGameStats('ak-47');
855
+ * console.log(data.damage, data.accuracy, data.range);
856
+ * ```
857
+ */
858
+ async getGameStats(id) {
859
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/game-stats`);
860
+ }
861
+ /**
862
+ * Get physical dimensions for a firearm.
863
+ *
864
+ * @param id - The firearm slug or ID.
865
+ * @returns Detailed dimension data (length, height, width, barrel length, weight).
866
+ * @throws {NotFoundError} If the firearm does not exist.
867
+ * @throws {AuthenticationError} If the API key is missing or invalid.
868
+ *
869
+ * @example
870
+ * ```typescript
871
+ * const { data } = await client.firearms.getDimensions('glock-g19');
872
+ * console.log(`${data.overallLengthMm}mm overall`);
873
+ * ```
874
+ */
875
+ async getDimensions(id) {
876
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/dimensions`);
877
+ }
878
+ /**
879
+ * Get known military/law-enforcement adopters of a firearm.
880
+ *
881
+ * @param id - The firearm slug or ID.
882
+ * @returns An array of users/adopters with country and organization data.
883
+ * @throws {NotFoundError} If the firearm does not exist.
884
+ * @throws {AuthenticationError} If the API key is missing or invalid.
885
+ *
886
+ * @example
887
+ * ```typescript
888
+ * const { data } = await client.firearms.getUsers('beretta-m9');
889
+ * for (const user of data) {
890
+ * console.log(user.country, user.organization);
891
+ * }
892
+ * ```
893
+ */
894
+ async getUsers(id) {
895
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/users`);
896
+ }
897
+ /**
898
+ * Get the family tree (lineage) of a firearm.
899
+ *
900
+ * @param id - The firearm slug or ID.
901
+ * @returns A tree structure showing predecessors, descendants, and related models.
902
+ * @throws {NotFoundError} If the firearm does not exist.
903
+ * @throws {AuthenticationError} If the API key is missing or invalid.
904
+ *
905
+ * @example
906
+ * ```typescript
907
+ * const { data } = await client.firearms.getFamilyTree('m16a2');
908
+ * console.log(data.ancestors, data.descendants);
909
+ * ```
910
+ */
911
+ async getFamilyTree(id) {
912
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/family-tree`);
913
+ }
914
+ /**
915
+ * Get firearms similar to a given firearm.
916
+ *
917
+ * @param id - The firearm slug or ID.
918
+ * @returns An array of similar firearms ranked by similarity score.
919
+ * @throws {NotFoundError} If the firearm does not exist.
920
+ * @throws {AuthenticationError} If the API key is missing or invalid.
921
+ *
922
+ * @example
923
+ * ```typescript
924
+ * const { data } = await client.firearms.getSimilar('glock-g17');
925
+ * for (const match of data) {
926
+ * console.log(match.name, match.similarityScore);
927
+ * }
928
+ * ```
929
+ */
930
+ async getSimilar(id) {
931
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/similar`);
932
+ }
933
+ /**
934
+ * Get the worldwide adoption map for a firearm.
935
+ *
936
+ * @param id - The firearm slug or ID.
937
+ * @returns Adoption data by country with usage type and date ranges.
938
+ * @throws {NotFoundError} If the firearm does not exist.
939
+ * @throws {AuthenticationError} If the API key is missing or invalid.
940
+ *
941
+ * @example
942
+ * ```typescript
943
+ * const { data } = await client.firearms.getAdoptionMap('fn-fal');
944
+ * console.log(data.countries.length, 'countries adopted this firearm');
945
+ * ```
946
+ */
947
+ async getAdoptionMap(id) {
948
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/adoption-map`);
949
+ }
950
+ /**
951
+ * Get the full game profile for a firearm.
952
+ *
953
+ * @param id - The firearm slug or ID.
954
+ * @returns Game-specific profile including archetype, tier, and stat breakdown.
955
+ * @throws {NotFoundError} If the firearm does not exist.
956
+ * @throws {AuthenticationError} If the API key is missing or invalid.
957
+ *
958
+ * @example
959
+ * ```typescript
960
+ * const { data } = await client.firearms.getGameProfile('mp5');
961
+ * console.log(data.archetype, data.tier);
962
+ * ```
963
+ */
964
+ async getGameProfile(id) {
965
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/game-profile`);
966
+ }
967
+ /**
968
+ * Get an SVG silhouette (line art) of a firearm.
969
+ *
970
+ * @param id - The firearm slug or ID.
971
+ * @param params - Optional format and stroke customization parameters.
972
+ * @returns Silhouette data in the requested format (raw SVG, data URI, or JSON).
973
+ * @throws {NotFoundError} If the firearm does not exist or has no silhouette.
974
+ * @throws {AuthenticationError} If the API key is missing or invalid.
975
+ *
976
+ * @example
977
+ * ```typescript
978
+ * const { data } = await client.firearms.getSilhouette('ak-47', {
979
+ * format: 'datauri',
980
+ * stroke_width: 2,
981
+ * stroke_color: '#333',
982
+ * });
983
+ * // Use data.dataUri in an <img> tag
984
+ * ```
985
+ */
986
+ async getSilhouette(id, params) {
987
+ return this.client.get(
988
+ `/v1/firearms/${encodeURIComponent(id)}/silhouette`,
989
+ params
990
+ );
991
+ }
992
+ /**
993
+ * Calculate ballistics for a firearm with specific ammunition.
994
+ *
995
+ * @param id - The firearm slug or ID.
996
+ * @param params - Ammunition ID and optional ballistic parameters.
997
+ * @returns Calculated ballistic data including velocity, energy, and trajectory.
998
+ * @throws {NotFoundError} If the firearm or ammunition does not exist.
999
+ * @throws {BadRequestError} If the ammunition is incompatible with the firearm.
1000
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1001
+ *
1002
+ * @example
1003
+ * ```typescript
1004
+ * const { data } = await client.firearms.calculate('glock-g17', {
1005
+ * ammo_id: '9mm-federal-hst-124gr',
1006
+ * });
1007
+ * console.log(data.muzzleVelocity, data.muzzleEnergy);
1008
+ * ```
1009
+ */
1010
+ async calculate(id, params) {
1011
+ return this.client.get(
1012
+ `/v1/firearms/${encodeURIComponent(id)}/calculate`,
1013
+ params
1014
+ );
1015
+ }
1016
+ /**
1017
+ * Load a firearm with ammunition and get combined performance data.
1018
+ *
1019
+ * @param id - The firearm slug or ID.
1020
+ * @param params - Optional ammunition ID to load.
1021
+ * @returns Combined firearm + ammunition data with computed performance metrics.
1022
+ * @throws {NotFoundError} If the firearm does not exist.
1023
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1024
+ *
1025
+ * @example
1026
+ * ```typescript
1027
+ * const { data } = await client.firearms.load('sig-sauer-p226', {
1028
+ * ammo_id: '9mm-winchester-ranger-147gr',
1029
+ * });
1030
+ * ```
1031
+ */
1032
+ async load(id, params) {
1033
+ return this.client.get(
1034
+ `/v1/firearms/${encodeURIComponent(id)}/load`,
1035
+ params
1036
+ );
1037
+ }
1038
+ };
1039
+
1040
+ // src/resources/manufacturers.ts
1041
+ var ManufacturersResource = class {
1042
+ constructor(client) {
1043
+ this.client = client;
1044
+ }
1045
+ /**
1046
+ * List manufacturers with optional filters and pagination.
1047
+ *
1048
+ * @param params - Optional query parameters for filtering by country, sorting, and pagination.
1049
+ * @returns A paginated list of manufacturers.
1050
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1051
+ *
1052
+ * @example
1053
+ * ```typescript
1054
+ * const result = await client.manufacturers.list({
1055
+ * country: 'DE',
1056
+ * sort: 'name',
1057
+ * order: 'asc',
1058
+ * per_page: 50,
1059
+ * });
1060
+ * ```
1061
+ */
1062
+ async list(params) {
1063
+ return this.client.getPaginated("/v1/manufacturers", params);
1064
+ }
1065
+ /**
1066
+ * Auto-paginate through all manufacturers matching the given filters.
1067
+ *
1068
+ * Returns an async iterator that fetches pages on demand, yielding
1069
+ * individual {@link Manufacturer} objects.
1070
+ *
1071
+ * @param params - Optional query parameters for filtering and sorting.
1072
+ * @returns An async iterable iterator yielding individual manufacturers.
1073
+ *
1074
+ * @example
1075
+ * ```typescript
1076
+ * for await (const mfr of client.manufacturers.listAutoPaging()) {
1077
+ * console.log(mfr.name, mfr.country);
1078
+ * }
1079
+ * ```
1080
+ */
1081
+ async *listAutoPaging(params) {
1082
+ let page = params?.page ?? 1;
1083
+ while (true) {
1084
+ const result = await this.list({ ...params, page });
1085
+ for (const item of result.data) {
1086
+ yield item;
1087
+ }
1088
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1089
+ page++;
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Get a single manufacturer by its slug or ID.
1094
+ *
1095
+ * @param id - The manufacturer slug (e.g. `"beretta"`) or numeric ID.
1096
+ * @returns The full manufacturer record.
1097
+ * @throws {NotFoundError} If no manufacturer matches the given identifier.
1098
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1099
+ *
1100
+ * @example
1101
+ * ```typescript
1102
+ * const { data } = await client.manufacturers.get('sig-sauer');
1103
+ * console.log(data.name, data.foundedYear, data.country);
1104
+ * ```
1105
+ */
1106
+ async get(id) {
1107
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}`);
1108
+ }
1109
+ /**
1110
+ * Get all firearms produced by a manufacturer.
1111
+ *
1112
+ * @param id - The manufacturer slug or ID.
1113
+ * @param params - Optional pagination parameters.
1114
+ * @returns A paginated list of firearms from the specified manufacturer.
1115
+ * @throws {NotFoundError} If the manufacturer does not exist.
1116
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1117
+ *
1118
+ * @example
1119
+ * ```typescript
1120
+ * const result = await client.manufacturers.getFirearms('colt', { per_page: 50 });
1121
+ * console.log(`Colt makes ${result.pagination.total} firearms`);
1122
+ * ```
1123
+ */
1124
+ async getFirearms(id, params) {
1125
+ return this.client.getPaginated(
1126
+ `/v1/manufacturers/${encodeURIComponent(id)}/firearms`,
1127
+ params
1128
+ );
1129
+ }
1130
+ /**
1131
+ * Get a chronological timeline for a manufacturer.
1132
+ *
1133
+ * @param id - The manufacturer slug or ID.
1134
+ * @returns Timeline data with key events, product launches, and milestones.
1135
+ * @throws {NotFoundError} If the manufacturer does not exist.
1136
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1137
+ *
1138
+ * @example
1139
+ * ```typescript
1140
+ * const { data } = await client.manufacturers.getTimeline('browning');
1141
+ * for (const event of data.events) {
1142
+ * console.log(event.year, event.description);
1143
+ * }
1144
+ * ```
1145
+ */
1146
+ async getTimeline(id) {
1147
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}/timeline`);
1148
+ }
1149
+ /**
1150
+ * Get aggregate statistics for a manufacturer.
1151
+ *
1152
+ * @param id - The manufacturer slug or ID.
1153
+ * @returns Statistical summary including firearm counts, caliber distribution, and more.
1154
+ * @throws {NotFoundError} If the manufacturer does not exist.
1155
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * const { data } = await client.manufacturers.getStats('glock');
1160
+ * console.log(data.totalFirearms, data.caliberBreakdown);
1161
+ * ```
1162
+ */
1163
+ async getStats(id) {
1164
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}/stats`);
1165
+ }
1166
+ };
1167
+
1168
+ // src/resources/calibers.ts
1169
+ var CalibersResource = class {
1170
+ constructor(client) {
1171
+ this.client = client;
1172
+ }
1173
+ /**
1174
+ * List calibers with optional filters and pagination.
1175
+ *
1176
+ * @param params - Optional query parameters for filtering by cartridge type, primer type, etc.
1177
+ * @returns A paginated list of calibers.
1178
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1179
+ *
1180
+ * @example
1181
+ * ```typescript
1182
+ * const result = await client.calibers.list({
1183
+ * cartridge_type: 'centerfire',
1184
+ * primer_type: 'boxer',
1185
+ * per_page: 50,
1186
+ * });
1187
+ * ```
1188
+ */
1189
+ async list(params) {
1190
+ return this.client.getPaginated("/v1/calibers", params);
1191
+ }
1192
+ /**
1193
+ * Auto-paginate through all calibers matching the given filters.
1194
+ *
1195
+ * Returns an async iterator that fetches pages on demand, yielding
1196
+ * individual {@link Caliber} objects.
1197
+ *
1198
+ * @param params - Optional query parameters for filtering and sorting.
1199
+ * @returns An async iterable iterator yielding individual calibers.
1200
+ *
1201
+ * @example
1202
+ * ```typescript
1203
+ * for await (const caliber of client.calibers.listAutoPaging()) {
1204
+ * console.log(caliber.name, caliber.bulletDiameterMm);
1205
+ * }
1206
+ * ```
1207
+ */
1208
+ async *listAutoPaging(params) {
1209
+ let page = params?.page ?? 1;
1210
+ while (true) {
1211
+ const result = await this.list({ ...params, page });
1212
+ for (const item of result.data) {
1213
+ yield item;
1214
+ }
1215
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1216
+ page++;
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Compare up to 5 calibers side by side.
1221
+ *
1222
+ * @param params - Object containing comma-separated caliber IDs.
1223
+ * @returns An array of calibers with full details for comparison.
1224
+ * @throws {BadRequestError} If more than 5 IDs are provided.
1225
+ * @throws {NotFoundError} If any of the specified calibers do not exist.
1226
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1227
+ *
1228
+ * @example
1229
+ * ```typescript
1230
+ * const { data } = await client.calibers.compare({
1231
+ * ids: '9x19mm-parabellum,45-acp,40-s-w',
1232
+ * });
1233
+ * ```
1234
+ */
1235
+ async compare(params) {
1236
+ return this.client.get("/v1/calibers/compare", params);
1237
+ }
1238
+ /**
1239
+ * Get ballistics data for a caliber at a specified distance.
1240
+ *
1241
+ * @param params - Caliber ID and distance in meters.
1242
+ * @returns Ballistic data including velocity, energy, and drop at the specified distance.
1243
+ * @throws {NotFoundError} If the caliber does not exist.
1244
+ * @throws {BadRequestError} If the distance is out of range.
1245
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1246
+ *
1247
+ * @example
1248
+ * ```typescript
1249
+ * const { data } = await client.calibers.ballistics({
1250
+ * id: '9x19mm-parabellum',
1251
+ * distance: 100,
1252
+ * });
1253
+ * console.log(data.velocity, data.energy, data.drop);
1254
+ * ```
1255
+ */
1256
+ async ballistics(params) {
1257
+ return this.client.get("/v1/calibers/ballistics", params);
1258
+ }
1259
+ /**
1260
+ * Get a single caliber by its slug or ID.
1261
+ *
1262
+ * @param id - The caliber slug (e.g. `"9x19mm-parabellum"`) or numeric ID.
1263
+ * @returns The full caliber record with all specifications.
1264
+ * @throws {NotFoundError} If no caliber matches the given identifier.
1265
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1266
+ *
1267
+ * @example
1268
+ * ```typescript
1269
+ * const { data } = await client.calibers.get('45-acp');
1270
+ * console.log(data.name, data.bulletDiameterMm, data.caseLengthMm);
1271
+ * ```
1272
+ */
1273
+ async get(id) {
1274
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}`);
1275
+ }
1276
+ /**
1277
+ * Get all firearms that use a specific caliber.
1278
+ *
1279
+ * @param id - The caliber slug or ID.
1280
+ * @param params - Optional pagination parameters.
1281
+ * @returns A paginated list of firearms chambered in the specified caliber.
1282
+ * @throws {NotFoundError} If the caliber does not exist.
1283
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1284
+ *
1285
+ * @example
1286
+ * ```typescript
1287
+ * const result = await client.calibers.getFirearms('9x19mm-parabellum', { per_page: 50 });
1288
+ * console.log(`${result.pagination.total} firearms use 9mm`);
1289
+ * ```
1290
+ */
1291
+ async getFirearms(id, params) {
1292
+ return this.client.getPaginated(
1293
+ `/v1/calibers/${encodeURIComponent(id)}/firearms`,
1294
+ params
1295
+ );
1296
+ }
1297
+ /**
1298
+ * Get the parent caliber chain (ancestry) for a caliber.
1299
+ *
1300
+ * @param id - The caliber slug or ID.
1301
+ * @returns An ordered array of calibers from the given caliber up to the root ancestor.
1302
+ * @throws {NotFoundError} If the caliber does not exist.
1303
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1304
+ *
1305
+ * @example
1306
+ * ```typescript
1307
+ * const { data } = await client.calibers.getParentChain('300-blackout');
1308
+ * // [300 Blackout, 5.56x45mm NATO, .223 Remington, ...]
1309
+ * ```
1310
+ */
1311
+ async getParentChain(id) {
1312
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}/parent-chain`);
1313
+ }
1314
+ /**
1315
+ * Get the full family tree of related calibers.
1316
+ *
1317
+ * @param id - The caliber slug or ID.
1318
+ * @returns An array of calibers in the same family (parent, siblings, children).
1319
+ * @throws {NotFoundError} If the caliber does not exist.
1320
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1321
+ *
1322
+ * @example
1323
+ * ```typescript
1324
+ * const { data } = await client.calibers.getFamily('9x19mm-parabellum');
1325
+ * console.log(data.map(c => c.name));
1326
+ * ```
1327
+ */
1328
+ async getFamily(id) {
1329
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}/family`);
1330
+ }
1331
+ /**
1332
+ * Get ammunition loads available for a caliber.
1333
+ *
1334
+ * @param id - The caliber slug or ID.
1335
+ * @param params - Optional pagination parameters.
1336
+ * @returns A paginated list of ammunition loads for the specified caliber.
1337
+ * @throws {NotFoundError} If the caliber does not exist.
1338
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1339
+ *
1340
+ * @example
1341
+ * ```typescript
1342
+ * const result = await client.calibers.getAmmunition('9x19mm-parabellum', { per_page: 25 });
1343
+ * for (const ammo of result.data) {
1344
+ * console.log(ammo.name, ammo.bulletWeightGrains);
1345
+ * }
1346
+ * ```
1347
+ */
1348
+ async getAmmunition(id, params) {
1349
+ return this.client.getPaginated(
1350
+ `/v1/calibers/${encodeURIComponent(id)}/ammunition`,
1351
+ params
1352
+ );
1353
+ }
1354
+ };
1355
+
1356
+ // src/resources/categories.ts
1357
+ var CategoriesResource = class {
1358
+ constructor(client) {
1359
+ this.client = client;
1360
+ }
1361
+ /**
1362
+ * List all firearm categories.
1363
+ *
1364
+ * @returns An array of all category records.
1365
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1366
+ *
1367
+ * @example
1368
+ * ```typescript
1369
+ * const { data } = await client.categories.list();
1370
+ * // [{ slug: 'pistol', name: 'Pistol' }, { slug: 'rifle', name: 'Rifle' }, ...]
1371
+ * ```
1372
+ */
1373
+ async list() {
1374
+ return this.client.get("/v1/categories");
1375
+ }
1376
+ /**
1377
+ * Get all firearms within a specific category.
1378
+ *
1379
+ * @param slug - The category slug (e.g. `"pistol"`, `"rifle"`, `"shotgun"`).
1380
+ * @param params - Optional pagination parameters.
1381
+ * @returns A paginated list of firearms in the specified category.
1382
+ * @throws {NotFoundError} If the category slug does not exist.
1383
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1384
+ *
1385
+ * @example
1386
+ * ```typescript
1387
+ * const result = await client.categories.getFirearms('shotgun', {
1388
+ * per_page: 25,
1389
+ * sort: 'name',
1390
+ * order: 'asc',
1391
+ * });
1392
+ * console.log(`${result.pagination.total} shotguns in database`);
1393
+ * ```
1394
+ */
1395
+ async getFirearms(slug, params) {
1396
+ return this.client.getPaginated(
1397
+ `/v1/categories/${encodeURIComponent(slug)}/firearms`,
1398
+ params
1399
+ );
1400
+ }
1401
+ };
1402
+
1403
+ // src/resources/stats.ts
1404
+ var StatsResource = class {
1405
+ constructor(client) {
1406
+ this.client = client;
1407
+ }
1408
+ /**
1409
+ * Get a high-level summary of the database.
1410
+ *
1411
+ * @returns Summary counts for firearms, manufacturers, calibers, and categories.
1412
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1413
+ *
1414
+ * @example
1415
+ * ```typescript
1416
+ * const { data } = await client.stats.summary();
1417
+ * console.log(`Database has ${data.totalFirearms} firearms`);
1418
+ * ```
1419
+ */
1420
+ async summary() {
1421
+ return this.client.get("/v1/stats/summary");
1422
+ }
1423
+ /**
1424
+ * Get firearm counts grouped by production status.
1425
+ *
1426
+ * @returns Counts for in-production, discontinued, and prototype firearms.
1427
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1428
+ *
1429
+ * @example
1430
+ * ```typescript
1431
+ * const { data } = await client.stats.productionStatus();
1432
+ * // { in_production: 342, discontinued: 891, prototype: 12 }
1433
+ * ```
1434
+ */
1435
+ async productionStatus() {
1436
+ return this.client.get("/v1/stats/production-status");
1437
+ }
1438
+ /**
1439
+ * Get field coverage statistics across the database.
1440
+ *
1441
+ * Shows the percentage of firearms that have data for each field,
1442
+ * useful for assessing data completeness.
1443
+ *
1444
+ * @returns Per-field coverage percentages.
1445
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1446
+ *
1447
+ * @example
1448
+ * ```typescript
1449
+ * const { data } = await client.stats.fieldCoverage();
1450
+ * console.log(`Weight field: ${data.weight}% coverage`);
1451
+ * ```
1452
+ */
1453
+ async fieldCoverage() {
1454
+ return this.client.get("/v1/stats/field-coverage");
1455
+ }
1456
+ /**
1457
+ * Get the most popular calibers by firearm count.
1458
+ *
1459
+ * @param params - Optional limit parameter.
1460
+ * @returns An ordered list of calibers ranked by the number of firearms using them.
1461
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1462
+ *
1463
+ * @example
1464
+ * ```typescript
1465
+ * const { data } = await client.stats.popularCalibers({ limit: 10 });
1466
+ * for (const entry of data) {
1467
+ * console.log(`${entry.name}: ${entry.count} firearms`);
1468
+ * }
1469
+ * ```
1470
+ */
1471
+ async popularCalibers(params) {
1472
+ return this.client.get("/v1/stats/calibers/popular", params);
1473
+ }
1474
+ /**
1475
+ * Get the most prolific manufacturers by firearm count.
1476
+ *
1477
+ * @param params - Optional limit and category filter.
1478
+ * @returns An ordered list of manufacturers ranked by the number of firearms produced.
1479
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1480
+ *
1481
+ * @example
1482
+ * ```typescript
1483
+ * const { data } = await client.stats.prolificManufacturers({
1484
+ * limit: 10,
1485
+ * category: 'pistol',
1486
+ * });
1487
+ * ```
1488
+ */
1489
+ async prolificManufacturers(params) {
1490
+ return this.client.get("/v1/stats/manufacturers/prolific", params);
1491
+ }
1492
+ /**
1493
+ * Get firearm counts grouped by category.
1494
+ *
1495
+ * @returns Counts per category (pistol, rifle, shotgun, etc.).
1496
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1497
+ *
1498
+ * @example
1499
+ * ```typescript
1500
+ * const { data } = await client.stats.byCategory();
1501
+ * // [{ category: 'pistol', count: 512 }, { category: 'rifle', count: 234 }, ...]
1502
+ * ```
1503
+ */
1504
+ async byCategory() {
1505
+ return this.client.get("/v1/stats/by-category");
1506
+ }
1507
+ /**
1508
+ * Get firearm statistics for a specific decade/era.
1509
+ *
1510
+ * @param params - The decade string (e.g. `"1990s"`).
1511
+ * @returns Statistics for firearms introduced during the specified decade.
1512
+ * @throws {BadRequestError} If the decade format is invalid (must be like `"1990s"`).
1513
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1514
+ *
1515
+ * @example
1516
+ * ```typescript
1517
+ * const { data } = await client.stats.byEra({ decade: '1940s' });
1518
+ * ```
1519
+ */
1520
+ async byEra(params) {
1521
+ return this.client.get("/v1/stats/by-era", params);
1522
+ }
1523
+ /**
1524
+ * Get statistics about materials used across all firearms.
1525
+ *
1526
+ * @returns Material usage counts and percentages by component type.
1527
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1528
+ *
1529
+ * @example
1530
+ * ```typescript
1531
+ * const { data } = await client.stats.materials();
1532
+ * ```
1533
+ */
1534
+ async materials() {
1535
+ return this.client.get("/v1/stats/materials");
1536
+ }
1537
+ /**
1538
+ * Get firearm adoption statistics for a specific country.
1539
+ *
1540
+ * @param params - The country code (e.g. `"US"`, `"GB"`).
1541
+ * @returns Adoption data for the specified country.
1542
+ * @throws {BadRequestError} If the country code is invalid.
1543
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1544
+ *
1545
+ * @example
1546
+ * ```typescript
1547
+ * const { data } = await client.stats.adoptionByCountry({ code: 'US' });
1548
+ * ```
1549
+ */
1550
+ async adoptionByCountry(params) {
1551
+ return this.client.get("/v1/stats/adoption/by-country", params);
1552
+ }
1553
+ /**
1554
+ * Get firearm adoption statistics by usage type.
1555
+ *
1556
+ * @param params - The usage type (e.g. `"military"`, `"law_enforcement"`, `"civilian"`).
1557
+ * @returns Adoption data for the specified usage type.
1558
+ * @throws {BadRequestError} If the type is not a recognized usage category.
1559
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1560
+ *
1561
+ * @example
1562
+ * ```typescript
1563
+ * const { data } = await client.stats.adoptionByType({ type: 'military' });
1564
+ * ```
1565
+ */
1566
+ async adoptionByType(params) {
1567
+ return this.client.get("/v1/stats/adoption/by-type", params);
1568
+ }
1569
+ /**
1570
+ * Get firearm counts grouped by action type.
1571
+ *
1572
+ * @param params - Optional category filter.
1573
+ * @returns Counts per action type, optionally filtered by category.
1574
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1575
+ *
1576
+ * @example
1577
+ * ```typescript
1578
+ * const { data } = await client.stats.actionTypes({ category: 'rifle' });
1579
+ * ```
1580
+ */
1581
+ async actionTypes(params) {
1582
+ return this.client.get("/v1/stats/action-types", params);
1583
+ }
1584
+ /**
1585
+ * Get feature frequency statistics across the database.
1586
+ *
1587
+ * @param params - Optional category filter and result limit.
1588
+ * @returns An ordered list of features ranked by frequency of occurrence.
1589
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1590
+ *
1591
+ * @example
1592
+ * ```typescript
1593
+ * const { data } = await client.stats.featureFrequency({
1594
+ * category: 'pistol',
1595
+ * limit: 20,
1596
+ * });
1597
+ * ```
1598
+ */
1599
+ async featureFrequency(params) {
1600
+ return this.client.get("/v1/stats/feature-frequency", params);
1601
+ }
1602
+ /**
1603
+ * Get caliber popularity trends across historical eras.
1604
+ *
1605
+ * @param params - Optional from/to decade range (e.g. `"1940s"` to `"2020s"`).
1606
+ * @returns Caliber popularity data broken down by decade.
1607
+ * @throws {BadRequestError} If the decade format is invalid.
1608
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1609
+ *
1610
+ * @example
1611
+ * ```typescript
1612
+ * const { data } = await client.stats.caliberPopularityByEra({
1613
+ * from_decade: '1940s',
1614
+ * to_decade: '2020s',
1615
+ * });
1616
+ * ```
1617
+ */
1618
+ async caliberPopularityByEra(params) {
1619
+ return this.client.get("/v1/stats/caliber-popularity-by-era", params);
1620
+ }
1621
+ };
1622
+
1623
+ // src/resources/game.ts
1624
+ var GameResource = class {
1625
+ constructor(client) {
1626
+ this.client = client;
1627
+ }
1628
+ /**
1629
+ * Get a balance report showing outlier firearms.
1630
+ *
1631
+ * Identifies firearms whose game stats deviate significantly from
1632
+ * the average, useful for game balancing.
1633
+ *
1634
+ * @param params - Optional threshold for outlier detection (0-100).
1635
+ * @returns A balance report with overpowered and underpowered firearms.
1636
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1637
+ *
1638
+ * @example
1639
+ * ```typescript
1640
+ * const { data } = await client.game.balanceReport({ threshold: 15 });
1641
+ * console.log(data.length, 'firearms flagged as outliers');
1642
+ * ```
1643
+ */
1644
+ async balanceReport(params) {
1645
+ return this.client.get("/v1/game/balance-report", params);
1646
+ }
1647
+ /**
1648
+ * Get a tier list of firearms ranked by a specific stat.
1649
+ *
1650
+ * @param params - Optional category filter and stat to rank by.
1651
+ * @returns A tier list with firearms grouped into S/A/B/C/D/F tiers.
1652
+ * @throws {BadRequestError} If the stat is not a valid game stat.
1653
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1654
+ *
1655
+ * @example
1656
+ * ```typescript
1657
+ * const { data } = await client.game.tierList({
1658
+ * stat: 'accuracy',
1659
+ * category: 'rifle',
1660
+ * });
1661
+ * console.log('S-tier:', data.tiers.S.map(f => f.name));
1662
+ * ```
1663
+ */
1664
+ async tierList(params) {
1665
+ return this.client.get("/v1/game/tier-list", params);
1666
+ }
1667
+ /**
1668
+ * Get a game-balanced matchup between two firearms.
1669
+ *
1670
+ * @param params - The slugs of the two firearms to compare.
1671
+ * @returns A detailed matchup result with per-stat comparisons and a winner.
1672
+ * @throws {NotFoundError} If either firearm does not exist.
1673
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1674
+ *
1675
+ * @example
1676
+ * ```typescript
1677
+ * const { data } = await client.game.matchups({ a: 'ak-47', b: 'm4-carbine' });
1678
+ * console.log(data.winner, data.statComparisons);
1679
+ * ```
1680
+ */
1681
+ async matchups(params) {
1682
+ return this.client.get("/v1/game/matchups", params);
1683
+ }
1684
+ /**
1685
+ * Get a roster of firearms best suited for a specific game role.
1686
+ *
1687
+ * @param params - The role to query and optional count limit.
1688
+ * @returns A roster of firearms ranked by suitability for the given role.
1689
+ * @throws {BadRequestError} If the role is not a recognized game role.
1690
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1691
+ *
1692
+ * @example
1693
+ * ```typescript
1694
+ * const { data } = await client.game.roleRoster({
1695
+ * role: 'sniper',
1696
+ * count: 10,
1697
+ * });
1698
+ * for (const firearm of data) {
1699
+ * console.log(firearm.name, firearm.roleScore);
1700
+ * }
1701
+ * ```
1702
+ */
1703
+ async roleRoster(params) {
1704
+ return this.client.get("/v1/game/role-roster", params);
1705
+ }
1706
+ /**
1707
+ * Get the statistical distribution for a specific game stat.
1708
+ *
1709
+ * Shows how firearms are distributed across value ranges for a given
1710
+ * stat, useful for understanding the spread and identifying balance issues.
1711
+ *
1712
+ * @param params - The stat to analyze (e.g. `"damage"`, `"accuracy"`).
1713
+ * @returns Distribution data including histogram buckets and summary statistics.
1714
+ * @throws {BadRequestError} If the stat is not a valid game stat.
1715
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1716
+ *
1717
+ * @example
1718
+ * ```typescript
1719
+ * const { data } = await client.game.statDistribution({ stat: 'damage' });
1720
+ * console.log(data.mean, data.median, data.buckets);
1721
+ * ```
1722
+ */
1723
+ async statDistribution(params) {
1724
+ return this.client.get("/v1/game/stat-distribution", params);
1725
+ }
1726
+ };
1727
+
1728
+ // src/resources/game-stats.ts
1729
+ var GameStatsResource = class {
1730
+ constructor(client) {
1731
+ this.client = client;
1732
+ }
1733
+ /**
1734
+ * List all available game stats snapshot versions.
1735
+ *
1736
+ * @returns An array of version records with metadata about each snapshot.
1737
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1738
+ *
1739
+ * @example
1740
+ * ```typescript
1741
+ * const { data } = await client.gameStats.listVersions();
1742
+ * for (const version of data) {
1743
+ * console.log(version.version, version.createdAt, version.description);
1744
+ * }
1745
+ * ```
1746
+ */
1747
+ async listVersions() {
1748
+ return this.client.get("/v1/game-stats/versions");
1749
+ }
1750
+ /**
1751
+ * List all firearms in a specific game stats snapshot version.
1752
+ *
1753
+ * @param version - The snapshot version string (e.g. `"1.0.0"`).
1754
+ * @param params - Optional pagination parameters.
1755
+ * @returns A paginated list of firearms with their game stats for the given version.
1756
+ * @throws {NotFoundError} If the specified version does not exist.
1757
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1758
+ *
1759
+ * @example
1760
+ * ```typescript
1761
+ * const result = await client.gameStats.listFirearms('1.0.0', { per_page: 50 });
1762
+ * for (const firearm of result.data) {
1763
+ * console.log(firearm.name, firearm.damage);
1764
+ * }
1765
+ * ```
1766
+ */
1767
+ async listFirearms(version, params) {
1768
+ return this.client.getPaginated(
1769
+ `/v1/game-stats/versions/${encodeURIComponent(version)}/firearms`,
1770
+ params
1771
+ );
1772
+ }
1773
+ /**
1774
+ * Get a single firearm's game stats from a specific snapshot version.
1775
+ *
1776
+ * @param version - The snapshot version string (e.g. `"1.0.0"`).
1777
+ * @param id - The firearm slug or ID.
1778
+ * @returns The firearm's game stats as captured in the specified version.
1779
+ * @throws {NotFoundError} If the version or firearm does not exist in the snapshot.
1780
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1781
+ *
1782
+ * @example
1783
+ * ```typescript
1784
+ * const { data } = await client.gameStats.getFirearm('1.0.0', 'glock-g17');
1785
+ * console.log(data.damage, data.accuracy, data.range);
1786
+ * ```
1787
+ */
1788
+ async getFirearm(version, id) {
1789
+ return this.client.get(
1790
+ `/v1/game-stats/versions/${encodeURIComponent(version)}/firearms/${encodeURIComponent(id)}`
1791
+ );
1792
+ }
1793
+ };
1794
+
1795
+ // src/resources/ammunition.ts
1796
+ var AmmunitionResource = class {
1797
+ constructor(client) {
1798
+ this.client = client;
1799
+ }
1800
+ /**
1801
+ * List ammunition with optional filters and pagination.
1802
+ *
1803
+ * @param params - Optional query parameters for filtering by caliber, bullet type, etc.
1804
+ * @returns A paginated list of ammunition loads.
1805
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1806
+ *
1807
+ * @example
1808
+ * ```typescript
1809
+ * const result = await client.ammunition.list({
1810
+ * caliber_id: '9x19mm-parabellum',
1811
+ * bullet_type: 'hollow-point',
1812
+ * per_page: 25,
1813
+ * });
1814
+ * ```
1815
+ */
1816
+ async list(params) {
1817
+ return this.client.getPaginated("/v1/ammunition", params);
1818
+ }
1819
+ /**
1820
+ * Auto-paginate through all ammunition matching the given filters.
1821
+ *
1822
+ * Returns an async iterator that fetches pages on demand, yielding
1823
+ * individual {@link Ammunition} objects.
1824
+ *
1825
+ * @param params - Optional query parameters for filtering and sorting.
1826
+ * @returns An async iterable iterator yielding individual ammunition loads.
1827
+ *
1828
+ * @example
1829
+ * ```typescript
1830
+ * for await (const ammo of client.ammunition.listAutoPaging({ caliber_id: '45-acp' })) {
1831
+ * console.log(ammo.name, ammo.bulletWeightGrains);
1832
+ * }
1833
+ * ```
1834
+ */
1835
+ async *listAutoPaging(params) {
1836
+ let page = params?.page ?? 1;
1837
+ while (true) {
1838
+ const result = await this.list({ ...params, page });
1839
+ for (const item of result.data) {
1840
+ yield item;
1841
+ }
1842
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1843
+ page++;
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Get a single ammunition load by its slug or ID.
1848
+ *
1849
+ * @param id - The ammunition slug (e.g. `"9mm-federal-hst-124gr"`) or numeric ID.
1850
+ * @returns The full ammunition record with specifications.
1851
+ * @throws {NotFoundError} If no ammunition matches the given identifier.
1852
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1853
+ *
1854
+ * @example
1855
+ * ```typescript
1856
+ * const { data } = await client.ammunition.get('9mm-federal-hst-124gr');
1857
+ * console.log(data.name, data.muzzleVelocityFps, data.bulletWeightGrains);
1858
+ * ```
1859
+ */
1860
+ async get(id) {
1861
+ return this.client.get(`/v1/ammunition/${encodeURIComponent(id)}`);
1862
+ }
1863
+ /**
1864
+ * Get a bullet profile SVG image for an ammunition load.
1865
+ *
1866
+ * Returns a raw SVG string representing the bullet projectile shape.
1867
+ * This endpoint returns raw SVG content, not the standard JSON envelope.
1868
+ *
1869
+ * @param id - The ammunition slug or ID.
1870
+ * @returns The raw SVG string of the bullet profile.
1871
+ * @throws {NotFoundError} If the ammunition does not exist.
1872
+ *
1873
+ * @example
1874
+ * ```typescript
1875
+ * const svg = await client.ammunition.getBulletSvg('9mm-federal-hst-124gr');
1876
+ * // Insert into DOM: element.innerHTML = svg;
1877
+ * ```
1878
+ */
1879
+ async getBulletSvg(id) {
1880
+ const response = await this.client.get(
1881
+ `/v1/ammunition/${encodeURIComponent(id)}/bullet.svg`
1882
+ );
1883
+ return response.data;
1884
+ }
1885
+ /**
1886
+ * Get ballistic trajectory data for an ammunition load.
1887
+ *
1888
+ * @param id - The ammunition slug or ID.
1889
+ * @param params - Optional barrel length and distance parameters.
1890
+ * @returns Ballistic trajectory data at specified distances.
1891
+ * @throws {NotFoundError} If the ammunition does not exist.
1892
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1893
+ *
1894
+ * @example
1895
+ * ```typescript
1896
+ * const { data } = await client.ammunition.ballistics('9mm-federal-hst-124gr', {
1897
+ * barrel_length_mm: 102,
1898
+ * distances: '0,25,50,100,200',
1899
+ * });
1900
+ * for (const point of data.trajectory) {
1901
+ * console.log(`${point.distanceM}m: ${point.velocityFps} fps, ${point.energyFtLbs} ft-lbs`);
1902
+ * }
1903
+ * ```
1904
+ */
1905
+ async ballistics(id, params) {
1906
+ return this.client.get(
1907
+ `/v1/ammunition/${encodeURIComponent(id)}/ballistics`,
1908
+ params
1909
+ );
1910
+ }
1911
+ };
1912
+
1913
+ // src/resources/countries.ts
1914
+ var CountriesResource = class {
1915
+ constructor(client) {
1916
+ this.client = client;
1917
+ }
1918
+ /**
1919
+ * List all countries that have adopted at least one firearm.
1920
+ *
1921
+ * @returns An array of country records with codes and names.
1922
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1923
+ *
1924
+ * @example
1925
+ * ```typescript
1926
+ * const { data } = await client.countries.list();
1927
+ * for (const country of data) {
1928
+ * console.log(country.code, country.name);
1929
+ * }
1930
+ * ```
1931
+ */
1932
+ async list() {
1933
+ return this.client.get("/v1/countries");
1934
+ }
1935
+ /**
1936
+ * Get the full firearm arsenal for a specific country.
1937
+ *
1938
+ * Returns all firearms adopted by the country, grouped by usage type
1939
+ * (military, law enforcement, etc.).
1940
+ *
1941
+ * @param code - The country code (e.g. `"US"`, `"GB"`, `"DE"`).
1942
+ * @returns The country's arsenal data with firearms grouped by adoption type.
1943
+ * @throws {NotFoundError} If the country code is not found in the database.
1944
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1945
+ *
1946
+ * @example
1947
+ * ```typescript
1948
+ * const { data } = await client.countries.getArsenal('US');
1949
+ * console.log(`${data.country.name} has ${data.totalFirearms} firearms`);
1950
+ * for (const group of data.groups) {
1951
+ * console.log(`${group.type}: ${group.firearms.length} firearms`);
1952
+ * }
1953
+ * ```
1954
+ */
1955
+ async getArsenal(code) {
1956
+ return this.client.get(`/v1/countries/${encodeURIComponent(code)}/arsenal`);
1957
+ }
1958
+ };
1959
+
1960
+ // src/resources/conflicts.ts
1961
+ var ConflictsResource = class {
1962
+ constructor(client) {
1963
+ this.client = client;
1964
+ }
1965
+ /**
1966
+ * List all military conflicts in the database.
1967
+ *
1968
+ * Returns conflicts with metadata including name, date range, and
1969
+ * participating nations. Use the conflict identifiers with
1970
+ * `client.firearms.byConflict()` to find firearms used in a specific conflict.
1971
+ *
1972
+ * @returns An array of all conflict records.
1973
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1974
+ *
1975
+ * @example
1976
+ * ```typescript
1977
+ * const { data } = await client.conflicts.list();
1978
+ * for (const conflict of data) {
1979
+ * console.log(`${conflict.name} (${conflict.startYear}-${conflict.endYear})`);
1980
+ * }
1981
+ *
1982
+ * // Then find firearms used in a conflict:
1983
+ * const wwii = await client.firearms.byConflict({ conflict: 'world-war-ii' });
1984
+ * ```
1985
+ */
1986
+ async list() {
1987
+ return this.client.get("/v1/conflicts");
1988
+ }
1989
+ };
1990
+
1991
+ // src/resources/data-quality.ts
1992
+ var DataQualityResource = class {
1993
+ constructor(client) {
1994
+ this.client = client;
1995
+ }
1996
+ /**
1997
+ * Get overall data coverage statistics for the database.
1998
+ *
1999
+ * Returns per-field and aggregate coverage metrics showing what
2000
+ * percentage of records have data for each field.
2001
+ *
2002
+ * @returns Data coverage statistics including per-field percentages.
2003
+ * @throws {AuthenticationError} If the API key is missing or invalid.
2004
+ *
2005
+ * @example
2006
+ * ```typescript
2007
+ * const { data } = await client.dataQuality.coverage();
2008
+ * console.log(`Overall: ${data.overallCoverage}%`);
2009
+ * for (const field of data.fields) {
2010
+ * console.log(`${field.name}: ${field.coverage}%`);
2011
+ * }
2012
+ * ```
2013
+ */
2014
+ async coverage() {
2015
+ return this.client.get("/v1/data/coverage");
2016
+ }
2017
+ /**
2018
+ * Get firearms with confidence scores below a specified threshold.
2019
+ *
2020
+ * Useful for identifying records that may need additional data
2021
+ * verification or enrichment.
2022
+ *
2023
+ * @param params - Optional threshold and pagination parameters.
2024
+ * @returns A paginated list of firearms with their confidence scores.
2025
+ * @throws {BadRequestError} If the threshold is outside the 0-1 range.
2026
+ * @throws {AuthenticationError} If the API key is missing or invalid.
2027
+ *
2028
+ * @example
2029
+ * ```typescript
2030
+ * const result = await client.dataQuality.confidence({
2031
+ * below: 0.5,
2032
+ * per_page: 25,
2033
+ * });
2034
+ * for (const item of result.data) {
2035
+ * console.log(`${item.name}: confidence ${item.confidenceScore}`);
2036
+ * }
2037
+ * ```
2038
+ */
2039
+ async confidence(params) {
2040
+ return this.client.getPaginated("/v1/data/confidence", params);
2041
+ }
2042
+ };
2043
+
2044
+ // src/resources/favorites.ts
2045
+ var FavoritesResource = class {
2046
+ constructor(client) {
2047
+ this.client = client;
2048
+ }
2049
+ async list(params) {
2050
+ return this.client.getPaginated("/v1/me/favorites", params);
2051
+ }
2052
+ async add(firearmId) {
2053
+ return this.client.post(`/v1/me/favorites/${encodeURIComponent(firearmId)}`);
2054
+ }
2055
+ async remove(firearmId) {
2056
+ return this.client.delete(`/v1/me/favorites/${encodeURIComponent(firearmId)}`);
2057
+ }
2058
+ };
2059
+
2060
+ // src/resources/reports.ts
2061
+ var ReportsResource = class {
2062
+ constructor(client) {
2063
+ this.client = client;
2064
+ }
2065
+ async create(params) {
2066
+ return this.client.post("/v1/me/reports", params);
2067
+ }
2068
+ async list(params) {
2069
+ return this.client.getPaginated("/v1/me/reports", params);
2070
+ }
2071
+ };
2072
+
2073
+ // src/resources/support.ts
2074
+ var SupportResource = class {
2075
+ constructor(client) {
2076
+ this.client = client;
2077
+ }
2078
+ async create(params) {
2079
+ return this.client.post("/v1/me/support", params);
2080
+ }
2081
+ async list(params) {
2082
+ return this.client.getPaginated("/v1/me/support", params);
2083
+ }
2084
+ async get(ticketId) {
2085
+ return this.client.get(`/v1/me/support/${encodeURIComponent(ticketId)}`);
2086
+ }
2087
+ async reply(ticketId, params) {
2088
+ return this.client.post(`/v1/me/support/${encodeURIComponent(ticketId)}/replies`, params);
2089
+ }
2090
+ };
2091
+
2092
+ // src/resources/webhooks.ts
2093
+ var WebhooksResource = class {
2094
+ constructor(client) {
2095
+ this.client = client;
2096
+ }
2097
+ async list(params) {
2098
+ return this.client.getPaginated("/v1/me/webhooks", params);
2099
+ }
2100
+ async create(params) {
2101
+ return this.client.post("/v1/me/webhooks", params);
2102
+ }
2103
+ async get(id) {
2104
+ return this.client.get(`/v1/me/webhooks/${encodeURIComponent(id)}`);
2105
+ }
2106
+ async update(id, params) {
2107
+ return this.client.put(`/v1/me/webhooks/${encodeURIComponent(id)}`, params);
2108
+ }
2109
+ async delete(id) {
2110
+ return this.client.delete(`/v1/me/webhooks/${encodeURIComponent(id)}`);
2111
+ }
2112
+ async test(id) {
2113
+ return this.client.post(`/v1/me/webhooks/${encodeURIComponent(id)}/test`);
2114
+ }
2115
+ };
2116
+
2117
+ // src/resources/usage.ts
2118
+ var UsageResource = class {
2119
+ constructor(client) {
2120
+ this.client = client;
2121
+ }
2122
+ async get(params) {
2123
+ return this.client.get("/v1/me/usage", params);
2124
+ }
2125
+ };
2126
+
2127
+ // src/version.ts
2128
+ var VERSION = "0.1.0";
2129
+
2130
+ // src/client.ts
2131
+ var GunSpec = class {
2132
+ /** Firearms specifications, search, comparison, and related data */
2133
+ firearms;
2134
+ /** Firearm manufacturers and their products */
2135
+ manufacturers;
2136
+ /** Ammunition calibers and cartridge specifications */
2137
+ calibers;
2138
+ /** Firearm categories (pistol, rifle, shotgun, etc.) */
2139
+ categories;
2140
+ /** Aggregate statistics across the database */
2141
+ stats;
2142
+ /** Game development tools — balance reports, tier lists, matchups */
2143
+ game;
2144
+ /** Versioned game stats snapshots for pinning game builds */
2145
+ gameStats;
2146
+ /** Ammunition loads, ballistics, and bullet data */
2147
+ ammunition;
2148
+ /** Countries and their military/law enforcement arsenals */
2149
+ countries;
2150
+ /** Armed conflicts and the firearms used in them */
2151
+ conflicts;
2152
+ /** Data quality metrics (Enterprise tier) */
2153
+ dataQuality;
2154
+ /** User's favorited firearms */
2155
+ favorites;
2156
+ /** Data quality reports */
2157
+ reports;
2158
+ /** Support tickets (paid tiers) */
2159
+ support;
2160
+ /** Webhook endpoint management (studio+ tier) */
2161
+ webhooks;
2162
+ /** API usage statistics */
2163
+ usage;
2164
+ /** The underlying HTTP client (for advanced use) */
2165
+ _client;
2166
+ constructor(options = {}) {
2167
+ this._client = new HttpClient({
2168
+ baseUrl: options.baseURL ?? "https://api.gunspec.io",
2169
+ timeout: options.timeout ?? 3e4,
2170
+ auth: {
2171
+ apiKey: options.apiKey
2172
+ },
2173
+ retry: options.retry,
2174
+ headers: {
2175
+ ...options.defaultHeaders,
2176
+ "User-Agent": `gunspec-sdk/typescript/${VERSION}`,
2177
+ "X-SDK-Version": VERSION,
2178
+ "X-SDK-Language": "typescript"
2179
+ }
2180
+ });
2181
+ this.firearms = new FirearmsResource(this._client);
2182
+ this.manufacturers = new ManufacturersResource(this._client);
2183
+ this.calibers = new CalibersResource(this._client);
2184
+ this.categories = new CategoriesResource(this._client);
2185
+ this.stats = new StatsResource(this._client);
2186
+ this.game = new GameResource(this._client);
2187
+ this.gameStats = new GameStatsResource(this._client);
2188
+ this.ammunition = new AmmunitionResource(this._client);
2189
+ this.countries = new CountriesResource(this._client);
2190
+ this.conflicts = new ConflictsResource(this._client);
2191
+ this.dataQuality = new DataQualityResource(this._client);
2192
+ this.favorites = new FavoritesResource(this._client);
2193
+ this.reports = new ReportsResource(this._client);
2194
+ this.support = new SupportResource(this._client);
2195
+ this.webhooks = new WebhooksResource(this._client);
2196
+ this.usage = new UsageResource(this._client);
2197
+ }
2198
+ };
2199
+
2200
+ // src/core/pagination.ts
2201
+ var Page = class _Page {
2202
+ /** The items on this page. */
2203
+ data;
2204
+ /** Pagination metadata returned by the server. */
2205
+ pagination;
2206
+ /** The request ID for this page's HTTP response. */
2207
+ requestId;
2208
+ /** The base query parameters used to fetch this page (without `page`). */
2209
+ baseQuery;
2210
+ /** The fetcher function used to retrieve subsequent pages. */
2211
+ fetcher;
2212
+ /**
2213
+ * @internal
2214
+ * Consumers should not construct `Page` instances directly. Use the
2215
+ * resource methods on the high-level client instead.
2216
+ */
2217
+ constructor(response, baseQuery, fetcher) {
2218
+ this.data = response.data;
2219
+ this.pagination = response.pagination;
2220
+ this.requestId = response.requestId;
2221
+ this.baseQuery = baseQuery;
2222
+ this.fetcher = fetcher;
2223
+ }
2224
+ /**
2225
+ * Whether there is a next page of results.
2226
+ *
2227
+ * When `totalPages` is available (pro/enterprise tiers) the check is exact.
2228
+ * Otherwise the heuristic is: if the current page returned a full page of
2229
+ * results (i.e. `data.length === limit`), there is likely another page.
2230
+ */
2231
+ hasNextPage() {
2232
+ if (this.pagination.totalPages !== void 0) {
2233
+ return this.pagination.page < this.pagination.totalPages;
2234
+ }
2235
+ return this.data.length >= this.pagination.limit;
2236
+ }
2237
+ /**
2238
+ * Fetch the next page of results.
2239
+ *
2240
+ * @returns A new {@link Page} instance for the next page.
2241
+ * @throws {Error} If there is no next page. Always check
2242
+ * {@link hasNextPage} first.
2243
+ */
2244
+ async getNextPage() {
2245
+ if (!this.hasNextPage()) {
2246
+ throw new Error("No more pages available. Check hasNextPage() before calling getNextPage().");
2247
+ }
2248
+ const nextQuery = {
2249
+ ...this.baseQuery,
2250
+ page: this.pagination.page + 1
2251
+ };
2252
+ const response = await this.fetcher(nextQuery);
2253
+ return new _Page(response, this.baseQuery, this.fetcher);
2254
+ }
2255
+ /**
2256
+ * Fetch the previous page of results.
2257
+ *
2258
+ * @returns A new {@link Page} instance for the previous page.
2259
+ * @throws {Error} If this is already the first page.
2260
+ */
2261
+ async getPreviousPage() {
2262
+ if (this.pagination.page <= 1) {
2263
+ throw new Error("Already on the first page. Cannot navigate to a previous page.");
2264
+ }
2265
+ const prevQuery = {
2266
+ ...this.baseQuery,
2267
+ page: this.pagination.page - 1
2268
+ };
2269
+ const response = await this.fetcher(prevQuery);
2270
+ return new _Page(response, this.baseQuery, this.fetcher);
2271
+ }
2272
+ /**
2273
+ * Async iterator that yields every item across **all** pages, starting
2274
+ * from the current page.
2275
+ *
2276
+ * Fetches subsequent pages on demand (lazily) so memory usage stays
2277
+ * constant regardless of the total result set size.
2278
+ *
2279
+ * @example
2280
+ * ```ts
2281
+ * const firstPage = await client.firearms.list({ limit: 50 });
2282
+ * for await (const firearm of firstPage) {
2283
+ * // Iterates page 1, then auto-fetches page 2, 3, ... until exhausted.
2284
+ * console.log(firearm.name);
2285
+ * }
2286
+ * ```
2287
+ */
2288
+ async *[Symbol.asyncIterator]() {
2289
+ let current = this;
2290
+ while (true) {
2291
+ for (const item of current.data) {
2292
+ yield item;
2293
+ }
2294
+ if (!current.hasNextPage()) {
2295
+ break;
2296
+ }
2297
+ current = await current.getNextPage();
2298
+ }
2299
+ }
2300
+ };
2301
+ function createPage(response, baseQuery, fetcher) {
2302
+ return new Page(response, baseQuery, fetcher);
2303
+ }
2304
+
2305
+ export { APIError, AmmunitionResource, AuthenticationError, BadRequestError, CalibersResource, CategoriesResource, ConflictsResource, ConnectionError, CountriesResource, DataQualityResource, FavoritesResource, FirearmsResource, GameResource, GameStatsResource, GunSpec, GunSpecError, HttpClient, InternalServerError, ManufacturersResource, NotFoundError, Page, PermissionError, RateLimitError, ReportsResource, StatsResource, SupportResource, TimeoutError, UsageResource, VERSION, WebhooksResource, buildAuthHeaders, createAPIError, createPage, resolveApiKey };
2306
+ //# sourceMappingURL=index.js.map
2307
+ //# sourceMappingURL=index.js.map