@felixgeelhaar/jira-sdk 0.3.0 → 1.0.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.
Files changed (46) hide show
  1. package/README.md +214 -374
  2. package/dist/auth/index.cjs +400 -0
  3. package/dist/auth/index.cjs.map +1 -0
  4. package/dist/auth/index.d.cts +192 -0
  5. package/dist/auth/index.d.ts +192 -0
  6. package/dist/auth/index.js +386 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/errors/index.cjs +272 -0
  9. package/dist/errors/index.cjs.map +1 -0
  10. package/dist/errors/index.d.cts +203 -0
  11. package/dist/errors/index.d.ts +203 -0
  12. package/dist/errors/index.js +254 -0
  13. package/dist/errors/index.js.map +1 -0
  14. package/dist/http-client-BSzRYQZa.d.cts +317 -0
  15. package/dist/http-client-erRvYNs-.d.ts +317 -0
  16. package/dist/index.cjs +9541 -13612
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +1242 -76
  19. package/dist/index.d.ts +1242 -76
  20. package/dist/index.js +9118 -13450
  21. package/dist/index.js.map +1 -1
  22. package/dist/schemas/index.cjs +2429 -13094
  23. package/dist/schemas/index.cjs.map +1 -1
  24. package/dist/schemas/index.d.cts +351 -13
  25. package/dist/schemas/index.d.ts +351 -13
  26. package/dist/schemas/index.js +2096 -13052
  27. package/dist/schemas/index.js.map +1 -1
  28. package/dist/services/index.cjs +6346 -13497
  29. package/dist/services/index.cjs.map +1 -1
  30. package/dist/services/index.d.cts +3828 -358
  31. package/dist/services/index.d.ts +3828 -358
  32. package/dist/services/index.js +6330 -13504
  33. package/dist/services/index.js.map +1 -1
  34. package/dist/transport/index.cjs +1016 -0
  35. package/dist/transport/index.cjs.map +1 -0
  36. package/dist/transport/index.d.cts +372 -0
  37. package/dist/transport/index.d.ts +372 -0
  38. package/dist/transport/index.js +997 -0
  39. package/dist/transport/index.js.map +1 -0
  40. package/dist/types-E6djPHpW.d.cts +95 -0
  41. package/dist/types-E6djPHpW.d.ts +95 -0
  42. package/dist/webhook-Bn8gme6Y.d.cts +6959 -0
  43. package/dist/webhook-Bn8gme6Y.d.ts +6959 -0
  44. package/package.json +73 -27
  45. package/dist/project-B1UelBH4.d.cts +0 -1810
  46. package/dist/project-B1UelBH4.d.ts +0 -1810
@@ -0,0 +1,997 @@
1
+ // src/logging/noop-logger.ts
2
+ var NoopLogger = class {
3
+ debug(_message, _fields) {
4
+ }
5
+ info(_message, _fields) {
6
+ }
7
+ warn(_message, _fields) {
8
+ }
9
+ error(_message, _fields) {
10
+ }
11
+ child(_fields) {
12
+ return this;
13
+ }
14
+ };
15
+
16
+ // src/utils/runtime.ts
17
+ function runtime() {
18
+ return globalThis;
19
+ }
20
+ function processEnv() {
21
+ return runtime().process?.env ?? {};
22
+ }
23
+ function readEnvVar(name) {
24
+ return processEnv()[name];
25
+ }
26
+ function warn(message) {
27
+ runtime().console?.warn?.(message);
28
+ }
29
+ function randomId() {
30
+ const uuid = runtime().crypto?.randomUUID?.();
31
+ if (uuid !== void 0) {
32
+ return uuid;
33
+ }
34
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
35
+ }
36
+ function captureStackTrace(target, constructor) {
37
+ const capture = Error.captureStackTrace;
38
+ capture?.(target, constructor);
39
+ }
40
+
41
+ // src/errors/base.error.ts
42
+ var JiraSdkError = class extends Error {
43
+ /** HTTP status code if applicable */
44
+ statusCode;
45
+ /** Original cause of the error */
46
+ cause;
47
+ /** Additional context/metadata */
48
+ context;
49
+ constructor(message, options) {
50
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
51
+ this.name = this.constructor.name;
52
+ if (options?.cause !== void 0) {
53
+ this.cause = options.cause;
54
+ }
55
+ if (options?.statusCode !== void 0) {
56
+ this.statusCode = options.statusCode;
57
+ }
58
+ if (options?.context !== void 0) {
59
+ this.context = options.context;
60
+ }
61
+ captureStackTrace(this, this.constructor);
62
+ }
63
+ /**
64
+ * Returns a JSON representation of the error
65
+ */
66
+ toJSON() {
67
+ return {
68
+ name: this.name,
69
+ code: this.code,
70
+ message: this.message,
71
+ statusCode: this.statusCode,
72
+ context: this.context,
73
+ cause: this.cause?.message,
74
+ stack: this.stack
75
+ };
76
+ }
77
+ };
78
+
79
+ // src/errors/network.error.ts
80
+ var NetworkError = class extends JiraSdkError {
81
+ code = "NETWORK_ERROR";
82
+ constructor(message, options) {
83
+ super(message, { cause: options?.cause });
84
+ }
85
+ /**
86
+ * Check if this error indicates a connection failure
87
+ */
88
+ isConnectionError() {
89
+ const msg = this.message.toLowerCase();
90
+ return msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("fetch failed");
91
+ }
92
+ /**
93
+ * Check if this error might be retryable
94
+ */
95
+ isRetryable() {
96
+ const msg = this.message.toLowerCase();
97
+ return msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("temporary");
98
+ }
99
+ };
100
+ var TimeoutError = class extends JiraSdkError {
101
+ code = "TIMEOUT_ERROR";
102
+ /** The timeout duration in milliseconds */
103
+ timeoutMs;
104
+ constructor(message, options) {
105
+ super(message, { cause: options?.cause });
106
+ this.timeoutMs = options?.timeoutMs;
107
+ }
108
+ };
109
+ var AbortError = class extends JiraSdkError {
110
+ code = "ABORT_ERROR";
111
+ constructor(message = "Request was aborted", options) {
112
+ super(message, { cause: options?.cause });
113
+ }
114
+ };
115
+
116
+ // src/errors/api.error.ts
117
+ var ApiError = class extends JiraSdkError {
118
+ code = "JIRA_API_ERROR";
119
+ /** Parsed error details from Jira response */
120
+ details;
121
+ /** Raw response body */
122
+ responseBody;
123
+ constructor(message, statusCode, responseBody) {
124
+ const details = parseErrorBody(responseBody);
125
+ const finalMessage = details ? buildErrorMessage(message, details) : message;
126
+ super(finalMessage, { statusCode, context: { responseBody } });
127
+ this.details = details;
128
+ this.responseBody = responseBody;
129
+ }
130
+ /**
131
+ * Returns all error messages as a single array
132
+ */
133
+ getAllMessages() {
134
+ const messages = [];
135
+ if (this.details?.errorMessages) {
136
+ messages.push(...this.details.errorMessages);
137
+ }
138
+ if (this.details?.errors) {
139
+ for (const [field, msg] of Object.entries(this.details.errors)) {
140
+ messages.push(`${field}: ${msg}`);
141
+ }
142
+ }
143
+ if (messages.length === 0) {
144
+ messages.push(this.message);
145
+ }
146
+ return messages;
147
+ }
148
+ };
149
+ var UnauthorizedError = class extends ApiError {
150
+ code = "JIRA_UNAUTHORIZED";
151
+ constructor(message = "Authentication required", responseBody) {
152
+ super(message, 401, responseBody);
153
+ }
154
+ };
155
+ var ForbiddenError = class extends ApiError {
156
+ code = "JIRA_FORBIDDEN";
157
+ constructor(message = "Permission denied", responseBody) {
158
+ super(message, 403, responseBody);
159
+ }
160
+ };
161
+ var NotFoundError = class extends ApiError {
162
+ code = "JIRA_NOT_FOUND";
163
+ constructor(message = "Resource not found", responseBody) {
164
+ super(message, 404, responseBody);
165
+ }
166
+ };
167
+ var RateLimitError = class extends ApiError {
168
+ code = "JIRA_RATE_LIMITED";
169
+ /** When to retry (in milliseconds) */
170
+ retryAfter;
171
+ constructor(message = "Rate limit exceeded", responseBody, options) {
172
+ super(message, 429, responseBody);
173
+ this.retryAfter = options?.retryAfter;
174
+ }
175
+ };
176
+ var ServerError = class extends ApiError {
177
+ code = "JIRA_SERVER_ERROR";
178
+ constructor(message = "Internal server error", responseBody, statusCode = 500) {
179
+ super(message, statusCode, responseBody);
180
+ }
181
+ };
182
+ function parseErrorBody(body) {
183
+ if (!body || typeof body !== "object") {
184
+ return void 0;
185
+ }
186
+ const obj = body;
187
+ return {
188
+ errorMessages: Array.isArray(obj["errorMessages"]) ? obj["errorMessages"] : void 0,
189
+ errors: typeof obj["errors"] === "object" && obj["errors"] !== null ? obj["errors"] : void 0,
190
+ warningMessages: Array.isArray(obj["warningMessages"]) ? obj["warningMessages"] : void 0
191
+ };
192
+ }
193
+ function buildErrorMessage(baseMessage, details) {
194
+ if (details.errorMessages?.length) {
195
+ return details.errorMessages.join("; ");
196
+ }
197
+ if (details.errors && Object.keys(details.errors).length > 0) {
198
+ const fieldErrors = Object.entries(details.errors).map(([field, msg]) => `${field}: ${msg}`).join("; ");
199
+ return `${baseMessage}: ${fieldErrors}`;
200
+ }
201
+ return baseMessage;
202
+ }
203
+
204
+ // src/utils/index.ts
205
+ function sleep(ms) {
206
+ return new Promise((resolve) => setTimeout(resolve, ms));
207
+ }
208
+ function buildQueryString(params) {
209
+ const filtered = Object.entries(params).filter(
210
+ ([, value]) => value !== void 0 && value !== null && value !== ""
211
+ );
212
+ if (filtered.length === 0) {
213
+ return "";
214
+ }
215
+ const searchParams = new URLSearchParams();
216
+ for (const [key, value] of filtered) {
217
+ if (Array.isArray(value)) {
218
+ for (const item of value) {
219
+ searchParams.append(key, String(item));
220
+ }
221
+ } else {
222
+ searchParams.set(key, String(value));
223
+ }
224
+ }
225
+ return searchParams.toString();
226
+ }
227
+ function joinPath(...segments) {
228
+ return segments.map(String).map((s) => s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
229
+ }
230
+ function exponentialBackoff(attempt, baseDelay, maxDelay, multiplier = 2, jitter = true) {
231
+ const delay = Math.min(baseDelay * Math.pow(multiplier, attempt), maxDelay);
232
+ if (jitter) {
233
+ const jitterAmount = delay * 0.25;
234
+ return delay + (Math.random() * 2 - 1) * jitterAmount;
235
+ }
236
+ return delay;
237
+ }
238
+
239
+ // src/transport/http-client.ts
240
+ var DEFAULT_TIMEOUT_MS = 3e4;
241
+ function validateSecureUrl(url, allowInsecure) {
242
+ const isHttps = url.startsWith("https://");
243
+ const isLocalhost = url.includes("localhost") || url.includes("127.0.0.1");
244
+ const isProduction = readEnvVar("NODE_ENV") === "production";
245
+ if (!isHttps && !isLocalhost && !allowInsecure) {
246
+ if (isProduction) {
247
+ throw new Error(
248
+ `Security Error: HTTPS is required for production environments. URL "${url}" uses HTTP. Use HTTPS or set allowInsecureHttp: true for testing.`
249
+ );
250
+ }
251
+ warn(
252
+ `[SDK Warning] Using insecure HTTP connection to "${url}". HTTPS is strongly recommended for production use.`
253
+ );
254
+ }
255
+ }
256
+ var HttpClient = class {
257
+ baseUrl;
258
+ auth;
259
+ logger;
260
+ timeout;
261
+ defaultHeaders;
262
+ middleware;
263
+ fetchFn;
264
+ middlewareChain;
265
+ constructor(config) {
266
+ validateSecureUrl(config.baseUrl, config.allowInsecureHttp);
267
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
268
+ this.auth = config.auth;
269
+ this.logger = config.logger ?? new NoopLogger();
270
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
271
+ this.defaultHeaders = {
272
+ "Content-Type": "application/json",
273
+ Accept: "application/json",
274
+ ...config.defaultHeaders
275
+ };
276
+ this.middleware = config.middleware ?? [];
277
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
278
+ this.middlewareChain = this.buildMiddlewareChain();
279
+ }
280
+ /**
281
+ * Make a GET request
282
+ */
283
+ async get(path, params, options) {
284
+ return this.request({
285
+ ...options,
286
+ method: "GET",
287
+ url: path,
288
+ // Merge rather than let one clobber the other: callers may pass params
289
+ // positionally and additionally supply some via options.
290
+ params: { ...params, ...options?.params }
291
+ });
292
+ }
293
+ /**
294
+ * Make a POST request
295
+ */
296
+ async post(path, body, options) {
297
+ return this.request({
298
+ method: "POST",
299
+ url: path,
300
+ body,
301
+ ...options
302
+ });
303
+ }
304
+ /**
305
+ * Make a PUT request
306
+ */
307
+ async put(path, body, options) {
308
+ return this.request({
309
+ method: "PUT",
310
+ url: path,
311
+ body,
312
+ ...options
313
+ });
314
+ }
315
+ /**
316
+ * Make a PATCH request
317
+ */
318
+ async patch(path, body, options) {
319
+ return this.request({
320
+ method: "PATCH",
321
+ url: path,
322
+ body,
323
+ ...options
324
+ });
325
+ }
326
+ /**
327
+ * Make a DELETE request
328
+ */
329
+ async delete(path, options) {
330
+ return this.request({
331
+ method: "DELETE",
332
+ url: path,
333
+ ...options
334
+ });
335
+ }
336
+ /**
337
+ * Make a request with full control over the configuration
338
+ */
339
+ async request(requestConfig) {
340
+ const {
341
+ method = "GET",
342
+ url,
343
+ headers,
344
+ body,
345
+ params,
346
+ timeout,
347
+ signal,
348
+ metadata,
349
+ skipAuth
350
+ } = requestConfig;
351
+ const httpRequest = {
352
+ method,
353
+ url,
354
+ headers: { ...this.defaultHeaders, ...headers },
355
+ body,
356
+ params,
357
+ timeout: timeout ?? this.timeout,
358
+ signal,
359
+ metadata
360
+ };
361
+ const context = {
362
+ request: httpRequest,
363
+ auth: skipAuth ? void 0 : this.auth,
364
+ logger: this.logger,
365
+ retryCount: 0
366
+ };
367
+ const chain = this.middlewareChain ?? this.buildMiddlewareChain();
368
+ return chain(context);
369
+ }
370
+ /**
371
+ * Build the middleware chain with the final request handler
372
+ */
373
+ buildMiddlewareChain() {
374
+ const finalHandler = async (context) => {
375
+ return this.executeRequest(context);
376
+ };
377
+ return this.middleware.reduceRight((next, middleware) => {
378
+ return async (ctx) => middleware(ctx, next);
379
+ }, finalHandler);
380
+ }
381
+ /**
382
+ * Execute the actual HTTP request
383
+ */
384
+ async executeRequest(context) {
385
+ const { request, auth } = context;
386
+ const startTime = Date.now();
387
+ let fullUrl = request.url.startsWith("http") ? request.url : joinPath(this.baseUrl, request.url);
388
+ if (request.params) {
389
+ const queryString = buildQueryString(request.params);
390
+ if (queryString) {
391
+ fullUrl += (fullUrl.includes("?") ? "&" : "?") + queryString;
392
+ }
393
+ }
394
+ const headers = new Headers(request.headers);
395
+ if (auth) {
396
+ const authHeaders = await auth.getAuthHeaders();
397
+ for (const [key, value] of Object.entries(authHeaders)) {
398
+ headers.set(key, value);
399
+ }
400
+ }
401
+ let bodyContent;
402
+ if (request.body !== void 0) {
403
+ if (request.body instanceof FormData || request.body instanceof Blob) {
404
+ bodyContent = request.body;
405
+ headers.delete("Content-Type");
406
+ } else if (typeof request.body === "string") {
407
+ bodyContent = request.body;
408
+ } else {
409
+ bodyContent = JSON.stringify(request.body);
410
+ }
411
+ }
412
+ const controller = new AbortController();
413
+ const timeoutId = setTimeout(() => {
414
+ controller.abort();
415
+ }, request.timeout ?? this.timeout);
416
+ const combinedSignal = request.signal ? this.combineSignals(request.signal, controller.signal) : controller.signal;
417
+ try {
418
+ const response = await this.fetchFn(fullUrl, {
419
+ method: request.method,
420
+ headers,
421
+ ...bodyContent !== void 0 && { body: bodyContent },
422
+ signal: combinedSignal
423
+ });
424
+ clearTimeout(timeoutId);
425
+ const responseTime = Date.now() - startTime;
426
+ const contentType = response.headers.get("content-type");
427
+ let data;
428
+ const wantBlob = request.metadata?.["rawResponse"] === true;
429
+ if (wantBlob) {
430
+ data = await response.blob();
431
+ } else if (contentType?.includes("application/json")) {
432
+ const text = await response.text();
433
+ data = text ? JSON.parse(text) : null;
434
+ } else {
435
+ data = await response.text();
436
+ }
437
+ if (!response.ok) {
438
+ this.handleErrorResponse(response, data);
439
+ }
440
+ return {
441
+ status: response.status,
442
+ statusText: response.statusText,
443
+ headers: response.headers,
444
+ data,
445
+ request,
446
+ responseTime
447
+ };
448
+ } catch (error) {
449
+ clearTimeout(timeoutId);
450
+ if (error instanceof ApiError) {
451
+ throw error;
452
+ }
453
+ if (error instanceof Error) {
454
+ if (error.name === "AbortError") {
455
+ if (request.signal?.aborted) {
456
+ throw new AbortError("Request was aborted", { cause: error });
457
+ }
458
+ throw new TimeoutError(`Request timed out after ${request.timeout ?? this.timeout}ms`, {
459
+ cause: error
460
+ });
461
+ }
462
+ throw new NetworkError(`Network error: ${error.message}`, {
463
+ cause: error
464
+ });
465
+ }
466
+ throw new NetworkError("Unknown network error", { cause: error });
467
+ }
468
+ }
469
+ /**
470
+ * Handle error responses and throw appropriate errors
471
+ */
472
+ handleErrorResponse(response, data) {
473
+ const status = response.status;
474
+ switch (status) {
475
+ case 401:
476
+ throw new UnauthorizedError("Authentication required", data);
477
+ case 403:
478
+ throw new ForbiddenError("Access denied", data);
479
+ case 404:
480
+ throw new NotFoundError("Resource not found", data);
481
+ case 429: {
482
+ const retryAfter = this.parseRetryAfter(response.headers);
483
+ throw new RateLimitError(
484
+ "Rate limit exceeded",
485
+ data,
486
+ retryAfter !== void 0 ? { retryAfter } : void 0
487
+ );
488
+ }
489
+ default:
490
+ if (status >= 500) {
491
+ throw new ServerError(`Server error: ${response.statusText}`, data, status);
492
+ }
493
+ throw new ApiError(`API error: ${response.statusText}`, status, data);
494
+ }
495
+ }
496
+ /**
497
+ * Parse Retry-After header
498
+ */
499
+ parseRetryAfter(headers) {
500
+ const retryAfter = headers.get("retry-after");
501
+ if (!retryAfter) return void 0;
502
+ const seconds = parseInt(retryAfter, 10);
503
+ if (!isNaN(seconds)) {
504
+ return seconds * 1e3;
505
+ }
506
+ const date = Date.parse(retryAfter);
507
+ if (!isNaN(date)) {
508
+ return Math.max(0, date - Date.now());
509
+ }
510
+ return void 0;
511
+ }
512
+ /**
513
+ * Combine multiple AbortSignals
514
+ */
515
+ combineSignals(signal1, signal2) {
516
+ const controller = new AbortController();
517
+ const abort = () => {
518
+ controller.abort();
519
+ };
520
+ if (signal1.aborted || signal2.aborted) {
521
+ controller.abort();
522
+ return controller.signal;
523
+ }
524
+ signal1.addEventListener("abort", abort);
525
+ signal2.addEventListener("abort", abort);
526
+ return controller.signal;
527
+ }
528
+ /**
529
+ * Add middleware to the chain
530
+ * Note: This rebuilds the middleware chain for the new configuration
531
+ */
532
+ use(middleware) {
533
+ this.middleware.push(middleware);
534
+ this.middlewareChain = this.buildMiddlewareChain();
535
+ return this;
536
+ }
537
+ /**
538
+ * Get the base URL
539
+ */
540
+ getBaseUrl() {
541
+ return this.baseUrl;
542
+ }
543
+ /**
544
+ * Get the auth provider
545
+ */
546
+ getAuth() {
547
+ return this.auth;
548
+ }
549
+ };
550
+ function createHttpClient(config) {
551
+ return new HttpClient(config);
552
+ }
553
+
554
+ // src/transport/middleware.ts
555
+ function createLoggingMiddleware(config = {}) {
556
+ const {
557
+ logRequests = true,
558
+ logResponses = true,
559
+ logErrors = true,
560
+ redactHeaders = ["authorization", "x-api-key"]
561
+ } = config;
562
+ const redactHeadersLower = redactHeaders.map((h) => h.toLowerCase());
563
+ return async (context, next) => {
564
+ const { request, logger } = context;
565
+ if (logRequests) {
566
+ const headers = { ...request.headers };
567
+ for (const key of Object.keys(headers)) {
568
+ if (redactHeadersLower.includes(key.toLowerCase())) {
569
+ headers[key] = "[REDACTED]";
570
+ }
571
+ }
572
+ logger.debug("HTTP Request", {
573
+ method: request.method,
574
+ url: request.url,
575
+ headers,
576
+ params: request.params
577
+ });
578
+ }
579
+ try {
580
+ const response = await next(context);
581
+ if (logResponses) {
582
+ logger.debug("HTTP Response", {
583
+ status: response.status,
584
+ statusText: response.statusText,
585
+ responseTime: response.responseTime
586
+ });
587
+ }
588
+ return response;
589
+ } catch (error) {
590
+ if (logErrors) {
591
+ logger.error("HTTP Error", {
592
+ method: request.method,
593
+ url: request.url,
594
+ error: error instanceof Error ? error.message : String(error)
595
+ });
596
+ }
597
+ throw error;
598
+ }
599
+ };
600
+ }
601
+ function defaultShouldRetry(error) {
602
+ if (error instanceof RateLimitError) {
603
+ return true;
604
+ }
605
+ if (error instanceof ServerError) {
606
+ return true;
607
+ }
608
+ if (error instanceof NetworkError) {
609
+ return true;
610
+ }
611
+ if (error instanceof ApiError) {
612
+ return false;
613
+ }
614
+ return false;
615
+ }
616
+ function createRetryMiddleware(config = {}) {
617
+ const {
618
+ maxRetries = 3,
619
+ initialDelayMs = 1e3,
620
+ maxDelayMs = 3e4,
621
+ multiplier = 2,
622
+ jitter = true,
623
+ shouldRetry = defaultShouldRetry
624
+ } = config;
625
+ return async (context, next) => {
626
+ let lastError;
627
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
628
+ try {
629
+ context.retryCount = attempt;
630
+ return await next(context);
631
+ } catch (error) {
632
+ lastError = error;
633
+ if (attempt >= maxRetries || !shouldRetry(error, context)) {
634
+ throw error;
635
+ }
636
+ let delayMs;
637
+ if (error instanceof RateLimitError && error.retryAfter) {
638
+ delayMs = error.retryAfter;
639
+ } else {
640
+ delayMs = exponentialBackoff(attempt, initialDelayMs, maxDelayMs, multiplier, jitter);
641
+ }
642
+ context.logger.debug("Retrying request", {
643
+ attempt: attempt + 1,
644
+ maxRetries,
645
+ delayMs,
646
+ error: error instanceof Error ? error.message : String(error)
647
+ });
648
+ await sleep(delayMs);
649
+ }
650
+ }
651
+ throw lastError;
652
+ };
653
+ }
654
+ function createRateLimitMiddleware(config) {
655
+ const { maxRequests, windowMs, waitForSlot = true } = config;
656
+ const timestamps = [];
657
+ return async (context, next) => {
658
+ const now = Date.now();
659
+ const cutoff = now - windowMs;
660
+ for (let oldest = timestamps[0]; oldest !== void 0 && oldest < cutoff; oldest = timestamps[0]) {
661
+ timestamps.shift();
662
+ }
663
+ if (timestamps.length >= maxRequests) {
664
+ if (waitForSlot) {
665
+ const oldestTimestamp = timestamps[0] ?? now;
666
+ const waitTime = oldestTimestamp + windowMs - now;
667
+ if (waitTime > 0) {
668
+ context.logger.debug("Rate limit reached, waiting", { waitTime });
669
+ await sleep(waitTime);
670
+ }
671
+ timestamps.shift();
672
+ } else {
673
+ throw new RateLimitError("Client-side rate limit exceeded", void 0, {
674
+ retryAfter: (timestamps[0] ?? now) + windowMs - now
675
+ });
676
+ }
677
+ }
678
+ timestamps.push(now);
679
+ return next(context);
680
+ };
681
+ }
682
+ function createRequestIdMiddleware(headerName = "X-Request-ID") {
683
+ return async (context, next) => {
684
+ const requestId = generateRequestId();
685
+ context.request.headers = {
686
+ ...context.request.headers,
687
+ [headerName]: requestId
688
+ };
689
+ context.requestId = requestId;
690
+ return next(context);
691
+ };
692
+ }
693
+ function generateRequestId() {
694
+ return randomId();
695
+ }
696
+ function createUserAgentMiddleware(config) {
697
+ const { sdkName, sdkVersion, suffix } = config;
698
+ let userAgent = `${sdkName}/${sdkVersion}`;
699
+ if (typeof process !== "undefined" && process.version) {
700
+ userAgent += ` Node.js/${process.version.slice(1)}`;
701
+ } else if (typeof navigator !== "undefined") {
702
+ userAgent += " Browser";
703
+ }
704
+ if (suffix) {
705
+ userAgent += ` ${suffix}`;
706
+ }
707
+ return async (context, next) => {
708
+ context.request.headers = {
709
+ ...context.request.headers,
710
+ "User-Agent": userAgent
711
+ };
712
+ return next(context);
713
+ };
714
+ }
715
+ function composeMiddleware(...middlewares) {
716
+ return async (context, next) => {
717
+ const chain = middlewares.reduceRight((nextFn, middleware) => {
718
+ return async (ctx) => middleware(ctx, nextFn);
719
+ }, next);
720
+ return chain(context);
721
+ };
722
+ }
723
+
724
+ // src/transport/circuit-breaker.ts
725
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
726
+ CircuitState2["CLOSED"] = "CLOSED";
727
+ CircuitState2["OPEN"] = "OPEN";
728
+ CircuitState2["HALF_OPEN"] = "HALF_OPEN";
729
+ return CircuitState2;
730
+ })(CircuitState || {});
731
+ var CircuitBreakerOpenError = class extends Error {
732
+ code = "CIRCUIT_BREAKER_OPEN";
733
+ remainingMs;
734
+ constructor(message, remainingMs) {
735
+ super(message);
736
+ this.name = "CircuitBreakerOpenError";
737
+ this.remainingMs = remainingMs;
738
+ }
739
+ };
740
+ var CircuitBreaker = class {
741
+ state = "CLOSED" /* CLOSED */;
742
+ failures = [];
743
+ lastFailureTime = 0;
744
+ successCount = 0;
745
+ failureThreshold;
746
+ resetTimeoutMs;
747
+ failureWindowMs;
748
+ successThreshold;
749
+ isFailure;
750
+ onStateChange;
751
+ constructor(config = {}) {
752
+ this.failureThreshold = config.failureThreshold ?? 5;
753
+ this.resetTimeoutMs = config.resetTimeoutMs ?? 3e4;
754
+ this.failureWindowMs = config.failureWindowMs ?? 6e4;
755
+ this.successThreshold = config.successThreshold ?? 1;
756
+ this.isFailure = config.isFailure ?? defaultIsFailure;
757
+ this.onStateChange = config.onStateChange;
758
+ }
759
+ /**
760
+ * Get current circuit state
761
+ */
762
+ getState() {
763
+ this.updateState();
764
+ return this.state;
765
+ }
766
+ /**
767
+ * Check if request is allowed
768
+ */
769
+ canExecute() {
770
+ this.updateState();
771
+ return this.state !== "OPEN" /* OPEN */;
772
+ }
773
+ /**
774
+ * Record a successful request
775
+ */
776
+ recordSuccess() {
777
+ if (this.state === "HALF_OPEN" /* HALF_OPEN */) {
778
+ this.successCount++;
779
+ if (this.successCount >= this.successThreshold) {
780
+ this.transitionTo("CLOSED" /* CLOSED */);
781
+ this.reset();
782
+ }
783
+ } else if (this.state === "CLOSED" /* CLOSED */) {
784
+ this.cleanupFailures();
785
+ }
786
+ }
787
+ /**
788
+ * Record a failed request
789
+ */
790
+ recordFailure(error) {
791
+ if (!this.isFailure(error)) {
792
+ return;
793
+ }
794
+ const now = Date.now();
795
+ this.failures.push(now);
796
+ this.lastFailureTime = now;
797
+ this.cleanupFailures();
798
+ if (this.state === "HALF_OPEN" /* HALF_OPEN */) {
799
+ this.transitionTo("OPEN" /* OPEN */);
800
+ this.successCount = 0;
801
+ } else if (this.state === "CLOSED" /* CLOSED */) {
802
+ if (this.failures.length >= this.failureThreshold) {
803
+ this.transitionTo("OPEN" /* OPEN */);
804
+ }
805
+ }
806
+ }
807
+ /**
808
+ * Get remaining time until circuit can transition to half-open
809
+ */
810
+ getRemainingOpenTime() {
811
+ if (this.state !== "OPEN" /* OPEN */) {
812
+ return 0;
813
+ }
814
+ const elapsed = Date.now() - this.lastFailureTime;
815
+ return Math.max(0, this.resetTimeoutMs - elapsed);
816
+ }
817
+ /**
818
+ * Force reset the circuit breaker
819
+ */
820
+ reset() {
821
+ this.failures = [];
822
+ this.successCount = 0;
823
+ this.lastFailureTime = 0;
824
+ if (this.state !== "CLOSED" /* CLOSED */) {
825
+ this.transitionTo("CLOSED" /* CLOSED */);
826
+ }
827
+ }
828
+ /**
829
+ * Get circuit breaker statistics
830
+ */
831
+ getStats() {
832
+ this.cleanupFailures();
833
+ return {
834
+ state: this.getState(),
835
+ failureCount: this.failures.length,
836
+ successCount: this.successCount,
837
+ remainingOpenTimeMs: this.getRemainingOpenTime()
838
+ };
839
+ }
840
+ updateState() {
841
+ if (this.state === "OPEN" /* OPEN */) {
842
+ const elapsed = Date.now() - this.lastFailureTime;
843
+ if (elapsed >= this.resetTimeoutMs) {
844
+ this.transitionTo("HALF_OPEN" /* HALF_OPEN */);
845
+ this.successCount = 0;
846
+ }
847
+ }
848
+ }
849
+ transitionTo(newState) {
850
+ if (this.state !== newState) {
851
+ const oldState = this.state;
852
+ this.state = newState;
853
+ this.onStateChange?.(oldState, newState);
854
+ }
855
+ }
856
+ cleanupFailures() {
857
+ const cutoff = Date.now() - this.failureWindowMs;
858
+ this.failures = this.failures.filter((t) => t > cutoff);
859
+ }
860
+ };
861
+ function defaultIsFailure(error) {
862
+ if (error instanceof NetworkError) {
863
+ return true;
864
+ }
865
+ if (error instanceof ServerError) {
866
+ return true;
867
+ }
868
+ if (error instanceof RateLimitError) {
869
+ return true;
870
+ }
871
+ if (error instanceof ApiError && error.statusCode !== void 0) {
872
+ return error.statusCode >= 500;
873
+ }
874
+ return false;
875
+ }
876
+ function createCircuitBreakerMiddleware(circuitBreaker) {
877
+ return async (context, next) => {
878
+ if (!circuitBreaker.canExecute()) {
879
+ const remainingMs = circuitBreaker.getRemainingOpenTime();
880
+ context.logger.warn("Circuit breaker open, rejecting request", {
881
+ remainingMs,
882
+ url: context.request.url
883
+ });
884
+ throw new CircuitBreakerOpenError(
885
+ `Circuit breaker is open. Retry after ${remainingMs}ms`,
886
+ remainingMs
887
+ );
888
+ }
889
+ try {
890
+ const response = await next(context);
891
+ circuitBreaker.recordSuccess();
892
+ return response;
893
+ } catch (error) {
894
+ circuitBreaker.recordFailure(error);
895
+ throw error;
896
+ }
897
+ };
898
+ }
899
+ function createDefaultCircuitBreaker(config) {
900
+ return new CircuitBreaker(config);
901
+ }
902
+
903
+ // src/transport/rate-limit-headers.ts
904
+ function parseBetaRateLimitPolicy(header) {
905
+ if (header === null || header === void 0 || header === "") {
906
+ return void 0;
907
+ }
908
+ const parts = header.split(";");
909
+ const rawLimit = parts[0]?.trim();
910
+ if (rawLimit === void 0 || rawLimit === "") {
911
+ return void 0;
912
+ }
913
+ const limit = Number.parseInt(rawLimit, 10);
914
+ if (!Number.isFinite(limit)) {
915
+ return void 0;
916
+ }
917
+ let windowSeconds = 0;
918
+ for (const part of parts.slice(1)) {
919
+ const [key, value] = part.trim().split("=", 2);
920
+ if (key === "w" && value !== void 0) {
921
+ const parsed = Number.parseInt(value, 10);
922
+ if (Number.isFinite(parsed)) {
923
+ windowSeconds = parsed;
924
+ }
925
+ }
926
+ }
927
+ return { limit, windowSeconds };
928
+ }
929
+ function parseBetaRateLimit(header) {
930
+ if (header === null || header === void 0 || header === "") {
931
+ return void 0;
932
+ }
933
+ for (const part of header.split(";")) {
934
+ const [key, value] = part.trim().split("=", 2);
935
+ if (key === "r" && value !== void 0) {
936
+ const remaining = Number.parseInt(value, 10);
937
+ if (Number.isFinite(remaining)) {
938
+ return remaining;
939
+ }
940
+ }
941
+ }
942
+ return void 0;
943
+ }
944
+ function parseRetryAfterSeconds(header, now = Date.now()) {
945
+ if (header === null || header === void 0 || header === "") {
946
+ return void 0;
947
+ }
948
+ const seconds = Number.parseInt(header.trim(), 10);
949
+ if (Number.isFinite(seconds) && String(seconds) === header.trim()) {
950
+ return Math.max(0, seconds);
951
+ }
952
+ const date = Date.parse(header);
953
+ if (Number.isFinite(date)) {
954
+ return Math.max(0, Math.ceil((date - now) / 1e3));
955
+ }
956
+ return void 0;
957
+ }
958
+ function readRateLimitHeaders(headers) {
959
+ const snapshot = {};
960
+ const remaining = headers.get("x-ratelimit-remaining");
961
+ if (remaining !== null) {
962
+ const parsed = Number.parseInt(remaining, 10);
963
+ if (Number.isFinite(parsed)) {
964
+ snapshot.remainingRequests = parsed;
965
+ }
966
+ }
967
+ const retryAfter = parseRetryAfterSeconds(headers.get("retry-after"));
968
+ if (retryAfter !== void 0) {
969
+ snapshot.retryAfterSeconds = retryAfter;
970
+ }
971
+ const policy = parseBetaRateLimitPolicy(headers.get("beta-ratelimit-policy"));
972
+ if (policy !== void 0) {
973
+ snapshot.policy = policy;
974
+ }
975
+ const points = parseBetaRateLimit(headers.get("beta-ratelimit"));
976
+ if (points !== void 0) {
977
+ snapshot.remainingPoints = points;
978
+ }
979
+ return snapshot;
980
+ }
981
+ function createRateLimitHeaderMiddleware(config) {
982
+ return async (ctx, next) => {
983
+ const response = await next(ctx);
984
+ const snapshot = readRateLimitHeaders(response.headers);
985
+ if (Object.keys(snapshot).length > 0) {
986
+ try {
987
+ config.onRateLimit(snapshot);
988
+ } catch {
989
+ }
990
+ }
991
+ return response;
992
+ };
993
+ }
994
+
995
+ export { CircuitBreaker, CircuitBreakerOpenError, CircuitState, HttpClient, composeMiddleware, createCircuitBreakerMiddleware, createDefaultCircuitBreaker, createHttpClient, createLoggingMiddleware, createRateLimitHeaderMiddleware, createRateLimitMiddleware, createRequestIdMiddleware, createRetryMiddleware, createUserAgentMiddleware, parseBetaRateLimit, parseBetaRateLimitPolicy, parseRetryAfterSeconds, readRateLimitHeaders };
996
+ //# sourceMappingURL=index.js.map
997
+ //# sourceMappingURL=index.js.map