quickbase-js 2.4.0 → 2.4.1

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 +144 -76
  2. package/dist/quickbase.esm.js +3 -3
  3. package/dist/quickbase.min.js +3 -3
  4. package/dist/quickbase.min.js.map +1 -1
  5. package/package.json +13 -9
  6. package/src/auth/index.ts +55 -0
  7. package/src/auth/sso-token.ts +143 -0
  8. package/src/auth/temp-token.ts +160 -0
  9. package/src/auth/ticket.ts +329 -0
  10. package/src/auth/token-cache.ts +97 -0
  11. package/src/auth/types.ts +50 -0
  12. package/src/auth/user-token.ts +38 -0
  13. package/src/cli/schema.ts +512 -0
  14. package/src/client/index.ts +36 -0
  15. package/src/client/pagination.ts +337 -0
  16. package/src/client/request.ts +544 -0
  17. package/src/client/retry.ts +169 -0
  18. package/src/client/throttle.ts +105 -0
  19. package/src/core/config.ts +160 -0
  20. package/src/core/dates.ts +63 -0
  21. package/src/core/errors.ts +349 -0
  22. package/src/core/index.ts +50 -0
  23. package/src/core/logger.ts +113 -0
  24. package/src/core/schema.ts +213 -0
  25. package/src/core/transform.ts +295 -0
  26. package/src/core/types.ts +337 -0
  27. package/src/generated/client.ts +1785 -0
  28. package/src/generated/operations.json +672 -0
  29. package/src/generated/types.ts +4884 -0
  30. package/src/index.ts +259 -0
  31. package/src/xml/codepages.ts +119 -0
  32. package/src/xml/dbvars.ts +75 -0
  33. package/src/xml/errors.ts +133 -0
  34. package/src/xml/fields.ts +121 -0
  35. package/src/xml/groups.ts +560 -0
  36. package/src/xml/html.ts +152 -0
  37. package/src/xml/index.ts +1196 -0
  38. package/src/xml/metadata.ts +111 -0
  39. package/src/xml/records.ts +301 -0
  40. package/src/xml/request.ts +238 -0
  41. package/src/xml/roles.ts +321 -0
  42. package/src/xml/schema.ts +436 -0
  43. package/src/xml/transform.ts +381 -0
  44. package/src/xml/types.ts +602 -0
  45. package/src/xml/users.ts +221 -0
  46. package/src/xml/webhooks.ts +260 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Client-side throttling (optional)
3
+ *
4
+ * Implements a sliding window rate limiter that matches QuickBase's
5
+ * "100 requests per 10 seconds per user token" limit.
6
+ *
7
+ * This is OPTIONAL - QuickBase handles rate limiting server-side.
8
+ * Enable this if you want to avoid hitting 429s proactively.
9
+ */
10
+
11
+ export class SlidingWindowThrottle {
12
+ private readonly requestsPer10Seconds: number;
13
+ private readonly timestamps: number[] = [];
14
+
15
+ constructor(requestsPer10Seconds: number = 100) {
16
+ this.requestsPer10Seconds = requestsPer10Seconds;
17
+ }
18
+
19
+ /**
20
+ * Wait until a request slot is available
21
+ */
22
+ async acquire(): Promise<void> {
23
+ const now = Date.now();
24
+ const windowStart = now - 10000; // 10 second window
25
+
26
+ // Remove timestamps outside the window
27
+ while (this.timestamps.length > 0 && this.timestamps[0]! < windowStart) {
28
+ this.timestamps.shift();
29
+ }
30
+
31
+ // If we're at the limit, wait until the oldest request exits the window
32
+ if (this.timestamps.length >= this.requestsPer10Seconds) {
33
+ const oldestTimestamp = this.timestamps[0]!;
34
+ const waitTime = oldestTimestamp + 10000 - now;
35
+
36
+ if (waitTime > 0) {
37
+ await this.sleep(waitTime);
38
+ // Recurse to recheck after waiting
39
+ return this.acquire();
40
+ }
41
+ }
42
+
43
+ // Record this request
44
+ this.timestamps.push(Date.now());
45
+ }
46
+
47
+ /**
48
+ * Get the number of requests made in the current window
49
+ */
50
+ getWindowCount(): number {
51
+ const windowStart = Date.now() - 10000;
52
+ return this.timestamps.filter((t) => t >= windowStart).length;
53
+ }
54
+
55
+ /**
56
+ * Get remaining requests available in the current window
57
+ */
58
+ getRemaining(): number {
59
+ return Math.max(0, this.requestsPer10Seconds - this.getWindowCount());
60
+ }
61
+
62
+ /**
63
+ * Reset the throttle state
64
+ */
65
+ reset(): void {
66
+ this.timestamps.length = 0;
67
+ }
68
+
69
+ private sleep(ms: number): Promise<void> {
70
+ return new Promise((resolve) => setTimeout(resolve, ms));
71
+ }
72
+ }
73
+
74
+ /**
75
+ * No-op throttle for when proactive throttling is disabled
76
+ */
77
+ export class NoOpThrottle {
78
+ async acquire(): Promise<void> {
79
+ // No-op
80
+ }
81
+
82
+ getWindowCount(): number {
83
+ return 0;
84
+ }
85
+
86
+ getRemaining(): number {
87
+ return Infinity;
88
+ }
89
+
90
+ reset(): void {
91
+ // No-op
92
+ }
93
+ }
94
+
95
+ export type Throttle = SlidingWindowThrottle | NoOpThrottle;
96
+
97
+ export function createThrottle(config: {
98
+ enabled: boolean;
99
+ requestsPer10Seconds: number;
100
+ }): Throttle {
101
+ if (config.enabled) {
102
+ return new SlidingWindowThrottle(config.requestsPer10Seconds);
103
+ }
104
+ return new NoOpThrottle();
105
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Configuration resolution and validation
3
+ */
4
+
5
+ import type { QuickbaseConfig, ResolvedConfig, RetryConfig } from './types.js';
6
+ import { DEFAULT_CONFIG } from './types.js';
7
+ import { resolveSchema } from './schema.js';
8
+
9
+ /**
10
+ * Resolve user configuration with defaults
11
+ */
12
+ export function resolveConfig(config: QuickbaseConfig): ResolvedConfig {
13
+ validateConfig(config);
14
+
15
+ const retry: Required<RetryConfig> = {
16
+ maxAttempts: config.rateLimit?.retry?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
17
+ initialDelayMs: config.rateLimit?.retry?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
18
+ maxDelayMs: config.rateLimit?.retry?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
19
+ multiplier: config.rateLimit?.retry?.multiplier ?? DEFAULT_CONFIG.retry.multiplier,
20
+ };
21
+
22
+ const proactiveThrottle = {
23
+ enabled: config.rateLimit?.proactiveThrottle?.enabled ?? DEFAULT_CONFIG.proactiveThrottle.enabled,
24
+ requestsPer10Seconds:
25
+ config.rateLimit?.proactiveThrottle?.requestsPer10Seconds ??
26
+ DEFAULT_CONFIG.proactiveThrottle.requestsPer10Seconds,
27
+ };
28
+
29
+ return {
30
+ realm: config.realm,
31
+ baseUrl: config.baseUrl ?? DEFAULT_CONFIG.baseUrl,
32
+ timeout: config.timeout ?? DEFAULT_CONFIG.timeout,
33
+ debug: config.debug ?? DEFAULT_CONFIG.debug,
34
+ fetchApi: config.fetchApi ?? globalThis.fetch.bind(globalThis),
35
+ auth: config.auth,
36
+ retry,
37
+ proactiveThrottle,
38
+ onRateLimit: config.rateLimit?.onRateLimit,
39
+ autoPaginate: config.autoPaginate ?? DEFAULT_CONFIG.autoPaginate,
40
+ convertDates: config.convertDates ?? DEFAULT_CONFIG.convertDates,
41
+ schema: resolveSchema(config.schema),
42
+ readOnly: config.readOnly ?? false,
43
+ appToken: config.appToken,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Validate configuration and throw if invalid
49
+ */
50
+ function validateConfig(config: QuickbaseConfig): void {
51
+ if (!config.realm) {
52
+ throw new Error('QuickBase realm is required');
53
+ }
54
+
55
+ if (typeof config.realm !== 'string') {
56
+ throw new Error('QuickBase realm must be a string');
57
+ }
58
+
59
+ // Remove .quickbase.com suffix if provided
60
+ if (config.realm.includes('.')) {
61
+ throw new Error(
62
+ 'Realm should be just the subdomain (e.g., "mycompany" not "mycompany.quickbase.com")'
63
+ );
64
+ }
65
+
66
+ if (!config.auth) {
67
+ throw new Error('Authentication configuration is required');
68
+ }
69
+
70
+ validateAuthConfig(config.auth);
71
+
72
+ if (config.timeout !== undefined) {
73
+ if (typeof config.timeout !== 'number' || config.timeout <= 0) {
74
+ throw new Error('Timeout must be a positive number');
75
+ }
76
+ }
77
+
78
+ if (config.rateLimit?.retry) {
79
+ validateRetryConfig(config.rateLimit.retry);
80
+ }
81
+
82
+ if (config.rateLimit?.proactiveThrottle) {
83
+ validateThrottleConfig(config.rateLimit.proactiveThrottle);
84
+ }
85
+ }
86
+
87
+ function validateAuthConfig(auth: QuickbaseConfig['auth']): void {
88
+ switch (auth.type) {
89
+ case 'user-token':
90
+ if (!auth.userToken) {
91
+ throw new Error('User token is required for user-token auth');
92
+ }
93
+ break;
94
+
95
+ case 'temp-token':
96
+ // userToken is optional for temp-token auth
97
+ // When running in Code Pages, the browser session is used
98
+ break;
99
+
100
+ case 'sso':
101
+ if (!auth.samlToken) {
102
+ throw new Error('SAML token is required for SSO auth');
103
+ }
104
+ break;
105
+
106
+ // XML-API-TICKET: Remove this case if XML API is discontinued
107
+ case 'ticket':
108
+ if (!auth.username) {
109
+ throw new Error('Username is required for ticket auth');
110
+ }
111
+ if (!auth.password) {
112
+ throw new Error('Password is required for ticket auth');
113
+ }
114
+ break;
115
+
116
+ default:
117
+ throw new Error(`Unknown auth type: ${(auth as { type: string }).type}`);
118
+ }
119
+ }
120
+
121
+ function validateRetryConfig(retry: RetryConfig): void {
122
+ if (retry.maxAttempts !== undefined) {
123
+ if (typeof retry.maxAttempts !== 'number' || retry.maxAttempts < 0) {
124
+ throw new Error('maxAttempts must be a non-negative number');
125
+ }
126
+ }
127
+
128
+ if (retry.initialDelayMs !== undefined) {
129
+ if (typeof retry.initialDelayMs !== 'number' || retry.initialDelayMs < 0) {
130
+ throw new Error('initialDelayMs must be a non-negative number');
131
+ }
132
+ }
133
+
134
+ if (retry.maxDelayMs !== undefined) {
135
+ if (typeof retry.maxDelayMs !== 'number' || retry.maxDelayMs < 0) {
136
+ throw new Error('maxDelayMs must be a non-negative number');
137
+ }
138
+ }
139
+
140
+ if (retry.multiplier !== undefined) {
141
+ if (typeof retry.multiplier !== 'number' || retry.multiplier < 1) {
142
+ throw new Error('multiplier must be a number >= 1');
143
+ }
144
+ }
145
+ }
146
+
147
+ function validateThrottleConfig(throttle: { enabled?: boolean; requestsPer10Seconds?: number }): void {
148
+ if (throttle.requestsPer10Seconds !== undefined) {
149
+ if (typeof throttle.requestsPer10Seconds !== 'number' || throttle.requestsPer10Seconds <= 0) {
150
+ throw new Error('requestsPer10Seconds must be a positive number');
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Build the realm hostname from the realm name
157
+ */
158
+ export function getRealmHostname(realm: string): string {
159
+ return `${realm}.quickbase.com`;
160
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Date transformation utilities
3
+ *
4
+ * Converts ISO 8601 date strings in API responses to JavaScript Date objects.
5
+ */
6
+
7
+ /**
8
+ * ISO 8601 date pattern
9
+ * Matches: 2024-01-15, 2024-01-15T10:30:00, 2024-01-15T10:30:00.000Z, etc.
10
+ */
11
+ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:?\d{2})?)?$/;
12
+
13
+ /**
14
+ * Check if a string looks like an ISO 8601 date
15
+ */
16
+ export function isIsoDateString(value: string): boolean {
17
+ return ISO_DATE_PATTERN.test(value);
18
+ }
19
+
20
+ /**
21
+ * Recursively transform ISO date strings to Date objects in an object
22
+ *
23
+ * @param obj - The object to transform
24
+ * @param enabled - Whether to perform conversion (pass false to skip)
25
+ * @returns A new object with date strings converted to Date objects
26
+ */
27
+ export function transformDates<T>(obj: T, enabled: boolean): T {
28
+ if (!enabled || obj === null || obj === undefined) {
29
+ return obj;
30
+ }
31
+
32
+ if (typeof obj !== 'object') {
33
+ return obj;
34
+ }
35
+
36
+ if (Array.isArray(obj)) {
37
+ return obj.map((item) => transformDates(item, enabled)) as T;
38
+ }
39
+
40
+ if (obj instanceof Date) {
41
+ return obj;
42
+ }
43
+
44
+ const result: Record<string, unknown> = {};
45
+
46
+ for (const [key, value] of Object.entries(obj)) {
47
+ if (typeof value === 'string' && isIsoDateString(value)) {
48
+ // Convert ISO date string to Date object
49
+ result[key] = new Date(value);
50
+ } else if (value instanceof Date) {
51
+ // Keep existing Date objects as-is
52
+ result[key] = value;
53
+ } else if (typeof value === 'object' && value !== null) {
54
+ // Recursively transform nested objects
55
+ result[key] = transformDates(value, enabled);
56
+ } else {
57
+ // Keep other values as-is
58
+ result[key] = value;
59
+ }
60
+ }
61
+
62
+ return result as T;
63
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Custom error types for the QuickBase SDK
3
+ */
4
+
5
+ import type { RateLimitInfo } from './types.js';
6
+
7
+ /**
8
+ * Base error class for all QuickBase SDK errors
9
+ */
10
+ export class QuickbaseError extends Error {
11
+ readonly statusCode: number;
12
+ readonly description?: string;
13
+ readonly rayId?: string;
14
+
15
+ constructor(
16
+ message: string,
17
+ statusCode: number,
18
+ options?: {
19
+ description?: string;
20
+ rayId?: string;
21
+ cause?: Error;
22
+ }
23
+ ) {
24
+ super(message, { cause: options?.cause });
25
+ this.name = 'QuickbaseError';
26
+ this.statusCode = statusCode;
27
+ this.description = options?.description;
28
+ this.rayId = options?.rayId;
29
+
30
+ // Maintains proper stack trace in V8 environments
31
+ if (Error.captureStackTrace) {
32
+ Error.captureStackTrace(this, QuickbaseError);
33
+ }
34
+ }
35
+
36
+ toJSON() {
37
+ return {
38
+ name: this.name,
39
+ message: this.message,
40
+ statusCode: this.statusCode,
41
+ description: this.description,
42
+ rayId: this.rayId,
43
+ };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Error thrown when rate limited (HTTP 429)
49
+ */
50
+ export class RateLimitError extends QuickbaseError {
51
+ readonly retryAfter?: number;
52
+ readonly rateLimitInfo: RateLimitInfo;
53
+
54
+ constructor(rateLimitInfo: RateLimitInfo, message?: string) {
55
+ super(
56
+ message || `Rate limited. Retry after ${rateLimitInfo.retryAfter ?? 'unknown'} seconds`,
57
+ 429,
58
+ { rayId: rateLimitInfo.qbApiRay || rateLimitInfo.cfRay }
59
+ );
60
+ this.name = 'RateLimitError';
61
+ this.retryAfter = rateLimitInfo.retryAfter;
62
+ this.rateLimitInfo = rateLimitInfo;
63
+ }
64
+
65
+ toJSON() {
66
+ return {
67
+ ...super.toJSON(),
68
+ retryAfter: this.retryAfter,
69
+ rateLimitInfo: this.rateLimitInfo,
70
+ };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Error thrown when authentication fails (HTTP 401)
76
+ */
77
+ export class AuthenticationError extends QuickbaseError {
78
+ constructor(message: string, options?: { rayId?: string; cause?: Error }) {
79
+ super(message, 401, options);
80
+ this.name = 'AuthenticationError';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Error thrown when authorization fails (HTTP 403)
86
+ */
87
+ export class AuthorizationError extends QuickbaseError {
88
+ constructor(message: string, options?: { rayId?: string; cause?: Error }) {
89
+ super(message, 403, options);
90
+ this.name = 'AuthorizationError';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Error thrown when a resource is not found (HTTP 404)
96
+ */
97
+ export class NotFoundError extends QuickbaseError {
98
+ constructor(message: string, options?: { rayId?: string; cause?: Error }) {
99
+ super(message, 404, options);
100
+ this.name = 'NotFoundError';
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Error thrown for bad requests (HTTP 400)
106
+ */
107
+ export class ValidationError extends QuickbaseError {
108
+ readonly errors?: Array<{ field?: string; message: string }>;
109
+
110
+ constructor(
111
+ message: string,
112
+ options?: {
113
+ description?: string;
114
+ rayId?: string;
115
+ errors?: Array<{ field?: string; message: string }>;
116
+ cause?: Error;
117
+ }
118
+ ) {
119
+ super(message, 400, options);
120
+ this.name = 'ValidationError';
121
+ this.errors = options?.errors;
122
+ }
123
+
124
+ toJSON() {
125
+ return {
126
+ ...super.toJSON(),
127
+ errors: this.errors,
128
+ };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Error thrown when request times out
134
+ */
135
+ export class TimeoutError extends QuickbaseError {
136
+ readonly timeoutMs: number;
137
+
138
+ constructor(timeoutMs: number, options?: { rayId?: string; cause?: Error }) {
139
+ super(`Request timed out after ${timeoutMs}ms`, 0, options);
140
+ this.name = 'TimeoutError';
141
+ this.timeoutMs = timeoutMs;
142
+ }
143
+
144
+ toJSON() {
145
+ return {
146
+ ...super.toJSON(),
147
+ timeoutMs: this.timeoutMs,
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Error thrown for server errors (HTTP 5xx)
154
+ */
155
+ export class ServerError extends QuickbaseError {
156
+ constructor(
157
+ statusCode: number,
158
+ message: string,
159
+ options?: { description?: string; rayId?: string; cause?: Error }
160
+ ) {
161
+ super(message, statusCode, options);
162
+ this.name = 'ServerError';
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Error thrown when an authentication ticket is invalid or expired.
168
+ * Specifically for ticket-based authentication (XML API).
169
+ */
170
+ export class QuickbaseTicketError extends QuickbaseError {
171
+ readonly code: number;
172
+
173
+ constructor(message: string, code: number, options?: { description?: string; action?: string; cause?: Error }) {
174
+ super(message, 401, options);
175
+ this.name = 'QuickbaseTicketError';
176
+ this.code = code;
177
+ }
178
+
179
+ toJSON() {
180
+ return {
181
+ ...super.toJSON(),
182
+ code: this.code,
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Error thrown when an application token is missing or invalid.
189
+ * Specifically for apps that require app tokens (XML API).
190
+ */
191
+ export class QuickbaseAppTokenError extends QuickbaseError {
192
+ readonly code: number;
193
+
194
+ constructor(message: string, code: number, options?: { description?: string; action?: string; cause?: Error }) {
195
+ super(message, 403, options);
196
+ this.name = 'QuickbaseAppTokenError';
197
+ this.code = code;
198
+ }
199
+
200
+ toJSON() {
201
+ return {
202
+ ...super.toJSON(),
203
+ code: this.code,
204
+ };
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Error thrown by the legacy QuickBase XML API.
210
+ * Wraps XML-specific error codes and messages.
211
+ */
212
+ export class QuickbaseXmlError extends QuickbaseError {
213
+ readonly code: number;
214
+ readonly text: string;
215
+ readonly action?: string;
216
+
217
+ constructor(code: number, text: string, options?: { description?: string; action?: string; cause?: Error }) {
218
+ const message = options?.description
219
+ ? `XML API error ${code}: ${text} (${options.description})`
220
+ : `XML API error ${code}: ${text}`;
221
+
222
+ // Most XML errors return HTTP 200, so we use 0 or a mapped status if known
223
+ super(message, 0, { rayId: undefined, description: options?.description, cause: options?.cause });
224
+ this.name = 'QuickbaseXmlError';
225
+ this.code = code;
226
+ this.text = text;
227
+ this.action = options?.action;
228
+ }
229
+
230
+ toJSON() {
231
+ return {
232
+ ...super.toJSON(),
233
+ code: this.code,
234
+ text: this.text,
235
+ action: this.action,
236
+ };
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Parse an error response from the QuickBase API
242
+ */
243
+ export async function parseErrorResponse(
244
+ response: Response,
245
+ requestUrl: string
246
+ ): Promise<QuickbaseError> {
247
+ const rayId = response.headers.get('qb-api-ray') || response.headers.get('cf-ray') || undefined;
248
+
249
+ let body: { message?: string; description?: string; errors?: unknown[] } | null = null;
250
+ try {
251
+ body = await response.json() as { message?: string; description?: string; errors?: unknown[] };
252
+ } catch {
253
+ // Response body is not JSON
254
+ }
255
+
256
+ const message = body?.message || response.statusText || 'Unknown error';
257
+ const description = body?.description;
258
+
259
+ switch (response.status) {
260
+ case 400:
261
+ return new ValidationError(message, {
262
+ description,
263
+ rayId,
264
+ errors: body?.errors as Array<{ field?: string; message: string }>,
265
+ });
266
+
267
+ case 401:
268
+ return new AuthenticationError(message, { rayId });
269
+
270
+ case 403:
271
+ return new AuthorizationError(message, { rayId });
272
+
273
+ case 404:
274
+ return new NotFoundError(message, { rayId });
275
+
276
+ case 429: {
277
+ const retryAfter = response.headers.get('Retry-After');
278
+ const rateLimitInfo: RateLimitInfo = {
279
+ timestamp: new Date(),
280
+ requestUrl,
281
+ httpStatus: 429,
282
+ retryAfter: retryAfter ? parseInt(retryAfter, 10) : undefined,
283
+ cfRay: response.headers.get('cf-ray') || undefined,
284
+ tid: response.headers.get('tid') || undefined,
285
+ qbApiRay: response.headers.get('qb-api-ray') || undefined,
286
+ attempt: 1, // Will be updated by caller
287
+ };
288
+ return new RateLimitError(rateLimitInfo, message);
289
+ }
290
+
291
+ default:
292
+ if (response.status >= 500) {
293
+ return new ServerError(response.status, message, { description, rayId });
294
+ }
295
+ return new QuickbaseError(message, response.status, { description, rayId });
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Check if an error is retryable
301
+ */
302
+ export function isRetryableError(error: unknown): boolean {
303
+ if (error instanceof RateLimitError) {
304
+ return true;
305
+ }
306
+ if (error instanceof ServerError) {
307
+ return true;
308
+ }
309
+ if (error instanceof TimeoutError) {
310
+ return true;
311
+ }
312
+ return false;
313
+ }
314
+
315
+ /**
316
+ * Error thrown when a write operation is attempted in read-only mode.
317
+ * Provides defense-in-depth for both JSON API and XML API requests.
318
+ */
319
+ export class ReadOnlyError extends Error {
320
+ readonly method: string;
321
+ readonly path: string;
322
+ readonly action?: string; // For XML API actions
323
+
324
+ constructor(method: string, path: string, action?: string) {
325
+ const msg = action
326
+ ? `Read-only mode: write operation blocked (XML action: ${action})`
327
+ : `Read-only mode: write operation blocked (${method} ${path})`;
328
+ super(msg);
329
+ this.name = 'ReadOnlyError';
330
+ this.method = method;
331
+ this.path = path;
332
+ this.action = action;
333
+
334
+ // Maintains proper stack trace in V8 environments
335
+ if (Error.captureStackTrace) {
336
+ Error.captureStackTrace(this, ReadOnlyError);
337
+ }
338
+ }
339
+
340
+ toJSON() {
341
+ return {
342
+ name: this.name,
343
+ message: this.message,
344
+ method: this.method,
345
+ path: this.path,
346
+ action: this.action,
347
+ };
348
+ }
349
+ }