zlient 1.0.3 → 1.0.5

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.cjs CHANGED
@@ -5,17 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
- };
11
- var __export = (all) => {
12
- let target = {};
13
- for (var name in all) __defProp(target, name, {
14
- get: all[name],
15
- enumerable: true
16
- });
17
- return target;
18
- };
19
8
  var __copyProps = (to, from, except, desc) => {
20
9
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
21
10
  key = keys[i];
@@ -30,7 +19,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
19
  value: mod,
31
20
  enumerable: true
32
21
  }) : target, mod));
33
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
22
 
35
23
  //#endregion
36
24
  let zod = require("zod");
@@ -46,7 +34,19 @@ const HTTPMethod = {
46
34
  HEAD: "HEAD",
47
35
  OPTIONS: "OPTIONS"
48
36
  };
49
- var ApiError = class extends Error {
37
+ /**
38
+ * Custom error class for API-related errors.
39
+ * Includes HTTP status codes, response details, and validation errors.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * throw new ApiError('Invalid request', {
44
+ * status: 400,
45
+ * details: { field: 'email', message: 'Invalid format' }
46
+ * });
47
+ * ```
48
+ */
49
+ var ApiError = class ApiError extends Error {
50
50
  status;
51
51
  details;
52
52
  zodError;
@@ -57,14 +57,62 @@ var ApiError = class extends Error {
57
57
  this.details = options?.details;
58
58
  this.cause = options?.cause;
59
59
  this.zodError = options?.zodError;
60
+ if (Error.captureStackTrace) Error.captureStackTrace(this, ApiError);
61
+ }
62
+ /**
63
+ * Check if this is a validation error (has zodError)
64
+ */
65
+ isValidationError() {
66
+ return !!this.zodError;
67
+ }
68
+ /**
69
+ * Check if this is a client error (4xx status)
70
+ */
71
+ isClientError() {
72
+ return !!this.status && this.status >= 400 && this.status < 500;
73
+ }
74
+ /**
75
+ * Check if this is a server error (5xx status)
76
+ */
77
+ isServerError() {
78
+ return !!this.status && this.status >= 500;
79
+ }
80
+ /**
81
+ * Get a formatted error message with all available details
82
+ */
83
+ toJSON() {
84
+ return {
85
+ name: this.name,
86
+ message: this.message,
87
+ status: this.status,
88
+ details: this.details,
89
+ zodError: this.zodError?.issues,
90
+ stack: this.stack
91
+ };
60
92
  }
61
93
  };
94
+ /**
95
+ * Schema for paginated responses
96
+ */
62
97
  const PaginationSchema = zod.z.object({
63
98
  items: zod.z.array(zod.z.unknown()),
64
99
  total: zod.z.number().int().nonnegative(),
65
100
  page: zod.z.number().int().nonnegative(),
66
101
  pageSize: zod.z.number().int().positive()
67
102
  });
103
+ /**
104
+ * Converts query parameters to a URL query string.
105
+ * Filters out undefined values automatically.
106
+ *
107
+ * @param q - Query parameters as URLSearchParams or object
108
+ * @returns Query string with leading '?' or empty string
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * toQueryString({ page: 1, filter: 'active' }) // "?page=1&filter=active"
113
+ * toQueryString({ optional: undefined }) // ""
114
+ * ```
115
+ */
68
116
  function toQueryString(q) {
69
117
  if (!q) return "";
70
118
  if (q instanceof URLSearchParams) {
@@ -81,48 +129,93 @@ function toQueryString(q) {
81
129
 
82
130
  //#endregion
83
131
  //#region lib/auth.ts
84
- var auth_exports = /* @__PURE__ */ __export({
85
- ApiKeyAuth: () => ApiKeyAuth,
86
- BearerTokenAuth: () => BearerTokenAuth,
87
- NoAuth: () => NoAuth
88
- });
89
- var NoAuth, ApiKeyAuth, BearerTokenAuth;
90
- var init_auth = __esm({ "lib/auth.ts": (() => {
91
- NoAuth = class {
92
- async apply() {}
93
- };
94
- ApiKeyAuth = class {
95
- constructor(opts) {
96
- this.opts = opts;
97
- }
98
- apply({ url, init }) {
99
- if (this.opts.header) init.headers = {
100
- ...init.headers,
101
- [this.opts.header]: this.opts.value
102
- };
103
- else if (this.opts.query) {
104
- const u = new URL(url);
105
- u.searchParams.set(this.opts.query, this.opts.value);
106
- init.__urlOverride = u.toString();
107
- }
108
- }
109
- };
110
- BearerTokenAuth = class {
111
- constructor(getToken) {
112
- this.getToken = getToken;
113
- }
114
- async apply({ init }) {
115
- const token = await this.getToken();
116
- init.headers = {
117
- ...init.headers,
118
- Authorization: `Bearer ${token}`
119
- };
132
+ /**
133
+ * No-op authentication provider (no authentication applied).
134
+ * Use this when you don't need authentication.
135
+ */
136
+ var NoAuth = class {
137
+ async apply() {}
138
+ };
139
+ /**
140
+ * API Key authentication provider.
141
+ * Supports both header-based and query parameter-based authentication.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Header-based
146
+ * const auth = new ApiKeyAuth({ header: 'X-API-Key', value: 'secret' });
147
+ *
148
+ * // Query parameter-based
149
+ * const auth = new ApiKeyAuth({ query: 'apiKey', value: 'secret' });
150
+ * ```
151
+ */
152
+ var ApiKeyAuth = class {
153
+ constructor(opts) {
154
+ this.opts = opts;
155
+ if (!opts.header && !opts.query) throw new Error("ApiKeyAuth requires either \"header\" or \"query\" option");
156
+ if (opts.header && opts.query) throw new Error("ApiKeyAuth cannot use both \"header\" and \"query\" options");
157
+ }
158
+ apply({ url, init }) {
159
+ if (this.opts.header) init.headers = {
160
+ ...init.headers,
161
+ [this.opts.header]: this.opts.value
162
+ };
163
+ else if (this.opts.query) {
164
+ const u = new URL(url);
165
+ u.searchParams.set(this.opts.query, this.opts.value);
166
+ init.__urlOverride = u.toString();
120
167
  }
121
- };
122
- }) });
168
+ }
169
+ };
170
+ /**
171
+ * Bearer token authentication provider.
172
+ * Supports both static tokens and dynamic token fetching (e.g., for OAuth2 refresh).
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // Static token
177
+ * const auth = new BearerTokenAuth(() => 'my-token');
178
+ *
179
+ * // Dynamic token with refresh
180
+ * const auth = new BearerTokenAuth(async () => {
181
+ * return await refreshAccessToken();
182
+ * });
183
+ * ```
184
+ */
185
+ var BearerTokenAuth = class {
186
+ constructor(getToken) {
187
+ this.getToken = getToken;
188
+ }
189
+ async apply({ init }) {
190
+ const token = await this.getToken();
191
+ if (!token) throw new Error("BearerTokenAuth: token is empty or undefined");
192
+ init.headers = {
193
+ ...init.headers,
194
+ Authorization: `Bearer ${token}`
195
+ };
196
+ }
197
+ };
123
198
 
124
199
  //#endregion
125
200
  //#region lib/validation.ts
201
+ /**
202
+ * Safely parse data with a Zod schema without throwing.
203
+ * Returns a result object with success status and data or error.
204
+ *
205
+ * @param schema - Zod schema to validate against
206
+ * @param data - Data to validate
207
+ * @returns Result object with success flag and data/error
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const result = safeParse(UserSchema, userData);
212
+ * if (result.success) {
213
+ * console.log(result.data);
214
+ * } else {
215
+ * console.error(result.error);
216
+ * }
217
+ * ```
218
+ */
126
219
  function safeParse(schema, data) {
127
220
  const res = schema.safeParse(data);
128
221
  if (res.success) return {
@@ -134,14 +227,220 @@ function safeParse(schema, data) {
134
227
  error: res.error
135
228
  };
136
229
  }
230
+ /**
231
+ * Parse data with a Zod schema, throwing an ApiError on validation failure.
232
+ * Use this when you want to fail fast on invalid data.
233
+ *
234
+ * @param schema - Zod schema to validate against
235
+ * @param data - Data to validate
236
+ * @returns Validated and typed data
237
+ * @throws {ApiError} If validation fails
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * try {
242
+ * const user = parseOrThrow(UserSchema, userData);
243
+ * console.log(user);
244
+ * } catch (error) {
245
+ * if (error instanceof ApiError && error.zodError) {
246
+ * console.error('Validation failed:', error.zodError.issues);
247
+ * }
248
+ * }
249
+ * ```
250
+ */
137
251
  function parseOrThrow(schema, data) {
138
252
  const res = schema.safeParse(data);
139
253
  if (!res.success) throw new ApiError("Response validation failed", { zodError: res.error });
140
254
  return res.data;
141
255
  }
142
256
 
257
+ //#endregion
258
+ //#region lib/logger.ts
259
+ /**
260
+ * Log levels for structured logging.
261
+ */
262
+ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
263
+ LogLevel$1["DEBUG"] = "debug";
264
+ LogLevel$1["INFO"] = "info";
265
+ LogLevel$1["WARN"] = "warn";
266
+ LogLevel$1["ERROR"] = "error";
267
+ return LogLevel$1;
268
+ }({});
269
+ /**
270
+ * Default console logger implementation.
271
+ * Formats log entries as JSON for easy parsing.
272
+ */
273
+ var ConsoleLogger = class {
274
+ constructor(minLevel = LogLevel.INFO) {
275
+ this.minLevel = minLevel;
276
+ }
277
+ log(entry) {
278
+ const levels = [
279
+ LogLevel.DEBUG,
280
+ LogLevel.INFO,
281
+ LogLevel.WARN,
282
+ LogLevel.ERROR
283
+ ];
284
+ if (levels.indexOf(entry.level) < levels.indexOf(this.minLevel)) return;
285
+ const output = {
286
+ ...entry,
287
+ error: entry.error ? {
288
+ message: entry.error.message,
289
+ stack: entry.error.stack,
290
+ name: entry.error.name
291
+ } : void 0
292
+ };
293
+ switch (entry.level) {
294
+ case LogLevel.DEBUG:
295
+ console.debug(JSON.stringify(output));
296
+ break;
297
+ case LogLevel.INFO:
298
+ console.info(JSON.stringify(output));
299
+ break;
300
+ case LogLevel.WARN:
301
+ console.warn(JSON.stringify(output));
302
+ break;
303
+ case LogLevel.ERROR:
304
+ console.error(JSON.stringify(output));
305
+ break;
306
+ }
307
+ }
308
+ };
309
+ /**
310
+ * No-op logger that discards all log entries.
311
+ * Use this in production if you don't want any logging.
312
+ */
313
+ var NoOpLogger = class {
314
+ log(_entry) {}
315
+ };
316
+ /**
317
+ * Utility class for creating structured log entries.
318
+ */
319
+ var LoggerUtil = class {
320
+ constructor(logger) {
321
+ this.logger = logger;
322
+ }
323
+ debug(message, context) {
324
+ this.logger.log({
325
+ level: LogLevel.DEBUG,
326
+ message,
327
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
328
+ context
329
+ });
330
+ }
331
+ info(message, context) {
332
+ this.logger.log({
333
+ level: LogLevel.INFO,
334
+ message,
335
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
336
+ context
337
+ });
338
+ }
339
+ warn(message, context) {
340
+ this.logger.log({
341
+ level: LogLevel.WARN,
342
+ message,
343
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
344
+ context
345
+ });
346
+ }
347
+ error(message, error, context) {
348
+ this.logger.log({
349
+ level: LogLevel.ERROR,
350
+ message,
351
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
352
+ context,
353
+ error
354
+ });
355
+ }
356
+ };
357
+
358
+ //#endregion
359
+ //#region lib/metrics.ts
360
+ /**
361
+ * No-op metrics collector that discards all metrics.
362
+ */
363
+ var NoOpMetricsCollector = class {
364
+ collect(_metrics) {}
365
+ };
366
+ /**
367
+ * In-memory metrics collector for testing and development.
368
+ * Stores metrics in memory with configurable retention.
369
+ */
370
+ var InMemoryMetricsCollector = class {
371
+ metrics = [];
372
+ maxEntries;
373
+ constructor(maxEntries = 1e3) {
374
+ this.maxEntries = maxEntries;
375
+ }
376
+ collect(metrics) {
377
+ this.metrics.push(metrics);
378
+ if (this.metrics.length > this.maxEntries) this.metrics.shift();
379
+ }
380
+ /**
381
+ * Get all collected metrics.
382
+ */
383
+ getMetrics() {
384
+ return [...this.metrics];
385
+ }
386
+ /**
387
+ * Get metrics summary statistics.
388
+ */
389
+ getSummary() {
390
+ if (this.metrics.length === 0) return {
391
+ total: 0,
392
+ successful: 0,
393
+ failed: 0,
394
+ avgDurationMs: 0,
395
+ minDurationMs: 0,
396
+ maxDurationMs: 0
397
+ };
398
+ const successful = this.metrics.filter((m) => m.success).length;
399
+ const durations = this.metrics.map((m) => m.durationMs);
400
+ const sum = durations.reduce((a, b) => a + b, 0);
401
+ return {
402
+ total: this.metrics.length,
403
+ successful,
404
+ failed: this.metrics.length - successful,
405
+ avgDurationMs: sum / this.metrics.length,
406
+ minDurationMs: Math.min(...durations),
407
+ maxDurationMs: Math.max(...durations)
408
+ };
409
+ }
410
+ /**
411
+ * Clear all collected metrics.
412
+ */
413
+ clear() {
414
+ this.metrics = [];
415
+ }
416
+ };
417
+ /**
418
+ * Console-based metrics collector for debugging.
419
+ */
420
+ var ConsoleMetricsCollector = class {
421
+ collect(metrics) {
422
+ console.log("[METRICS]", JSON.stringify(metrics));
423
+ }
424
+ };
425
+
143
426
  //#endregion
144
427
  //#region lib/http/HttpClient.ts
428
+ /**
429
+ * HTTP client with built-in retry logic, authentication, and interceptors.
430
+ * Supports multiple base URLs, type-safe requests, and comprehensive error handling.
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * const client = new HttpClient({
435
+ * baseUrls: { default: 'https://api.example.com' },
436
+ * headers: { 'Content-Type': 'application/json' },
437
+ * retry: { maxRetries: 3, baseDelayMs: 1000 },
438
+ * timeout: { requestTimeoutMs: 30000 }
439
+ * });
440
+ *
441
+ * const { data } = await client.request('GET', '/users', undefined, { query: { page: 1 } });
442
+ * ```
443
+ */
145
444
  var HttpClient = class {
146
445
  fetchImpl;
147
446
  baseUrls;
@@ -150,9 +449,19 @@ var HttpClient = class {
150
449
  retry;
151
450
  timeoutMs;
152
451
  auth;
452
+ logger;
453
+ metrics;
454
+ /**
455
+ * Creates a new HTTP client instance.
456
+ *
457
+ * @param opts - Client configuration options
458
+ * @throws {Error} If no fetch implementation is available
459
+ */
153
460
  constructor(opts) {
154
461
  this.fetchImpl = opts.fetch ?? globalThis.fetch?.bind(globalThis);
155
462
  if (!this.fetchImpl) throw new Error("No fetch implementation found. Pass one via options.fetch.");
463
+ if (!opts.baseUrls || typeof opts.baseUrls !== "object") throw new Error("baseUrls must be provided and must be an object");
464
+ if (!opts.baseUrls.default) throw new Error("baseUrls must include a \"default\" key");
156
465
  this.baseUrls = opts.baseUrls;
157
466
  this.headers = opts.headers ?? { "Content-Type": "application/json" };
158
467
  this.interceptors = opts.interceptors ?? {};
@@ -162,21 +471,47 @@ var HttpClient = class {
162
471
  jitter: .2,
163
472
  retryMethods: ["GET", "HEAD"]
164
473
  };
474
+ if (this.retry.maxRetries < 0) throw new Error("retry.maxRetries must be non-negative");
475
+ if (this.retry.baseDelayMs < 0) throw new Error("retry.baseDelayMs must be non-negative");
476
+ if (this.retry.jitter !== void 0 && (this.retry.jitter < 0 || this.retry.jitter > 1)) throw new Error("retry.jitter must be between 0 and 1");
165
477
  this.timeoutMs = opts.timeout?.requestTimeoutMs;
166
- this.auth = opts["auth"] ?? new (init_auth(), __toCommonJS(auth_exports)).NoAuth();
478
+ if (this.timeoutMs !== void 0 && this.timeoutMs < 0) throw new Error("timeout.requestTimeoutMs must be non-negative");
479
+ this.auth = opts["auth"] ?? new NoAuth();
480
+ this.logger = new LoggerUtil(opts.logger ?? new NoOpLogger());
481
+ this.metrics = opts.metrics ?? new NoOpMetricsCollector();
167
482
  }
483
+ /**
484
+ * Set or update the authentication provider.
485
+ *
486
+ * @param auth - Authentication provider instance
487
+ * @example
488
+ * ```ts
489
+ * client.setAuth(new BearerTokenAuth(() => getToken()));
490
+ * ```
491
+ */
168
492
  setAuth(auth) {
169
493
  this.auth = auth;
170
494
  }
171
495
  resolveBaseUrl(key) {
172
496
  const k = key || "default";
173
497
  const url = this.baseUrls[k];
174
- if (!url) throw new Error(`Unknown baseUrl key: ${k}`);
498
+ if (!url) {
499
+ const availableKeys = Object.keys(this.baseUrls).join(", ");
500
+ throw new Error(`Unknown baseUrl key: "${k}". Available keys: ${availableKeys}`);
501
+ }
175
502
  return url.replace(/\/$/, "");
176
503
  }
504
+ /**
505
+ * Sleep for a specified duration (used for retry backoff).
506
+ * @private
507
+ */
177
508
  sleep(ms) {
178
509
  return new Promise((res) => setTimeout(res, ms));
179
510
  }
511
+ /**
512
+ * Execute a function with retry logic and exponential backoff.
513
+ * @private
514
+ */
180
515
  async withRetry(fn, canRetry) {
181
516
  let attempt = 0;
182
517
  const { maxRetries, baseDelayMs, jitter = .2 } = this.retry;
@@ -193,12 +528,20 @@ var HttpClient = class {
193
528
  attempt++;
194
529
  }
195
530
  }
531
+ /**
532
+ * Run all registered before-request hooks.
533
+ * @private
534
+ */
196
535
  async runBeforeHooks(url, init) {
197
536
  for (const h of this.interceptors.beforeRequest ?? []) await h({
198
537
  url,
199
538
  init
200
539
  });
201
540
  }
541
+ /**
542
+ * Run all registered after-response hooks.
543
+ * @private
544
+ */
202
545
  async runAfterHooks(req, res, parsed) {
203
546
  for (const h of this.interceptors.afterResponse ?? []) await h({
204
547
  request: req,
@@ -206,8 +549,33 @@ var HttpClient = class {
206
549
  parsed
207
550
  });
208
551
  }
552
+ /**
553
+ * Make an HTTP request with automatic retry, authentication, and validation.
554
+ *
555
+ * @param method - HTTP method (GET, POST, PUT, etc.)
556
+ * @param path - Request path (will be appended to base URL)
557
+ * @param body - Request body (will be JSON.stringify'd if Content-Type is json)
558
+ * @param options - Additional request options (headers, query params, etc.)
559
+ * @returns Promise resolving to response data and Response object
560
+ * @throws {ApiError} If request fails or response validation fails
561
+ *
562
+ * @example
563
+ * ```ts
564
+ * const { data, response } = await client.request('GET', '/users', undefined, {
565
+ * query: { page: 1, limit: 10 },
566
+ * headers: { 'X-Custom': 'value' }
567
+ * });
568
+ * ```
569
+ */
209
570
  async request(method, path, body, options) {
571
+ const startTime = Date.now();
210
572
  let url = `${this.resolveBaseUrl(options?.baseUrlKey)}${path}${toQueryString(options?.query)}`;
573
+ this.logger.debug("HTTP request initiated", {
574
+ method,
575
+ path,
576
+ baseUrlKey: options?.baseUrlKey,
577
+ hasBody: body !== void 0
578
+ });
211
579
  const headers = {
212
580
  ...this.headers,
213
581
  ...options?.headers ?? {}
@@ -217,7 +585,7 @@ var HttpClient = class {
217
585
  const init = {
218
586
  method,
219
587
  headers,
220
- body: body != null ? headers["Content-Type"]?.includes("json") ? JSON.stringify(body) : body : void 0,
588
+ body: body != null ? headers["Content-Type"]?.includes("json") ? JSON.stringify(body) : String(body) : void 0,
221
589
  signal
222
590
  };
223
591
  await this.auth.apply({
@@ -229,16 +597,21 @@ var HttpClient = class {
229
597
  await this.runBeforeHooks(url, init);
230
598
  const doFetch = async () => {
231
599
  let timeoutId;
232
- if (this.timeoutMs && !options?.signal) timeoutId = setTimeout(() => controller.abort(new ApiError("Request timeout", { status: 0 })), this.timeoutMs);
600
+ if (this.timeoutMs && !options?.signal) timeoutId = setTimeout(() => {
601
+ const timeoutError = /* @__PURE__ */ new Error("Request timeout");
602
+ timeoutError.name = "TimeoutError";
603
+ controller.abort(timeoutError);
604
+ }, this.timeoutMs);
233
605
  try {
234
606
  const req = new Request(url, init);
235
- console.log("HTTP Request:", req.method, req.url, req);
236
607
  const res = await this.fetchImpl(req);
237
608
  if (!res.ok) {
238
609
  let text = "";
239
610
  try {
240
611
  text = await res.text();
241
- } catch {}
612
+ } catch (readError) {
613
+ text = `Failed to read response: ${readError instanceof Error ? readError.message : String(readError)}`;
614
+ }
242
615
  throw new ApiError(`HTTP ${res.status}: ${res.statusText}`, {
243
616
  status: res.status,
244
617
  details: text
@@ -246,33 +619,151 @@ var HttpClient = class {
246
619
  }
247
620
  const data = (res.headers.get("content-type") || "").includes("json") ? await res.json() : await res.text();
248
621
  await this.runAfterHooks(new Request(url, init), res, data);
622
+ const duration = Date.now() - startTime;
623
+ this.logger.info("HTTP request successful", {
624
+ method,
625
+ url,
626
+ status: res.status,
627
+ durationMs: duration
628
+ });
629
+ this.metrics.collect({
630
+ method,
631
+ path,
632
+ status: res.status,
633
+ durationMs: duration,
634
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
635
+ success: true
636
+ });
249
637
  return {
250
638
  data,
251
639
  response: res
252
640
  };
641
+ } catch (error) {
642
+ const duration = Date.now() - startTime;
643
+ this.logger.error("HTTP request failed", error, {
644
+ method,
645
+ url,
646
+ durationMs: duration
647
+ });
648
+ this.metrics.collect({
649
+ method,
650
+ path,
651
+ status: error instanceof ApiError ? error.status : void 0,
652
+ durationMs: duration,
653
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
654
+ success: false,
655
+ error: error instanceof Error ? error.message : String(error)
656
+ });
657
+ throw error;
253
658
  } finally {
254
659
  if (timeoutId) clearTimeout(timeoutId);
255
660
  }
256
661
  };
257
662
  const canRetry = ({ response, error }) => {
663
+ if (error && typeof error === "object" && "name" in error) {
664
+ const errorName = error.name;
665
+ if (errorName === "AbortError" || errorName === "TimeoutError") return false;
666
+ }
258
667
  if (error instanceof ApiError && error.status && error.status >= 500) return true;
259
- if (error?.name === "AbortError") return false;
260
- if (!error?.status && !response) return true;
668
+ if (error && !response) return true;
261
669
  return false;
262
670
  };
263
671
  if (!this.retry.retryMethods?.includes(method)) return doFetch();
264
672
  return this.withRetry(doFetch, canRetry);
265
673
  }
674
+ /**
675
+ * Convenience method for GET requests.
676
+ *
677
+ * @example
678
+ * ```ts
679
+ * const { data } = await client.get('/users', { query: { page: 1 } });
680
+ * ```
681
+ */
682
+ async get(path, options) {
683
+ return this.request("GET", path, void 0, options);
684
+ }
685
+ /**
686
+ * Convenience method for POST requests.
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * const { data } = await client.post('/users', { name: 'John' });
691
+ * ```
692
+ */
693
+ async post(path, body, options) {
694
+ return this.request("POST", path, body, options);
695
+ }
696
+ /**
697
+ * Convenience method for PUT requests.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * const { data } = await client.put('/users/1', { name: 'John Updated' });
702
+ * ```
703
+ */
704
+ async put(path, body, options) {
705
+ return this.request("PUT", path, body, options);
706
+ }
707
+ /**
708
+ * Convenience method for PATCH requests.
709
+ *
710
+ * @example
711
+ * ```ts
712
+ * const { data } = await client.patch('/users/1', { name: 'John' });
713
+ * ```
714
+ */
715
+ async patch(path, body, options) {
716
+ return this.request("PATCH", path, body, options);
717
+ }
718
+ /**
719
+ * Convenience method for DELETE requests.
720
+ *
721
+ * @example
722
+ * ```ts
723
+ * const { data } = await client.delete('/users/1');
724
+ * ```
725
+ */
726
+ async delete(path, options) {
727
+ return this.request("DELETE", path, void 0, options);
728
+ }
266
729
  };
267
730
 
268
731
  //#endregion
269
732
  //#region lib/endpoint/BaseEndpoint.ts
270
733
  /**
271
- * Generic, strongly-typed endpoint with Zod schemas for request and response.
734
+ * Generic, strongly-typed endpoint with Zod schemas for request and response validation.
735
+ * Extend this class to create type-safe API endpoints.
736
+ *
737
+ * @template ReqSchema - Zod schema for request validation
738
+ * @template ResSchema - Zod schema for response validation
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * const UserSchema = z.object({ id: z.number(), name: z.string() });
743
+ * const CreateUserSchema = z.object({ name: z.string() });
744
+ *
745
+ * class GetUser extends BaseEndpoint<typeof CreateUserSchema, typeof UserSchema> {
746
+ * protected method = 'GET' as const;
747
+ * protected path = (args: z.infer<typeof CreateUserSchema>) => `/users/${args.id}`;
748
+ *
749
+ * constructor(client: HttpClient) {
750
+ * super(client, {
751
+ * requestSchema: CreateUserSchema,
752
+ * responseSchema: UserSchema
753
+ * });
754
+ * }
755
+ * }
756
+ * ```
272
757
  */
273
758
  var BaseEndpoint = class {
759
+ /** Optional request schema for validation */
274
760
  requestSchema;
761
+ /** Response schema for validation */
275
762
  responseSchema;
763
+ /**
764
+ * @param client - HttpClient instance
765
+ * @param cfg - Configuration with request and response schemas
766
+ */
276
767
  constructor(client, cfg) {
277
768
  this.client = client;
278
769
  this.requestSchema = cfg.requestSchema;
@@ -280,6 +771,19 @@ var BaseEndpoint = class {
280
771
  }
281
772
  /**
282
773
  * Call the endpoint with strong typing derived from schemas.
774
+ * Validates request data before sending and response data after receiving.
775
+ *
776
+ * @param args - Request arguments (typed by ReqSchema)
777
+ * @param options - Additional request options
778
+ * @returns Promise resolving to validated response data (typed by ResSchema)
779
+ * @throws {ZodError} If request validation fails
780
+ * @throws {ApiError} If response validation fails or request fails
781
+ *
782
+ * @example
783
+ * ```ts
784
+ * const endpoint = new GetUser(client);
785
+ * const user = await endpoint.call({ id: 1 });
786
+ * ```
283
787
  */
284
788
  async call(args, options) {
285
789
  if (this.requestSchema) {
@@ -287,10 +791,16 @@ var BaseEndpoint = class {
287
791
  if (!parsed.success) throw parsed.error;
288
792
  }
289
793
  const path = typeof this.path === "function" ? this.path(args) : this.path;
290
- const body = this.method === "GET" || this.method === "HEAD" ? void 0 : args;
794
+ const body = this.method !== "GET" && this.method !== "HEAD" ? args : void 0;
795
+ const queryFromArgs = args?.query;
796
+ let mergedQuery;
797
+ if (options?.query || queryFromArgs) mergedQuery = {
798
+ ...typeof options?.query === "object" && !(options?.query instanceof URLSearchParams) ? options.query : {},
799
+ ...typeof queryFromArgs === "object" && queryFromArgs !== null ? queryFromArgs : {}
800
+ };
291
801
  const { data } = await this.client.request(this.method, path, body, {
292
802
  ...options,
293
- query: (this.method === "GET" ? args?.query : void 0) ?? options?.query
803
+ query: mergedQuery && Object.keys(mergedQuery).length > 0 ? mergedQuery : options?.query
294
804
  });
295
805
  return parseOrThrow(this.responseSchema, data);
296
806
  }
@@ -298,29 +808,81 @@ var BaseEndpoint = class {
298
808
 
299
809
  //#endregion
300
810
  //#region lib/schemas/common.ts
811
+ /**
812
+ * Common ID type that supports strings, numbers, or UUIDs.
813
+ * Use this for entity identifiers in your schemas.
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * const UserSchema = z.object({ id: Id, name: z.string() });
818
+ * ```
819
+ */
301
820
  const Id = zod.z.union([
302
821
  zod.z.string().min(1),
303
822
  zod.z.number(),
304
823
  zod.z.uuid({ version: "v4" })
305
824
  ]);
825
+ /**
826
+ * Common timestamp fields for entities.
827
+ * Use this for database models with creation/update tracking.
828
+ *
829
+ * @example
830
+ * ```ts
831
+ * const UserSchema = z.object({
832
+ * id: Id,
833
+ * name: z.string(),
834
+ * ...Timestamps.shape
835
+ * });
836
+ * ```
837
+ */
306
838
  const Timestamps = zod.z.object({
307
- createdAt: zod.z.iso.datetime(),
308
- updatedAt: zod.z.iso.datetime()
839
+ createdAt: zod.z.string().datetime(),
840
+ updatedAt: zod.z.string().datetime()
309
841
  });
842
+ /**
843
+ * Metadata information typically included in API responses.
844
+ * Contains request tracking and debugging information.
845
+ */
310
846
  const Meta = zod.z.object({
311
847
  requestId: zod.z.string().optional(),
312
- timestamp: zod.z.iso.datetime().optional(),
848
+ timestamp: zod.z.string().datetime().optional(),
313
849
  traceId: zod.z.string().optional()
314
850
  });
851
+ /**
852
+ * Detailed error information for a specific field or path.
853
+ */
315
854
  const ErrorDetail = zod.z.object({
316
855
  path: zod.z.string().optional(),
317
856
  message: zod.z.string()
318
857
  });
858
+ /**
859
+ * Standard API error response schema.
860
+ * Use this for consistent error handling across your API.
861
+ */
319
862
  const ApiErrorSchema = zod.z.object({
320
863
  code: zod.z.string(),
321
864
  message: zod.z.string(),
322
865
  details: zod.z.array(ErrorDetail).optional()
323
866
  });
867
+ /**
868
+ * Generic envelope wrapper for API responses.
869
+ * Provides consistent structure with success flag, data, error, and metadata.
870
+ *
871
+ * @param inner - Zod schema for the response data
872
+ * @returns Envelope schema wrapping the inner schema
873
+ *
874
+ * @example
875
+ * ```ts
876
+ * const UserResponseSchema = Envelope(z.object({ id: Id, name: z.string() }));
877
+ *
878
+ * // Response structure:
879
+ * // {
880
+ * // success: true,
881
+ * // data: { id: 1, name: 'John' },
882
+ * // meta: { requestId: '...' }
883
+ * // }
884
+ * ```
885
+ */
324
886
  const Envelope = (inner) => zod.z.object({
325
887
  success: zod.z.boolean(),
326
888
  data: inner.optional(),
@@ -328,23 +890,26 @@ const Envelope = (inner) => zod.z.object({
328
890
  meta: Meta.optional()
329
891
  });
330
892
 
331
- //#endregion
332
- //#region lib/index.ts
333
- init_auth();
334
-
335
893
  //#endregion
336
894
  exports.ApiError = ApiError;
337
895
  exports.ApiErrorSchema = ApiErrorSchema;
338
896
  exports.ApiKeyAuth = ApiKeyAuth;
339
897
  exports.BaseEndpoint = BaseEndpoint;
340
898
  exports.BearerTokenAuth = BearerTokenAuth;
899
+ exports.ConsoleLogger = ConsoleLogger;
900
+ exports.ConsoleMetricsCollector = ConsoleMetricsCollector;
341
901
  exports.Envelope = Envelope;
342
902
  exports.ErrorDetail = ErrorDetail;
343
903
  exports.HTTPMethod = HTTPMethod;
344
904
  exports.HttpClient = HttpClient;
345
905
  exports.Id = Id;
906
+ exports.InMemoryMetricsCollector = InMemoryMetricsCollector;
907
+ exports.LogLevel = LogLevel;
908
+ exports.LoggerUtil = LoggerUtil;
346
909
  exports.Meta = Meta;
347
910
  exports.NoAuth = NoAuth;
911
+ exports.NoOpLogger = NoOpLogger;
912
+ exports.NoOpMetricsCollector = NoOpMetricsCollector;
348
913
  exports.PaginationSchema = PaginationSchema;
349
914
  exports.Timestamps = Timestamps;
350
915
  exports.parseOrThrow = parseOrThrow;