cyberdesk 2.2.7 → 2.2.9

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.d.ts CHANGED
@@ -30,13 +30,72 @@ export * from './client/types.gen';
30
30
  export * from './client/sdk.gen';
31
31
  export * from './client/client.gen';
32
32
  /**
33
- * Client options for configuring retry behavior and other settings
33
+ * Client options for configuring the SDK
34
34
  */
35
35
  export interface CyberdeskClientOptions {
36
- /** Maximum number of retry attempts (default: 2, meaning 3 total attempts) */
37
- maxRetries?: number;
38
36
  /** Custom fetch implementation (for testing or advanced use cases) */
39
37
  fetch?: typeof fetch;
38
+ /** Retry configuration for transient failures (network errors, 5xx, 429, etc.). */
39
+ retry?: CyberdeskRetryOptions;
40
+ /** Idempotency configuration (auto-adds Idempotency-Key for write requests). */
41
+ idempotency?: CyberdeskIdempotencyOptions;
42
+ }
43
+ export interface CyberdeskRetryOptions {
44
+ /**
45
+ * Number of retries after the initial attempt.
46
+ * Total attempts = 1 + maxRetries.
47
+ *
48
+ * Default: 3
49
+ */
50
+ maxRetries?: number;
51
+ /**
52
+ * Per-attempt timeout in milliseconds.
53
+ *
54
+ * Default: 30000 (30s)
55
+ */
56
+ timeoutMs?: number;
57
+ /**
58
+ * Initial backoff delay in milliseconds (before exponential growth + jitter).
59
+ *
60
+ * Default: 250ms
61
+ */
62
+ minDelayMs?: number;
63
+ /**
64
+ * Maximum backoff delay in milliseconds.
65
+ *
66
+ * Default: 8000ms
67
+ */
68
+ maxDelayMs?: number;
69
+ /**
70
+ * Called before a retry is scheduled (useful for logging/metrics).
71
+ */
72
+ onRetry?: (info: {
73
+ attempt: number;
74
+ maxRetries: number;
75
+ delayMs: number;
76
+ reason: 'timeout' | 'network_error' | 'http_status';
77
+ status?: number;
78
+ }) => void;
79
+ }
80
+ export interface CyberdeskIdempotencyOptions {
81
+ /**
82
+ * Enable automatic idempotency keys for write requests (POST/PUT/PATCH/DELETE).
83
+ *
84
+ * Default: true
85
+ */
86
+ enabled?: boolean;
87
+ /**
88
+ * Header name to use.
89
+ *
90
+ * Default: 'Idempotency-Key'
91
+ */
92
+ headerName?: string;
93
+ /**
94
+ * Custom generator for idempotency keys.
95
+ *
96
+ * Default: crypto.randomUUID() (with fallback)
97
+ */
98
+ generateKey?: () => string;
40
99
  }
41
100
  /**
42
101
  * Create a Cyberdesk API client
@@ -44,7 +103,6 @@ export interface CyberdeskClientOptions {
44
103
  * @param apiKey - Your Cyberdesk API key
45
104
  * @param baseUrl - Optional API base URL (defaults to https://api.cyberdesk.io)
46
105
  * @param options - Optional client configuration
47
- * @param options.maxRetries - Maximum retry attempts for failed requests (default: 2)
48
106
  * @returns Configured client with all API endpoints
49
107
  *
50
108
  * @example
@@ -52,12 +110,6 @@ export interface CyberdeskClientOptions {
52
110
  * // Basic usage
53
111
  * const client = createCyberdeskClient('your-api-key');
54
112
  * const machines = await client.machines.list();
55
- *
56
- * // With custom retry configuration
57
- * const client = createCyberdeskClient('your-api-key', undefined, { maxRetries: 3 });
58
- *
59
- * // Disable retries
60
- * const client = createCyberdeskClient('your-api-key', undefined, { maxRetries: 0 });
61
113
  * ```
62
114
  */
63
115
  export declare function createCyberdeskClient(apiKey: string, baseUrl?: string, options?: CyberdeskClientOptions): {
package/dist/index.js CHANGED
@@ -52,172 +52,196 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.createCyberdeskClient = createCyberdeskClient;
54
54
  const client_fetch_1 = require("@hey-api/client-fetch");
55
- // ============================================================================
56
- // Retry Configuration (Stripe SDK-style)
57
- // ============================================================================
58
- /** Default number of retry attempts (2 retries = 3 total attempts, like Stripe) */
59
- const DEFAULT_MAX_RETRIES = 2;
60
- /** Initial retry delay in milliseconds */
61
- const INITIAL_RETRY_DELAY_MS = 500;
62
- /** Maximum retry delay in milliseconds */
63
- const MAX_RETRY_DELAY_MS = 5000;
64
- /** HTTP status codes that should trigger a retry */
65
- const RETRYABLE_STATUS_CODES = new Set([
66
- 408, // Request Timeout
67
- 409, // Conflict (can be retried with idempotency)
68
- 429, // Too Many Requests
69
- 500, // Internal Server Error
70
- 502, // Bad Gateway
71
- 503, // Service Unavailable
72
- 504, // Gateway Timeout
73
- ]);
74
- /**
75
- * Determines if an error is a retryable network error
76
- *
77
- * Note: AbortError (from AbortController.abort()) is NOT retried because:
78
- * - It indicates intentional cancellation by the caller
79
- * - The abort signal remains aborted, so retries would fail immediately
80
- * - Retrying would waste attempts and delay error propagation
81
- */
82
- function isNetworkError(error) {
83
- // AbortError means intentional cancellation - don't retry
84
- if (error instanceof DOMException && error.name === 'AbortError') {
85
- return false;
86
- }
87
- if (error instanceof TypeError) {
88
- const message = error.message.toLowerCase();
89
- return (message.includes('fetch') ||
90
- message.includes('network') ||
91
- message.includes('failed') ||
92
- message.includes('timeout'));
93
- }
94
- return false;
95
- }
96
- /**
97
- * Determines if a response status code should trigger a retry
98
- */
99
- function shouldRetryResponse(response) {
100
- return RETRYABLE_STATUS_CODES.has(response.status);
55
+ // Import SDK methods from sdk.gen
56
+ const sdk_gen_1 = require("./client/sdk.gen");
57
+ // Export all generated types and methods for direct use
58
+ __exportStar(require("./client/types.gen"), exports);
59
+ __exportStar(require("./client/sdk.gen"), exports);
60
+ __exportStar(require("./client/client.gen"), exports);
61
+ // Configuration
62
+ /** Default API base URL for Cyberdesk Cloud API */
63
+ const DEFAULT_API_BASE_URL = "https://api.cyberdesk.io";
64
+ function sleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
101
66
  }
102
- /**
103
- * Calculate delay for exponential backoff with full jitter (Stripe-style)
104
- *
105
- * Formula: min(cap, random(0, base * 2^attempt))
106
- * This provides better distribution than adding jitter to exponential backoff
107
- */
108
- function calculateRetryDelay(attempt, retryAfterMs) {
109
- if (retryAfterMs && retryAfterMs > 0) {
110
- // Respect server's Retry-After header, but cap it
111
- return Math.min(retryAfterMs, MAX_RETRY_DELAY_MS);
67
+ function defaultGenerateIdempotencyKey() {
68
+ // Browser + modern Node runtimes
69
+ const anyCrypto = globalThis.crypto;
70
+ if (anyCrypto && typeof anyCrypto.randomUUID === 'function') {
71
+ return anyCrypto.randomUUID();
112
72
  }
113
- // Full jitter exponential backoff
114
- const exponentialDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
115
- const maxDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
116
- return Math.random() * maxDelay;
73
+ // Fallback: not a UUID but sufficiently unique for idempotency keys
74
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
117
75
  }
118
- /**
119
- * Parse Retry-After header value to milliseconds
120
- */
121
- function parseRetryAfter(response) {
122
- const retryAfter = response.headers.get('Retry-After');
76
+ function parseRetryAfterMs(retryAfter) {
123
77
  if (!retryAfter)
124
- return undefined;
125
- // Try parsing as seconds (integer)
126
- const seconds = parseInt(retryAfter, 10);
127
- if (!isNaN(seconds)) {
78
+ return null;
79
+ const trimmed = retryAfter.trim();
80
+ if (!trimmed)
81
+ return null;
82
+ // Seconds
83
+ if (/^\d+$/.test(trimmed)) {
84
+ const seconds = Number(trimmed);
85
+ if (!Number.isFinite(seconds) || seconds < 0)
86
+ return null;
128
87
  return seconds * 1000;
129
88
  }
130
- // Try parsing as HTTP date
131
- const date = Date.parse(retryAfter);
132
- if (!isNaN(date)) {
133
- return Math.max(0, date - Date.now());
134
- }
135
- return undefined;
89
+ // HTTP date
90
+ const dateMs = Date.parse(trimmed);
91
+ if (Number.isNaN(dateMs))
92
+ return null;
93
+ const delta = dateMs - Date.now();
94
+ return delta > 0 ? delta : 0;
136
95
  }
137
- /**
138
- * Sleep for a specified duration
139
- */
140
- function sleep(ms) {
141
- return new Promise(resolve => setTimeout(resolve, ms));
96
+ function isWriteMethod(method) {
97
+ const m = method.toUpperCase();
98
+ return m === 'POST' || m === 'PUT' || m === 'PATCH' || m === 'DELETE';
142
99
  }
143
- /**
144
- * Creates a fetch wrapper with automatic retry logic
145
- *
146
- * Implements Stripe-style retry behavior:
147
- * - Retries on network errors (connection failures, timeouts)
148
- * - Retries on 5xx server errors and 429 (rate limit)
149
- * - Does NOT retry on 4xx client errors (except 408, 409, 429)
150
- * - Uses exponential backoff with full jitter
151
- * - Respects Retry-After headers
152
- */
153
- function createRetryFetch(maxRetries = DEFAULT_MAX_RETRIES) {
154
- return function retryFetch(input, init) {
155
- return __awaiter(this, void 0, void 0, function* () {
156
- let lastError;
157
- let lastResponse;
158
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
159
- try {
160
- // Use globalThis.fetch directly bound to avoid "Illegal invocation"
161
- const response = yield globalThis.fetch(input, init);
162
- // Success - return immediately
163
- if (!shouldRetryResponse(response)) {
164
- return response;
165
- }
166
- // Store response for potential retry
167
- lastResponse = response;
168
- // Don't retry on last attempt
169
- if (attempt === maxRetries) {
170
- return response;
171
- }
172
- // Calculate delay with Retry-After header support
173
- const retryAfterMs = parseRetryAfter(response);
174
- const delay = calculateRetryDelay(attempt, retryAfterMs);
175
- yield sleep(delay);
100
+ function isRetryableStatus(status, response) {
101
+ var _a;
102
+ // Standard transient statuses
103
+ if (status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
104
+ return true;
105
+ }
106
+ // Special-case: idempotency in progress (API returns 409 + Retry-After + Idempotency-Status)
107
+ if (status === 409 && response) {
108
+ const idemStatus = (_a = response.headers.get('Idempotency-Status')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
109
+ if (idemStatus === 'in_progress')
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ function createRetryingFetch(baseFetch, opts) {
115
+ return (input, init) => __awaiter(this, void 0, void 0, function* () {
116
+ var _a, _b, _c, _d, _e;
117
+ const retry = opts.retry;
118
+ const idempotency = opts.idempotency;
119
+ const baseMethod = ((_a = init === null || init === void 0 ? void 0 : init.method) !== null && _a !== void 0 ? _a : (input instanceof Request ? input.method : 'GET')).toUpperCase();
120
+ const shouldAddIdempotency = idempotency.enabled && isWriteMethod(baseMethod);
121
+ // Build merged headers once, and reuse across attempts
122
+ const headers = new Headers();
123
+ if (input instanceof Request) {
124
+ input.headers.forEach((v, k) => headers.set(k, v));
125
+ }
126
+ if (init === null || init === void 0 ? void 0 : init.headers) {
127
+ new Headers(init.headers).forEach((v, k) => headers.set(k, v));
128
+ }
129
+ let idempotencyKey = null;
130
+ if (shouldAddIdempotency) {
131
+ const headerName = idempotency.headerName || 'Idempotency-Key';
132
+ if (!headers.has(headerName)) {
133
+ idempotencyKey = idempotency.generateKey();
134
+ headers.set(headerName, idempotencyKey);
135
+ }
136
+ }
137
+ // If we cannot ensure idempotency for a write request, do not retry by default (unsafe).
138
+ const writeRequestWithoutIdempotency = isWriteMethod(baseMethod) && !(idempotencyKey || headers.has(idempotency.headerName || 'Idempotency-Key'));
139
+ const maxRetries = Math.max(0, retry.maxRetries);
140
+ const totalAttempts = 1 + maxRetries;
141
+ let lastError;
142
+ for (let attempt = 0; attempt < totalAttempts; attempt++) {
143
+ // Per-attempt timeout
144
+ const controller = new AbortController();
145
+ const timeout = setTimeout(() => controller.abort(), retry.timeoutMs);
146
+ // Respect caller-provided AbortSignal, if any
147
+ const parentSignal = (_b = init === null || init === void 0 ? void 0 : init.signal) !== null && _b !== void 0 ? _b : (input instanceof Request ? input.signal : undefined);
148
+ if (parentSignal) {
149
+ if (parentSignal.aborted) {
150
+ clearTimeout(timeout);
151
+ throw new DOMException('The operation was aborted.', 'AbortError');
176
152
  }
177
- catch (error) {
178
- lastError = error;
179
- // Only retry on network errors
180
- if (!isNetworkError(error)) {
181
- throw error;
182
- }
183
- // Don't retry on last attempt
184
- if (attempt === maxRetries) {
185
- throw error;
186
- }
187
- const delay = calculateRetryDelay(attempt);
188
- yield sleep(delay);
153
+ const onAbort = () => controller.abort();
154
+ parentSignal.addEventListener('abort', onAbort, { once: true });
155
+ // Best-effort cleanup handled below when fetch resolves/rejects
156
+ }
157
+ try {
158
+ // Use a fresh Request object each attempt when possible (avoids body reuse issues).
159
+ const attemptInput = input instanceof Request ? input.clone() : input;
160
+ const attemptInit = Object.assign(Object.assign({}, init), { method: baseMethod, headers, signal: controller.signal });
161
+ const response = yield baseFetch(attemptInput, attemptInit);
162
+ clearTimeout(timeout);
163
+ if (!isRetryableStatus(response.status, response) || attempt === maxRetries) {
164
+ return response;
189
165
  }
166
+ if (writeRequestWithoutIdempotency) {
167
+ return response;
168
+ }
169
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After'));
170
+ const expBackoff = Math.min(retry.maxDelayMs, retry.minDelayMs * Math.pow(2, attempt));
171
+ const jittered = Math.floor(Math.random() * expBackoff);
172
+ const delayMs = Math.max(0, Math.min(retry.maxDelayMs, retryAfterMs !== null && retryAfterMs !== void 0 ? retryAfterMs : jittered));
173
+ (_c = retry.onRetry) === null || _c === void 0 ? void 0 : _c.call(retry, {
174
+ attempt,
175
+ maxRetries,
176
+ delayMs,
177
+ reason: 'http_status',
178
+ status: response.status,
179
+ });
180
+ yield sleep(delayMs);
181
+ continue;
190
182
  }
191
- // This should never be reached, but TypeScript needs it
192
- if (lastResponse) {
193
- return lastResponse;
183
+ catch (err) {
184
+ clearTimeout(timeout);
185
+ // If caller aborted, don't retry
186
+ const isAbort = (err instanceof DOMException && err.name === 'AbortError') ||
187
+ (typeof err === 'object' && err !== null && err.name === 'AbortError');
188
+ if (isAbort) {
189
+ // If our timeout triggered, we can retry; if caller aborted, we should not.
190
+ const callerAborted = (parentSignal === null || parentSignal === void 0 ? void 0 : parentSignal.aborted) === true;
191
+ if (callerAborted || attempt === maxRetries || writeRequestWithoutIdempotency) {
192
+ throw err;
193
+ }
194
+ const expBackoff = Math.min(retry.maxDelayMs, retry.minDelayMs * Math.pow(2, attempt));
195
+ const delayMs = Math.floor(Math.random() * expBackoff);
196
+ (_d = retry.onRetry) === null || _d === void 0 ? void 0 : _d.call(retry, { attempt, maxRetries, delayMs, reason: 'timeout' });
197
+ yield sleep(delayMs);
198
+ lastError = err;
199
+ continue;
200
+ }
201
+ if (attempt === maxRetries || writeRequestWithoutIdempotency) {
202
+ throw err;
203
+ }
204
+ const expBackoff = Math.min(retry.maxDelayMs, retry.minDelayMs * Math.pow(2, attempt));
205
+ const delayMs = Math.floor(Math.random() * expBackoff);
206
+ (_e = retry.onRetry) === null || _e === void 0 ? void 0 : _e.call(retry, { attempt, maxRetries, delayMs, reason: 'network_error' });
207
+ yield sleep(delayMs);
208
+ lastError = err;
209
+ continue;
194
210
  }
195
- throw lastError;
196
- });
197
- };
211
+ }
212
+ // Should be unreachable; keep TypeScript happy
213
+ throw lastError !== null && lastError !== void 0 ? lastError : new Error('Request failed');
214
+ });
198
215
  }
199
- // Import SDK methods from sdk.gen
200
- const sdk_gen_1 = require("./client/sdk.gen");
201
- // Export all generated types and methods for direct use
202
- __exportStar(require("./client/types.gen"), exports);
203
- __exportStar(require("./client/sdk.gen"), exports);
204
- __exportStar(require("./client/client.gen"), exports);
205
- // Configuration
206
- /** Default API base URL for Cyberdesk Cloud API */
207
- const DEFAULT_API_BASE_URL = "https://api.cyberdesk.io";
208
216
  /**
209
- * Create a configured HTTP client with authentication and automatic retries
217
+ * Create a configured HTTP client with authentication
210
218
  *
211
219
  * @internal
212
220
  * @param apiKey - Your Cyberdesk API key
213
221
  * @param baseUrl - API base URL
214
222
  * @param options - Client configuration options
215
- * @returns Configured HTTP client with retry logic
223
+ * @returns Configured HTTP client
216
224
  */
217
225
  function createApiClient(apiKey, baseUrl = DEFAULT_API_BASE_URL, options = {}) {
218
- const { maxRetries = DEFAULT_MAX_RETRIES, fetch: customFetch } = options;
219
- // Use custom fetch if provided, otherwise create retry-enabled fetch
220
- const fetchWithRetry = customFetch !== null && customFetch !== void 0 ? customFetch : createRetryFetch(maxRetries);
226
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
227
+ const { fetch: customFetch } = options;
228
+ const baseFetch = customFetch !== null && customFetch !== void 0 ? customFetch : fetch;
229
+ const retryOptions = {
230
+ maxRetries: (_b = (_a = options.retry) === null || _a === void 0 ? void 0 : _a.maxRetries) !== null && _b !== void 0 ? _b : 3,
231
+ timeoutMs: (_d = (_c = options.retry) === null || _c === void 0 ? void 0 : _c.timeoutMs) !== null && _d !== void 0 ? _d : 30000,
232
+ minDelayMs: (_f = (_e = options.retry) === null || _e === void 0 ? void 0 : _e.minDelayMs) !== null && _f !== void 0 ? _f : 250,
233
+ maxDelayMs: (_h = (_g = options.retry) === null || _g === void 0 ? void 0 : _g.maxDelayMs) !== null && _h !== void 0 ? _h : 8000,
234
+ onRetry: (_j = options.retry) === null || _j === void 0 ? void 0 : _j.onRetry,
235
+ };
236
+ const idempotencyOptions = {
237
+ enabled: (_l = (_k = options.idempotency) === null || _k === void 0 ? void 0 : _k.enabled) !== null && _l !== void 0 ? _l : true,
238
+ headerName: (_o = (_m = options.idempotency) === null || _m === void 0 ? void 0 : _m.headerName) !== null && _o !== void 0 ? _o : 'Idempotency-Key',
239
+ generateKey: (_q = (_p = options.idempotency) === null || _p === void 0 ? void 0 : _p.generateKey) !== null && _q !== void 0 ? _q : defaultGenerateIdempotencyKey,
240
+ };
241
+ const wrappedFetch = createRetryingFetch(baseFetch, {
242
+ retry: retryOptions,
243
+ idempotency: idempotencyOptions,
244
+ });
221
245
  return (0, client_fetch_1.createClient)({
222
246
  baseUrl,
223
247
  headers: {
@@ -225,7 +249,7 @@ function createApiClient(apiKey, baseUrl = DEFAULT_API_BASE_URL, options = {}) {
225
249
  'Authorization': `Bearer ${apiKey}`,
226
250
  'Connection': 'keep-alive',
227
251
  },
228
- fetch: fetchWithRetry,
252
+ fetch: wrappedFetch,
229
253
  });
230
254
  }
231
255
  // Helpers
@@ -240,7 +264,6 @@ function toIsoUtc(value) {
240
264
  * @param apiKey - Your Cyberdesk API key
241
265
  * @param baseUrl - Optional API base URL (defaults to https://api.cyberdesk.io)
242
266
  * @param options - Optional client configuration
243
- * @param options.maxRetries - Maximum retry attempts for failed requests (default: 2)
244
267
  * @returns Configured client with all API endpoints
245
268
  *
246
269
  * @example
@@ -248,12 +271,6 @@ function toIsoUtc(value) {
248
271
  * // Basic usage
249
272
  * const client = createCyberdeskClient('your-api-key');
250
273
  * const machines = await client.machines.list();
251
- *
252
- * // With custom retry configuration
253
- * const client = createCyberdeskClient('your-api-key', undefined, { maxRetries: 3 });
254
- *
255
- * // Disable retries
256
- * const client = createCyberdeskClient('your-api-key', undefined, { maxRetries: 0 });
257
274
  * ```
258
275
  */
259
276
  function createCyberdeskClient(apiKey, baseUrl, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberdesk",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "description": "The official TypeScript SDK for Cyberdesk",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",