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