@suiteportal/connector 0.2.2 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -25,7 +25,9 @@ __export(index_exports, {
25
25
  NetSuiteError: () => NetSuiteError,
26
26
  RateLimitError: () => RateLimitError,
27
27
  RateLimiter: () => RateLimiter,
28
+ RequestThrottler: () => RequestThrottler,
28
29
  TimeoutError: () => TimeoutError,
30
+ TokenBucket: () => TokenBucket,
29
31
  buildAuthorizationHeader: () => buildAuthorizationHeader,
30
32
  buildSignatureBaseString: () => buildSignatureBaseString,
31
33
  createRecord: () => createRecord,
@@ -42,6 +44,7 @@ __export(index_exports, {
42
44
  percentEncode: () => percentEncode,
43
45
  resolveConfig: () => resolveConfig,
44
46
  signHmacSha256: () => signHmacSha256,
47
+ transformRecord: () => transformRecord,
45
48
  updateRecord: () => updateRecord,
46
49
  upsertRecord: () => upsertRecord,
47
50
  withRetry: () => withRetry
@@ -50,8 +53,9 @@ module.exports = __toCommonJS(index_exports);
50
53
 
51
54
  // src/auth/oauth.ts
52
55
  var import_node_crypto = require("crypto");
56
+ var RFC5849_EXTRA = /[!'()*]/g;
53
57
  function percentEncode(str) {
54
- return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
58
+ return encodeURIComponent(str).replace(RFC5849_EXTRA, (c) => {
55
59
  return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;
56
60
  });
57
61
  }
@@ -62,9 +66,12 @@ function generateTimestamp() {
62
66
  return Math.floor(Date.now() / 1e3).toString();
63
67
  }
64
68
  function buildSignatureBaseString(method, baseUrl, params) {
65
- const sorted = [...params].sort((a, b) => {
66
- const keyCompare = a[0].localeCompare(b[0]);
67
- return keyCompare !== 0 ? keyCompare : a[1].localeCompare(b[1]);
69
+ const sorted = params.slice().sort((a, b) => {
70
+ if (a[0] < b[0]) return -1;
71
+ if (a[0] > b[0]) return 1;
72
+ if (a[1] < b[1]) return -1;
73
+ if (a[1] > b[1]) return 1;
74
+ return 0;
68
75
  });
69
76
  const paramString = sorted.map(([k, v]) => `${percentEncode(k)}=${percentEncode(v)}`).join("&");
70
77
  return [
@@ -88,12 +95,18 @@ function generateOAuthSignature(params) {
88
95
  oauth_signature_method: "HMAC-SHA256",
89
96
  oauth_version: "1.0"
90
97
  };
91
- const urlObj = new URL(params.url);
92
- const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
98
+ const qIdx = params.url.indexOf("?");
99
+ const baseUrl = qIdx === -1 ? params.url : params.url.slice(0, qIdx);
93
100
  const allParams = Object.entries(oauthParams);
94
- urlObj.searchParams.forEach((value, key) => {
95
- allParams.push([key, value]);
96
- });
101
+ if (qIdx !== -1) {
102
+ const qs = params.url.slice(qIdx + 1);
103
+ for (const pair of qs.split("&")) {
104
+ const eqIdx = pair.indexOf("=");
105
+ if (eqIdx !== -1) {
106
+ allParams.push([decodeURIComponent(pair.slice(0, eqIdx)), decodeURIComponent(pair.slice(eqIdx + 1))]);
107
+ }
108
+ }
109
+ }
97
110
  const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);
98
111
  const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);
99
112
  return { signature, oauthParams };
@@ -139,6 +152,8 @@ function resolveConfig(config) {
139
152
  timeout: config.timeout ?? DEFAULTS.timeout,
140
153
  concurrency: config.concurrency ?? DEFAULTS.concurrency,
141
154
  maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,
155
+ requestsPerSecond: config.requestsPerSecond,
156
+ burstSize: config.burstSize,
142
157
  baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId)
143
158
  };
144
159
  }
@@ -231,6 +246,95 @@ var RateLimiter = class {
231
246
  }
232
247
  };
233
248
 
249
+ // src/http/token-bucket.ts
250
+ var TokenBucket = class {
251
+ tokens;
252
+ maxTokens;
253
+ refillRate;
254
+ // tokens per ms
255
+ lastRefill;
256
+ pendingTimers = /* @__PURE__ */ new Set();
257
+ constructor(options) {
258
+ this.maxTokens = options.burstSize ?? options.requestsPerSecond;
259
+ this.refillRate = options.requestsPerSecond / 1e3;
260
+ this.tokens = this.maxTokens;
261
+ this.lastRefill = Date.now();
262
+ }
263
+ /**
264
+ * Acquire a token, waiting if necessary.
265
+ * Resolves immediately if a token is available, otherwise
266
+ * delays until the bucket refills enough for one token.
267
+ */
268
+ async acquire() {
269
+ this.refill();
270
+ if (this.tokens >= 1) {
271
+ this.tokens--;
272
+ return;
273
+ }
274
+ const waitMs = (1 - this.tokens) / this.refillRate;
275
+ await new Promise((resolve) => {
276
+ const timer = setTimeout(() => {
277
+ this.pendingTimers.delete(timer);
278
+ resolve();
279
+ }, waitMs);
280
+ this.pendingTimers.add(timer);
281
+ });
282
+ this.refill();
283
+ this.tokens--;
284
+ }
285
+ /** Number of tokens currently available. */
286
+ get availableTokens() {
287
+ this.refill();
288
+ return this.tokens;
289
+ }
290
+ /** Cancel all pending timers. Call on client teardown. */
291
+ destroy() {
292
+ for (const timer of this.pendingTimers) {
293
+ clearTimeout(timer);
294
+ }
295
+ this.pendingTimers.clear();
296
+ }
297
+ refill() {
298
+ const now = Date.now();
299
+ const elapsed = now - this.lastRefill;
300
+ if (elapsed <= 0) return;
301
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
302
+ this.lastRefill = now;
303
+ }
304
+ };
305
+
306
+ // src/http/request-throttler.ts
307
+ var RequestThrottler = class {
308
+ semaphore;
309
+ tokenBucket;
310
+ constructor(options = {}) {
311
+ this.semaphore = new RateLimiter(options.concurrency ?? 5);
312
+ this.tokenBucket = options.requestsPerSecond != null ? new TokenBucket({
313
+ requestsPerSecond: options.requestsPerSecond,
314
+ burstSize: options.burstSize
315
+ }) : null;
316
+ }
317
+ /** Run an async function within both rate and concurrency limits. */
318
+ async run(fn) {
319
+ if (this.tokenBucket) {
320
+ await this.tokenBucket.acquire();
321
+ }
322
+ return this.semaphore.run(fn);
323
+ }
324
+ /** Number of currently active tasks. */
325
+ get activeCount() {
326
+ return this.semaphore.activeCount;
327
+ }
328
+ /** Number of tasks waiting for a slot. */
329
+ get waitingCount() {
330
+ return this.semaphore.waitingCount;
331
+ }
332
+ /** Cancel pending token bucket timers. Call on client teardown. */
333
+ destroy() {
334
+ this.tokenBucket?.destroy();
335
+ }
336
+ };
337
+
234
338
  // src/http/retry.ts
235
339
  async function withRetry(fn, options) {
236
340
  const { maxRetries, baseDelay = 500, maxDelay = 3e4 } = options;
@@ -266,17 +370,22 @@ function sleep(ms) {
266
370
  // src/http/client.ts
267
371
  var NetSuiteClient = class {
268
372
  config;
269
- rateLimiter;
373
+ throttler;
270
374
  constructor(config) {
271
375
  this.config = resolveConfig(config);
272
- this.rateLimiter = new RateLimiter(this.config.concurrency);
376
+ this.throttler = new RequestThrottler({
377
+ concurrency: this.config.concurrency,
378
+ requestsPerSecond: this.config.requestsPerSecond,
379
+ burstSize: this.config.burstSize
380
+ });
273
381
  }
274
382
  /** Make an authenticated request to the NetSuite REST API. */
275
383
  async request(options) {
276
- return this.rateLimiter.run(
384
+ const shouldRetry = options.retry ?? options.method === "GET";
385
+ return this.throttler.run(
277
386
  () => withRetry(
278
387
  () => this.executeRequest(options),
279
- { maxRetries: this.config.maxRetries }
388
+ { maxRetries: shouldRetry ? this.config.maxRetries : 0 }
280
389
  )
281
390
  );
282
391
  }
@@ -288,6 +397,10 @@ var NetSuiteClient = class {
288
397
  get resolvedConfig() {
289
398
  return this.config;
290
399
  }
400
+ /** Release resources (cancel pending token bucket timers). */
401
+ destroy() {
402
+ this.throttler.destroy();
403
+ }
291
404
  async executeRequest(options) {
292
405
  const url = this.buildUrl(options.path, options.query);
293
406
  const realm = getRealm(this.config.accountId);
@@ -306,8 +419,9 @@ var NetSuiteClient = class {
306
419
  Accept: "application/json",
307
420
  ...options.headers
308
421
  };
422
+ const timeoutMs = options.timeout ?? this.config.timeout;
309
423
  const controller = new AbortController();
310
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
424
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
311
425
  try {
312
426
  const response = await fetch(url, {
313
427
  method: options.method,
@@ -325,7 +439,7 @@ var NetSuiteClient = class {
325
439
  clearTimeout(timeoutId);
326
440
  if (error instanceof NetSuiteError) throw error;
327
441
  if (error instanceof DOMException && error.name === "AbortError") {
328
- throw new TimeoutError(this.config.timeout);
442
+ throw new TimeoutError(timeoutMs);
329
443
  }
330
444
  throw new NetSuiteError(
331
445
  `Network error: ${error instanceof Error ? error.message : String(error)}`,
@@ -386,7 +500,9 @@ async function executeSuiteQL(client, query, options) {
386
500
  query: {
387
501
  limit: limit.toString(),
388
502
  offset: offset.toString()
389
- }
503
+ },
504
+ retry: true
505
+ // POST-for-query is idempotent
390
506
  });
391
507
  const items = (response.data.items ?? []).map(({ links, ...rest }) => rest);
392
508
  return {
@@ -453,6 +569,39 @@ async function getRecord(client, recordType, id) {
453
569
  });
454
570
  return response.data;
455
571
  }
572
+
573
+ // src/rest/transform.ts
574
+ var RECORD_BASE2 = "/services/rest/record/v1";
575
+ var DEFAULT_TRANSFORM_TIMEOUT = 12e4;
576
+ async function transformRecord(client, sourceType, sourceId, targetType, options) {
577
+ const response = await client.request({
578
+ method: "POST",
579
+ path: `${RECORD_BASE2}/${sourceType}/${sourceId}/!transform/${targetType}`,
580
+ body: options?.body ?? {},
581
+ timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,
582
+ retry: false
583
+ });
584
+ const location = response.headers.get("Location");
585
+ let id;
586
+ if (location) {
587
+ const match = location.match(/\/(\d+)$/);
588
+ if (match) {
589
+ id = match[1];
590
+ }
591
+ }
592
+ if (!id && response.data) {
593
+ const rawId = response.data["id"] ?? response.data["internalid"] ?? response.data["internalId"];
594
+ if (rawId != null) {
595
+ id = String(rawId);
596
+ }
597
+ }
598
+ if (!id) {
599
+ throw new Error(
600
+ `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. Location header: ${location ?? "(none)"}`
601
+ );
602
+ }
603
+ return { id, type: targetType };
604
+ }
456
605
  // Annotate the CommonJS export names for ESM import in node:
457
606
  0 && (module.exports = {
458
607
  AuthError,
@@ -460,7 +609,9 @@ async function getRecord(client, recordType, id) {
460
609
  NetSuiteError,
461
610
  RateLimitError,
462
611
  RateLimiter,
612
+ RequestThrottler,
463
613
  TimeoutError,
614
+ TokenBucket,
464
615
  buildAuthorizationHeader,
465
616
  buildSignatureBaseString,
466
617
  createRecord,
@@ -477,6 +628,7 @@ async function getRecord(client, recordType, id) {
477
628
  percentEncode,
478
629
  resolveConfig,
479
630
  signHmacSha256,
631
+ transformRecord,
480
632
  updateRecord,
481
633
  upsertRecord,
482
634
  withRetry
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts"],"sourcesContent":["// Core client\nexport { NetSuiteClient } from './http/client.js';\n\n// SuiteQL\nexport { executeSuiteQL } from './suiteql/executor.js';\nexport { executeSuiteQLPaginated } from './suiteql/paginator.js';\nexport type { PaginationOptions } from './suiteql/paginator.js';\n\n// Auth (exposed for advanced use / testing)\nexport { buildAuthorizationHeader } from './auth/headers.js';\nexport {\n generateOAuthSignature,\n percentEncode,\n buildSignatureBaseString,\n signHmacSha256,\n generateNonce,\n generateTimestamp,\n} from './auth/oauth.js';\n\n// REST Record API\nexport { createRecord, updateRecord, deleteRecord, upsertRecord, getRecord } from './rest/record.js';\n\n// Config\nexport { resolveConfig, deriveBaseUrl, getRealm } from './config.js';\nexport type { ResolvedConfig } from './config.js';\n\n// Errors\nexport {\n NetSuiteError,\n AuthError,\n RateLimitError,\n TimeoutError,\n isRetryableStatus,\n} from './http/errors.js';\n\n// Rate limiter\nexport { RateLimiter } from './http/rate-limiter.js';\n\n// Retry\nexport { withRetry } from './http/retry.js';\nexport type { RetryOptions } from './http/retry.js';\n\n// Types\nexport type {\n NetSuiteConfig,\n SuiteQLRow,\n SuiteQLResult,\n SuiteQLResponse,\n OAuthParams,\n RequestOptions,\n NetSuiteResponse,\n} from './types.js';\n","import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {\n baseUrl: string;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RateLimiter } from './rate-limiter.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.rateLimiter = new RateLimiter(this.config.concurrency);\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n return this.rateLimiter.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: this.config.maxRetries },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(this.config.timeout);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/token-bucket.ts","../src/http/request-throttler.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts","../src/rest/transform.ts"],"sourcesContent":["// Core client\nexport { NetSuiteClient } from './http/client.js';\n\n// SuiteQL\nexport { executeSuiteQL } from './suiteql/executor.js';\nexport { executeSuiteQLPaginated } from './suiteql/paginator.js';\nexport type { PaginationOptions } from './suiteql/paginator.js';\n\n// Auth (exposed for advanced use / testing)\nexport { buildAuthorizationHeader } from './auth/headers.js';\nexport {\n generateOAuthSignature,\n percentEncode,\n buildSignatureBaseString,\n signHmacSha256,\n generateNonce,\n generateTimestamp,\n} from './auth/oauth.js';\n\n// REST Record API\nexport { createRecord, updateRecord, deleteRecord, upsertRecord, getRecord } from './rest/record.js';\nexport { transformRecord } from './rest/transform.js';\nexport type { TransformOptions, TransformResult } from './rest/transform.js';\n\n// Config\nexport { resolveConfig, deriveBaseUrl, getRealm } from './config.js';\nexport type { ResolvedConfig } from './config.js';\n\n// Errors\nexport {\n NetSuiteError,\n AuthError,\n RateLimitError,\n TimeoutError,\n isRetryableStatus,\n} from './http/errors.js';\n\n// Rate limiter\nexport { RateLimiter } from './http/rate-limiter.js';\n\n// Token bucket + throttler\nexport { TokenBucket } from './http/token-bucket.js';\nexport type { TokenBucketOptions } from './http/token-bucket.js';\nexport { RequestThrottler } from './http/request-throttler.js';\nexport type { ThrottlerOptions } from './http/request-throttler.js';\n\n// Retry\nexport { withRetry } from './http/retry.js';\nexport type { RetryOptions } from './http/retry.js';\n\n// Types\nexport type {\n NetSuiteConfig,\n SuiteQLRow,\n SuiteQLResult,\n SuiteQLResponse,\n OAuthParams,\n RequestOptions,\n NetSuiteResponse,\n} from './types.js';\n","import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nconst RFC5849_EXTRA = /[!'()*]/g;\n\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(RFC5849_EXTRA, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value (ASCII order per RFC 5849)\n const sorted = params.slice().sort((a, b) => {\n if (a[0]! < b[0]!) return -1;\n if (a[0]! > b[0]!) return 1;\n if (a[1]! < b[1]!) return -1;\n if (a[1]! > b[1]!) return 1;\n return 0;\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string (avoid expensive URL constructor)\n const qIdx = params.url.indexOf('?');\n const baseUrl = qIdx === -1 ? params.url : params.url.slice(0, qIdx);\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n if (qIdx !== -1) {\n const qs = params.url.slice(qIdx + 1);\n for (const pair of qs.split('&')) {\n const eqIdx = pair.indexOf('=');\n if (eqIdx !== -1) {\n allParams.push([decodeURIComponent(pair.slice(0, eqIdx)), decodeURIComponent(pair.slice(eqIdx + 1))]);\n }\n }\n }\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {\n baseUrl: string;\n requestsPerSecond?: number;\n burstSize?: number;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n requestsPerSecond: config.requestsPerSecond,\n burstSize: config.burstSize,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","export interface TokenBucketOptions {\n /** Maximum requests per second. */\n requestsPerSecond: number;\n /** Maximum burst capacity. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Token bucket rate limiter for proactive request throttling.\n * Smooths request rate to stay within NetSuite's rate limits\n * instead of reacting to 429 errors.\n */\nexport class TokenBucket {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per ms\n private lastRefill: number;\n private readonly pendingTimers = new Set<ReturnType<typeof setTimeout>>();\n\n constructor(options: TokenBucketOptions) {\n this.maxTokens = options.burstSize ?? options.requestsPerSecond;\n this.refillRate = options.requestsPerSecond / 1000;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token, waiting if necessary.\n * Resolves immediately if a token is available, otherwise\n * delays until the bucket refills enough for one token.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens--;\n return;\n }\n\n const waitMs = (1 - this.tokens) / this.refillRate;\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.pendingTimers.delete(timer);\n resolve();\n }, waitMs);\n this.pendingTimers.add(timer);\n });\n this.refill();\n this.tokens--;\n }\n\n /** Number of tokens currently available. */\n get availableTokens(): number {\n this.refill();\n return this.tokens;\n }\n\n /** Cancel all pending timers. Call on client teardown. */\n destroy(): void {\n for (const timer of this.pendingTimers) {\n clearTimeout(timer);\n }\n this.pendingTimers.clear();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n if (elapsed <= 0) return;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);\n this.lastRefill = now;\n }\n}\n","import { RateLimiter } from './rate-limiter.js';\nimport { TokenBucket } from './token-bucket.js';\n\nexport interface ThrottlerOptions {\n /** Max concurrent requests. Default: 5. */\n concurrency?: number;\n /** Proactive rate limit (requests/sec). Undefined = disabled. */\n requestsPerSecond?: number;\n /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).\n * Token bucket fires first (delays to stay under rate limit), then the semaphore\n * limits parallelism.\n */\nexport class RequestThrottler {\n private readonly semaphore: RateLimiter;\n private readonly tokenBucket: TokenBucket | null;\n\n constructor(options: ThrottlerOptions = {}) {\n this.semaphore = new RateLimiter(options.concurrency ?? 5);\n this.tokenBucket =\n options.requestsPerSecond != null\n ? new TokenBucket({\n requestsPerSecond: options.requestsPerSecond,\n burstSize: options.burstSize,\n })\n : null;\n }\n\n /** Run an async function within both rate and concurrency limits. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n if (this.tokenBucket) {\n await this.tokenBucket.acquire();\n }\n return this.semaphore.run(fn);\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.semaphore.activeCount;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.semaphore.waitingCount;\n }\n\n /** Cancel pending token bucket timers. Call on client teardown. */\n destroy(): void {\n this.tokenBucket?.destroy();\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RequestThrottler } from './request-throttler.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly throttler: RequestThrottler;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.throttler = new RequestThrottler({\n concurrency: this.config.concurrency,\n requestsPerSecond: this.config.requestsPerSecond,\n burstSize: this.config.burstSize,\n });\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const shouldRetry = options.retry ?? options.method === 'GET';\n return this.throttler.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: shouldRetry ? this.config.maxRetries : 0 },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n /** Release resources (cancel pending token bucket timers). */\n destroy(): void {\n this.throttler.destroy();\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const timeoutMs = options.timeout ?? this.config.timeout;\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(timeoutMs);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n retry: true, // POST-for-query is idempotent\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\nconst DEFAULT_TRANSFORM_TIMEOUT = 120_000; // 2 minutes\n\nexport interface TransformOptions {\n /** Request body (field overrides on the target record). */\n body?: Record<string, unknown>;\n /** Timeout in ms. Default: 120000 (2 minutes). */\n timeout?: number;\n}\n\nexport interface TransformResult {\n /** The internal ID of the newly created record. */\n id: string;\n /** The target record type. */\n type: string;\n}\n\n/**\n * Transform a record from one type to another via the NetSuite REST API.\n *\n * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}\n *\n * Uses a longer default timeout (120s) since transforms can be slow.\n * Never retries — transforms are non-idempotent mutations.\n */\nexport async function transformRecord(\n client: NetSuiteClient,\n sourceType: string,\n sourceId: string | number,\n targetType: string,\n options?: TransformOptions,\n): Promise<TransformResult> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${sourceType}/${sourceId}/!transform/${targetType}`,\n body: options?.body ?? {},\n timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,\n retry: false,\n });\n\n // Extract ID from the Location header or response body\n const location = response.headers.get('Location');\n let id: string | undefined;\n\n if (location) {\n // Location: /services/rest/record/v1/{targetType}/{id}\n const match = location.match(/\\/(\\d+)$/);\n if (match) {\n id = match[1];\n }\n }\n\n // Fall back to response body if Location header doesn't have the ID\n if (!id && response.data) {\n const rawId = response.data['id'] ?? response.data['internalid'] ?? response.data['internalId'];\n if (rawId != null) {\n id = String(rawId);\n }\n }\n\n if (!id) {\n throw new Error(\n `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. ` +\n `Location header: ${location ?? '(none)'}`,\n );\n }\n\n return { id, type: targetType };\n}\n"],"mappings":"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","names":["RECORD_BASE"]}
package/dist/index.d.cts CHANGED
@@ -16,6 +16,10 @@ interface NetSuiteConfig {
16
16
  concurrency?: number;
17
17
  /** Max retry attempts for retryable failures. Default: 3. */
18
18
  maxRetries?: number;
19
+ /** Proactive rate limit in requests per second (token bucket). Undefined = disabled. */
20
+ requestsPerSecond?: number;
21
+ /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
22
+ burstSize?: number;
19
23
  /** Override the base URL (mainly for testing). */
20
24
  baseUrl?: string;
21
25
  }
@@ -58,6 +62,13 @@ interface RequestOptions {
58
62
  body?: unknown;
59
63
  headers?: Record<string, string>;
60
64
  query?: Record<string, string>;
65
+ /** Override the global timeout for this request (in ms). */
66
+ timeout?: number;
67
+ /**
68
+ * Whether to retry on failure. Default: true for GET, false for mutations.
69
+ * Set to `true` for idempotent POST operations (e.g. SuiteQL queries).
70
+ */
71
+ retry?: boolean;
61
72
  }
62
73
  /** Generic HTTP response wrapper. */
63
74
  interface NetSuiteResponse<T = unknown> {
@@ -66,8 +77,10 @@ interface NetSuiteResponse<T = unknown> {
66
77
  data: T;
67
78
  }
68
79
 
69
- interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {
80
+ interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {
70
81
  baseUrl: string;
82
+ requestsPerSecond?: number;
83
+ burstSize?: number;
71
84
  }
72
85
  /** Validate config and fill defaults. */
73
86
  declare function resolveConfig(config: NetSuiteConfig): ResolvedConfig;
@@ -86,7 +99,7 @@ declare function getRealm(accountId: string): string;
86
99
  */
87
100
  declare class NetSuiteClient {
88
101
  private readonly config;
89
- private readonly rateLimiter;
102
+ private readonly throttler;
90
103
  constructor(config: NetSuiteConfig);
91
104
  /** Make an authenticated request to the NetSuite REST API. */
92
105
  request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>>;
@@ -94,6 +107,8 @@ declare class NetSuiteClient {
94
107
  get baseUrl(): string;
95
108
  /** Get the resolved config (read-only). */
96
109
  get resolvedConfig(): Readonly<ResolvedConfig>;
110
+ /** Release resources (cancel pending token bucket timers). */
111
+ destroy(): void;
97
112
  private executeRequest;
98
113
  private buildUrl;
99
114
  private handleErrorResponse;
@@ -125,10 +140,6 @@ declare function executeSuiteQLPaginated<T = SuiteQLRow>(client: NetSuiteClient,
125
140
  */
126
141
  declare function buildAuthorizationHeader(params: OAuthParams): string;
127
142
 
128
- /**
129
- * RFC 5849 §3.6 percent-encoding.
130
- * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').
131
- */
132
143
  declare function percentEncode(str: string): string;
133
144
  /** Generate a random nonce for OAuth requests. */
134
145
  declare function generateNonce(): string;
@@ -178,6 +189,28 @@ declare function upsertRecord(client: NetSuiteClient, recordType: string, extern
178
189
  */
179
190
  declare function getRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<Record<string, unknown>>;
180
191
 
192
+ interface TransformOptions {
193
+ /** Request body (field overrides on the target record). */
194
+ body?: Record<string, unknown>;
195
+ /** Timeout in ms. Default: 120000 (2 minutes). */
196
+ timeout?: number;
197
+ }
198
+ interface TransformResult {
199
+ /** The internal ID of the newly created record. */
200
+ id: string;
201
+ /** The target record type. */
202
+ type: string;
203
+ }
204
+ /**
205
+ * Transform a record from one type to another via the NetSuite REST API.
206
+ *
207
+ * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}
208
+ *
209
+ * Uses a longer default timeout (120s) since transforms can be slow.
210
+ * Never retries — transforms are non-idempotent mutations.
211
+ */
212
+ declare function transformRecord(client: NetSuiteClient, sourceType: string, sourceId: string | number, targetType: string, options?: TransformOptions): Promise<TransformResult>;
213
+
181
214
  /** Base error class for all NetSuite connector errors. */
182
215
  declare class NetSuiteError extends Error {
183
216
  readonly status?: number | undefined;
@@ -222,6 +255,64 @@ declare class RateLimiter {
222
255
  get waitingCount(): number;
223
256
  }
224
257
 
258
+ interface TokenBucketOptions {
259
+ /** Maximum requests per second. */
260
+ requestsPerSecond: number;
261
+ /** Maximum burst capacity. Defaults to requestsPerSecond. */
262
+ burstSize?: number;
263
+ }
264
+ /**
265
+ * Token bucket rate limiter for proactive request throttling.
266
+ * Smooths request rate to stay within NetSuite's rate limits
267
+ * instead of reacting to 429 errors.
268
+ */
269
+ declare class TokenBucket {
270
+ private tokens;
271
+ private readonly maxTokens;
272
+ private readonly refillRate;
273
+ private lastRefill;
274
+ private readonly pendingTimers;
275
+ constructor(options: TokenBucketOptions);
276
+ /**
277
+ * Acquire a token, waiting if necessary.
278
+ * Resolves immediately if a token is available, otherwise
279
+ * delays until the bucket refills enough for one token.
280
+ */
281
+ acquire(): Promise<void>;
282
+ /** Number of tokens currently available. */
283
+ get availableTokens(): number;
284
+ /** Cancel all pending timers. Call on client teardown. */
285
+ destroy(): void;
286
+ private refill;
287
+ }
288
+
289
+ interface ThrottlerOptions {
290
+ /** Max concurrent requests. Default: 5. */
291
+ concurrency?: number;
292
+ /** Proactive rate limit (requests/sec). Undefined = disabled. */
293
+ requestsPerSecond?: number;
294
+ /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
295
+ burstSize?: number;
296
+ }
297
+ /**
298
+ * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).
299
+ * Token bucket fires first (delays to stay under rate limit), then the semaphore
300
+ * limits parallelism.
301
+ */
302
+ declare class RequestThrottler {
303
+ private readonly semaphore;
304
+ private readonly tokenBucket;
305
+ constructor(options?: ThrottlerOptions);
306
+ /** Run an async function within both rate and concurrency limits. */
307
+ run<T>(fn: () => Promise<T>): Promise<T>;
308
+ /** Number of currently active tasks. */
309
+ get activeCount(): number;
310
+ /** Number of tasks waiting for a slot. */
311
+ get waitingCount(): number;
312
+ /** Cancel pending token bucket timers. Call on client teardown. */
313
+ destroy(): void;
314
+ }
315
+
225
316
  interface RetryOptions {
226
317
  maxRetries: number;
227
318
  /** Base delay in ms before first retry. Default: 500. */
@@ -235,4 +326,4 @@ interface RetryOptions {
235
326
  */
236
327
  declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
237
328
 
238
- export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, TimeoutError, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, updateRecord, upsertRecord, withRetry };
329
+ export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, RequestThrottler, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, type ThrottlerOptions, TimeoutError, TokenBucket, type TokenBucketOptions, type TransformOptions, type TransformResult, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, transformRecord, updateRecord, upsertRecord, withRetry };
package/dist/index.d.ts CHANGED
@@ -16,6 +16,10 @@ interface NetSuiteConfig {
16
16
  concurrency?: number;
17
17
  /** Max retry attempts for retryable failures. Default: 3. */
18
18
  maxRetries?: number;
19
+ /** Proactive rate limit in requests per second (token bucket). Undefined = disabled. */
20
+ requestsPerSecond?: number;
21
+ /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
22
+ burstSize?: number;
19
23
  /** Override the base URL (mainly for testing). */
20
24
  baseUrl?: string;
21
25
  }
@@ -58,6 +62,13 @@ interface RequestOptions {
58
62
  body?: unknown;
59
63
  headers?: Record<string, string>;
60
64
  query?: Record<string, string>;
65
+ /** Override the global timeout for this request (in ms). */
66
+ timeout?: number;
67
+ /**
68
+ * Whether to retry on failure. Default: true for GET, false for mutations.
69
+ * Set to `true` for idempotent POST operations (e.g. SuiteQL queries).
70
+ */
71
+ retry?: boolean;
61
72
  }
62
73
  /** Generic HTTP response wrapper. */
63
74
  interface NetSuiteResponse<T = unknown> {
@@ -66,8 +77,10 @@ interface NetSuiteResponse<T = unknown> {
66
77
  data: T;
67
78
  }
68
79
 
69
- interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {
80
+ interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {
70
81
  baseUrl: string;
82
+ requestsPerSecond?: number;
83
+ burstSize?: number;
71
84
  }
72
85
  /** Validate config and fill defaults. */
73
86
  declare function resolveConfig(config: NetSuiteConfig): ResolvedConfig;
@@ -86,7 +99,7 @@ declare function getRealm(accountId: string): string;
86
99
  */
87
100
  declare class NetSuiteClient {
88
101
  private readonly config;
89
- private readonly rateLimiter;
102
+ private readonly throttler;
90
103
  constructor(config: NetSuiteConfig);
91
104
  /** Make an authenticated request to the NetSuite REST API. */
92
105
  request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>>;
@@ -94,6 +107,8 @@ declare class NetSuiteClient {
94
107
  get baseUrl(): string;
95
108
  /** Get the resolved config (read-only). */
96
109
  get resolvedConfig(): Readonly<ResolvedConfig>;
110
+ /** Release resources (cancel pending token bucket timers). */
111
+ destroy(): void;
97
112
  private executeRequest;
98
113
  private buildUrl;
99
114
  private handleErrorResponse;
@@ -125,10 +140,6 @@ declare function executeSuiteQLPaginated<T = SuiteQLRow>(client: NetSuiteClient,
125
140
  */
126
141
  declare function buildAuthorizationHeader(params: OAuthParams): string;
127
142
 
128
- /**
129
- * RFC 5849 §3.6 percent-encoding.
130
- * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').
131
- */
132
143
  declare function percentEncode(str: string): string;
133
144
  /** Generate a random nonce for OAuth requests. */
134
145
  declare function generateNonce(): string;
@@ -178,6 +189,28 @@ declare function upsertRecord(client: NetSuiteClient, recordType: string, extern
178
189
  */
179
190
  declare function getRecord(client: NetSuiteClient, recordType: string, id: string | number): Promise<Record<string, unknown>>;
180
191
 
192
+ interface TransformOptions {
193
+ /** Request body (field overrides on the target record). */
194
+ body?: Record<string, unknown>;
195
+ /** Timeout in ms. Default: 120000 (2 minutes). */
196
+ timeout?: number;
197
+ }
198
+ interface TransformResult {
199
+ /** The internal ID of the newly created record. */
200
+ id: string;
201
+ /** The target record type. */
202
+ type: string;
203
+ }
204
+ /**
205
+ * Transform a record from one type to another via the NetSuite REST API.
206
+ *
207
+ * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}
208
+ *
209
+ * Uses a longer default timeout (120s) since transforms can be slow.
210
+ * Never retries — transforms are non-idempotent mutations.
211
+ */
212
+ declare function transformRecord(client: NetSuiteClient, sourceType: string, sourceId: string | number, targetType: string, options?: TransformOptions): Promise<TransformResult>;
213
+
181
214
  /** Base error class for all NetSuite connector errors. */
182
215
  declare class NetSuiteError extends Error {
183
216
  readonly status?: number | undefined;
@@ -222,6 +255,64 @@ declare class RateLimiter {
222
255
  get waitingCount(): number;
223
256
  }
224
257
 
258
+ interface TokenBucketOptions {
259
+ /** Maximum requests per second. */
260
+ requestsPerSecond: number;
261
+ /** Maximum burst capacity. Defaults to requestsPerSecond. */
262
+ burstSize?: number;
263
+ }
264
+ /**
265
+ * Token bucket rate limiter for proactive request throttling.
266
+ * Smooths request rate to stay within NetSuite's rate limits
267
+ * instead of reacting to 429 errors.
268
+ */
269
+ declare class TokenBucket {
270
+ private tokens;
271
+ private readonly maxTokens;
272
+ private readonly refillRate;
273
+ private lastRefill;
274
+ private readonly pendingTimers;
275
+ constructor(options: TokenBucketOptions);
276
+ /**
277
+ * Acquire a token, waiting if necessary.
278
+ * Resolves immediately if a token is available, otherwise
279
+ * delays until the bucket refills enough for one token.
280
+ */
281
+ acquire(): Promise<void>;
282
+ /** Number of tokens currently available. */
283
+ get availableTokens(): number;
284
+ /** Cancel all pending timers. Call on client teardown. */
285
+ destroy(): void;
286
+ private refill;
287
+ }
288
+
289
+ interface ThrottlerOptions {
290
+ /** Max concurrent requests. Default: 5. */
291
+ concurrency?: number;
292
+ /** Proactive rate limit (requests/sec). Undefined = disabled. */
293
+ requestsPerSecond?: number;
294
+ /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */
295
+ burstSize?: number;
296
+ }
297
+ /**
298
+ * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).
299
+ * Token bucket fires first (delays to stay under rate limit), then the semaphore
300
+ * limits parallelism.
301
+ */
302
+ declare class RequestThrottler {
303
+ private readonly semaphore;
304
+ private readonly tokenBucket;
305
+ constructor(options?: ThrottlerOptions);
306
+ /** Run an async function within both rate and concurrency limits. */
307
+ run<T>(fn: () => Promise<T>): Promise<T>;
308
+ /** Number of currently active tasks. */
309
+ get activeCount(): number;
310
+ /** Number of tasks waiting for a slot. */
311
+ get waitingCount(): number;
312
+ /** Cancel pending token bucket timers. Call on client teardown. */
313
+ destroy(): void;
314
+ }
315
+
225
316
  interface RetryOptions {
226
317
  maxRetries: number;
227
318
  /** Base delay in ms before first retry. Default: 500. */
@@ -235,4 +326,4 @@ interface RetryOptions {
235
326
  */
236
327
  declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
237
328
 
238
- export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, TimeoutError, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, updateRecord, upsertRecord, withRetry };
329
+ export { AuthError, NetSuiteClient, type NetSuiteConfig, NetSuiteError, type NetSuiteResponse, type OAuthParams, type PaginationOptions, RateLimitError, RateLimiter, type RequestOptions, RequestThrottler, type ResolvedConfig, type RetryOptions, type SuiteQLResponse, type SuiteQLResult, type SuiteQLRow, type ThrottlerOptions, TimeoutError, TokenBucket, type TokenBucketOptions, type TransformOptions, type TransformResult, buildAuthorizationHeader, buildSignatureBaseString, createRecord, deleteRecord, deriveBaseUrl, executeSuiteQL, executeSuiteQLPaginated, generateNonce, generateOAuthSignature, generateTimestamp, getRealm, getRecord, isRetryableStatus, percentEncode, resolveConfig, signHmacSha256, transformRecord, updateRecord, upsertRecord, withRetry };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/auth/oauth.ts
2
2
  import { createHmac, randomBytes } from "crypto";
3
+ var RFC5849_EXTRA = /[!'()*]/g;
3
4
  function percentEncode(str) {
4
- return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
5
+ return encodeURIComponent(str).replace(RFC5849_EXTRA, (c) => {
5
6
  return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;
6
7
  });
7
8
  }
@@ -12,9 +13,12 @@ function generateTimestamp() {
12
13
  return Math.floor(Date.now() / 1e3).toString();
13
14
  }
14
15
  function buildSignatureBaseString(method, baseUrl, params) {
15
- const sorted = [...params].sort((a, b) => {
16
- const keyCompare = a[0].localeCompare(b[0]);
17
- return keyCompare !== 0 ? keyCompare : a[1].localeCompare(b[1]);
16
+ const sorted = params.slice().sort((a, b) => {
17
+ if (a[0] < b[0]) return -1;
18
+ if (a[0] > b[0]) return 1;
19
+ if (a[1] < b[1]) return -1;
20
+ if (a[1] > b[1]) return 1;
21
+ return 0;
18
22
  });
19
23
  const paramString = sorted.map(([k, v]) => `${percentEncode(k)}=${percentEncode(v)}`).join("&");
20
24
  return [
@@ -38,12 +42,18 @@ function generateOAuthSignature(params) {
38
42
  oauth_signature_method: "HMAC-SHA256",
39
43
  oauth_version: "1.0"
40
44
  };
41
- const urlObj = new URL(params.url);
42
- const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
45
+ const qIdx = params.url.indexOf("?");
46
+ const baseUrl = qIdx === -1 ? params.url : params.url.slice(0, qIdx);
43
47
  const allParams = Object.entries(oauthParams);
44
- urlObj.searchParams.forEach((value, key) => {
45
- allParams.push([key, value]);
46
- });
48
+ if (qIdx !== -1) {
49
+ const qs = params.url.slice(qIdx + 1);
50
+ for (const pair of qs.split("&")) {
51
+ const eqIdx = pair.indexOf("=");
52
+ if (eqIdx !== -1) {
53
+ allParams.push([decodeURIComponent(pair.slice(0, eqIdx)), decodeURIComponent(pair.slice(eqIdx + 1))]);
54
+ }
55
+ }
56
+ }
47
57
  const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);
48
58
  const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);
49
59
  return { signature, oauthParams };
@@ -89,6 +99,8 @@ function resolveConfig(config) {
89
99
  timeout: config.timeout ?? DEFAULTS.timeout,
90
100
  concurrency: config.concurrency ?? DEFAULTS.concurrency,
91
101
  maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,
102
+ requestsPerSecond: config.requestsPerSecond,
103
+ burstSize: config.burstSize,
92
104
  baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId)
93
105
  };
94
106
  }
@@ -181,6 +193,95 @@ var RateLimiter = class {
181
193
  }
182
194
  };
183
195
 
196
+ // src/http/token-bucket.ts
197
+ var TokenBucket = class {
198
+ tokens;
199
+ maxTokens;
200
+ refillRate;
201
+ // tokens per ms
202
+ lastRefill;
203
+ pendingTimers = /* @__PURE__ */ new Set();
204
+ constructor(options) {
205
+ this.maxTokens = options.burstSize ?? options.requestsPerSecond;
206
+ this.refillRate = options.requestsPerSecond / 1e3;
207
+ this.tokens = this.maxTokens;
208
+ this.lastRefill = Date.now();
209
+ }
210
+ /**
211
+ * Acquire a token, waiting if necessary.
212
+ * Resolves immediately if a token is available, otherwise
213
+ * delays until the bucket refills enough for one token.
214
+ */
215
+ async acquire() {
216
+ this.refill();
217
+ if (this.tokens >= 1) {
218
+ this.tokens--;
219
+ return;
220
+ }
221
+ const waitMs = (1 - this.tokens) / this.refillRate;
222
+ await new Promise((resolve) => {
223
+ const timer = setTimeout(() => {
224
+ this.pendingTimers.delete(timer);
225
+ resolve();
226
+ }, waitMs);
227
+ this.pendingTimers.add(timer);
228
+ });
229
+ this.refill();
230
+ this.tokens--;
231
+ }
232
+ /** Number of tokens currently available. */
233
+ get availableTokens() {
234
+ this.refill();
235
+ return this.tokens;
236
+ }
237
+ /** Cancel all pending timers. Call on client teardown. */
238
+ destroy() {
239
+ for (const timer of this.pendingTimers) {
240
+ clearTimeout(timer);
241
+ }
242
+ this.pendingTimers.clear();
243
+ }
244
+ refill() {
245
+ const now = Date.now();
246
+ const elapsed = now - this.lastRefill;
247
+ if (elapsed <= 0) return;
248
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
249
+ this.lastRefill = now;
250
+ }
251
+ };
252
+
253
+ // src/http/request-throttler.ts
254
+ var RequestThrottler = class {
255
+ semaphore;
256
+ tokenBucket;
257
+ constructor(options = {}) {
258
+ this.semaphore = new RateLimiter(options.concurrency ?? 5);
259
+ this.tokenBucket = options.requestsPerSecond != null ? new TokenBucket({
260
+ requestsPerSecond: options.requestsPerSecond,
261
+ burstSize: options.burstSize
262
+ }) : null;
263
+ }
264
+ /** Run an async function within both rate and concurrency limits. */
265
+ async run(fn) {
266
+ if (this.tokenBucket) {
267
+ await this.tokenBucket.acquire();
268
+ }
269
+ return this.semaphore.run(fn);
270
+ }
271
+ /** Number of currently active tasks. */
272
+ get activeCount() {
273
+ return this.semaphore.activeCount;
274
+ }
275
+ /** Number of tasks waiting for a slot. */
276
+ get waitingCount() {
277
+ return this.semaphore.waitingCount;
278
+ }
279
+ /** Cancel pending token bucket timers. Call on client teardown. */
280
+ destroy() {
281
+ this.tokenBucket?.destroy();
282
+ }
283
+ };
284
+
184
285
  // src/http/retry.ts
185
286
  async function withRetry(fn, options) {
186
287
  const { maxRetries, baseDelay = 500, maxDelay = 3e4 } = options;
@@ -216,17 +317,22 @@ function sleep(ms) {
216
317
  // src/http/client.ts
217
318
  var NetSuiteClient = class {
218
319
  config;
219
- rateLimiter;
320
+ throttler;
220
321
  constructor(config) {
221
322
  this.config = resolveConfig(config);
222
- this.rateLimiter = new RateLimiter(this.config.concurrency);
323
+ this.throttler = new RequestThrottler({
324
+ concurrency: this.config.concurrency,
325
+ requestsPerSecond: this.config.requestsPerSecond,
326
+ burstSize: this.config.burstSize
327
+ });
223
328
  }
224
329
  /** Make an authenticated request to the NetSuite REST API. */
225
330
  async request(options) {
226
- return this.rateLimiter.run(
331
+ const shouldRetry = options.retry ?? options.method === "GET";
332
+ return this.throttler.run(
227
333
  () => withRetry(
228
334
  () => this.executeRequest(options),
229
- { maxRetries: this.config.maxRetries }
335
+ { maxRetries: shouldRetry ? this.config.maxRetries : 0 }
230
336
  )
231
337
  );
232
338
  }
@@ -238,6 +344,10 @@ var NetSuiteClient = class {
238
344
  get resolvedConfig() {
239
345
  return this.config;
240
346
  }
347
+ /** Release resources (cancel pending token bucket timers). */
348
+ destroy() {
349
+ this.throttler.destroy();
350
+ }
241
351
  async executeRequest(options) {
242
352
  const url = this.buildUrl(options.path, options.query);
243
353
  const realm = getRealm(this.config.accountId);
@@ -256,8 +366,9 @@ var NetSuiteClient = class {
256
366
  Accept: "application/json",
257
367
  ...options.headers
258
368
  };
369
+ const timeoutMs = options.timeout ?? this.config.timeout;
259
370
  const controller = new AbortController();
260
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
371
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
261
372
  try {
262
373
  const response = await fetch(url, {
263
374
  method: options.method,
@@ -275,7 +386,7 @@ var NetSuiteClient = class {
275
386
  clearTimeout(timeoutId);
276
387
  if (error instanceof NetSuiteError) throw error;
277
388
  if (error instanceof DOMException && error.name === "AbortError") {
278
- throw new TimeoutError(this.config.timeout);
389
+ throw new TimeoutError(timeoutMs);
279
390
  }
280
391
  throw new NetSuiteError(
281
392
  `Network error: ${error instanceof Error ? error.message : String(error)}`,
@@ -336,7 +447,9 @@ async function executeSuiteQL(client, query, options) {
336
447
  query: {
337
448
  limit: limit.toString(),
338
449
  offset: offset.toString()
339
- }
450
+ },
451
+ retry: true
452
+ // POST-for-query is idempotent
340
453
  });
341
454
  const items = (response.data.items ?? []).map(({ links, ...rest }) => rest);
342
455
  return {
@@ -403,13 +516,48 @@ async function getRecord(client, recordType, id) {
403
516
  });
404
517
  return response.data;
405
518
  }
519
+
520
+ // src/rest/transform.ts
521
+ var RECORD_BASE2 = "/services/rest/record/v1";
522
+ var DEFAULT_TRANSFORM_TIMEOUT = 12e4;
523
+ async function transformRecord(client, sourceType, sourceId, targetType, options) {
524
+ const response = await client.request({
525
+ method: "POST",
526
+ path: `${RECORD_BASE2}/${sourceType}/${sourceId}/!transform/${targetType}`,
527
+ body: options?.body ?? {},
528
+ timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,
529
+ retry: false
530
+ });
531
+ const location = response.headers.get("Location");
532
+ let id;
533
+ if (location) {
534
+ const match = location.match(/\/(\d+)$/);
535
+ if (match) {
536
+ id = match[1];
537
+ }
538
+ }
539
+ if (!id && response.data) {
540
+ const rawId = response.data["id"] ?? response.data["internalid"] ?? response.data["internalId"];
541
+ if (rawId != null) {
542
+ id = String(rawId);
543
+ }
544
+ }
545
+ if (!id) {
546
+ throw new Error(
547
+ `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. Location header: ${location ?? "(none)"}`
548
+ );
549
+ }
550
+ return { id, type: targetType };
551
+ }
406
552
  export {
407
553
  AuthError,
408
554
  NetSuiteClient,
409
555
  NetSuiteError,
410
556
  RateLimitError,
411
557
  RateLimiter,
558
+ RequestThrottler,
412
559
  TimeoutError,
560
+ TokenBucket,
413
561
  buildAuthorizationHeader,
414
562
  buildSignatureBaseString,
415
563
  createRecord,
@@ -426,6 +574,7 @@ export {
426
574
  percentEncode,
427
575
  resolveConfig,
428
576
  signHmacSha256,
577
+ transformRecord,
429
578
  updateRecord,
430
579
  upsertRecord,
431
580
  withRetry
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts"],"sourcesContent":["import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value\n const sorted = [...params].sort((a, b) => {\n const keyCompare = a[0]!.localeCompare(b[0]!);\n return keyCompare !== 0 ? keyCompare : a[1]!.localeCompare(b[1]!);\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string\n const urlObj = new URL(params.url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n urlObj.searchParams.forEach((value, key) => {\n allParams.push([key, value]);\n });\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl'>> {\n baseUrl: string;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RateLimiter } from './rate-limiter.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly rateLimiter: RateLimiter;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.rateLimiter = new RateLimiter(this.config.concurrency);\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n return this.rateLimiter.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: this.config.maxRetries },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(this.config.timeout);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/auth/oauth.ts","../src/auth/headers.ts","../src/config.ts","../src/http/errors.ts","../src/http/rate-limiter.ts","../src/http/token-bucket.ts","../src/http/request-throttler.ts","../src/http/retry.ts","../src/http/client.ts","../src/suiteql/executor.ts","../src/suiteql/paginator.ts","../src/rest/record.ts","../src/rest/transform.ts"],"sourcesContent":["import { createHmac, randomBytes } from 'node:crypto';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * RFC 5849 §3.6 percent-encoding.\n * Encodes all characters except unreserved (ALPHA, DIGIT, '-', '.', '_', '~').\n */\nconst RFC5849_EXTRA = /[!'()*]/g;\n\nexport function percentEncode(str: string): string {\n return encodeURIComponent(str).replace(RFC5849_EXTRA, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`;\n });\n}\n\n/** Generate a random nonce for OAuth requests. */\nexport function generateNonce(): string {\n return randomBytes(16).toString('hex');\n}\n\n/** Generate a Unix timestamp string. */\nexport function generateTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString();\n}\n\n/**\n * Build the OAuth signature base string per RFC 5849 §3.4.1.\n */\nexport function buildSignatureBaseString(\n method: string,\n baseUrl: string,\n params: Array<[string, string]>,\n): string {\n // Sort params lexicographically by key, then by value (ASCII order per RFC 5849)\n const sorted = params.slice().sort((a, b) => {\n if (a[0]! < b[0]!) return -1;\n if (a[0]! > b[0]!) return 1;\n if (a[1]! < b[1]!) return -1;\n if (a[1]! > b[1]!) return 1;\n return 0;\n });\n\n const paramString = sorted\n .map(([k, v]) => `${percentEncode(k!)}=${percentEncode(v!)}`)\n .join('&');\n\n return [\n method.toUpperCase(),\n percentEncode(baseUrl),\n percentEncode(paramString),\n ].join('&');\n}\n\n/**\n * Sign the base string with HMAC-SHA256.\n * Signing key = percentEncode(consumerSecret) & percentEncode(tokenSecret)\n */\nexport function signHmacSha256(\n baseString: string,\n consumerSecret: string,\n tokenSecret: string,\n): string {\n const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;\n return createHmac('sha256', signingKey).update(baseString).digest('base64');\n}\n\n/**\n * Generate the full OAuth 1.0a signature for a request.\n * Returns the signature string and the oauth params used (for header construction).\n */\nexport function generateOAuthSignature(params: OAuthParams): {\n signature: string;\n oauthParams: Record<string, string>;\n} {\n const timestamp = params.timestamp ?? generateTimestamp();\n const nonce = params.nonce ?? generateNonce();\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: params.consumerKey,\n oauth_token: params.tokenId,\n oauth_nonce: nonce,\n oauth_timestamp: timestamp,\n oauth_signature_method: 'HMAC-SHA256',\n oauth_version: '1.0',\n };\n\n // Strip query string from URL for base string (avoid expensive URL constructor)\n const qIdx = params.url.indexOf('?');\n const baseUrl = qIdx === -1 ? params.url : params.url.slice(0, qIdx);\n\n // Collect all params: oauth params + query string params\n const allParams: Array<[string, string]> = Object.entries(oauthParams);\n if (qIdx !== -1) {\n const qs = params.url.slice(qIdx + 1);\n for (const pair of qs.split('&')) {\n const eqIdx = pair.indexOf('=');\n if (eqIdx !== -1) {\n allParams.push([decodeURIComponent(pair.slice(0, eqIdx)), decodeURIComponent(pair.slice(eqIdx + 1))]);\n }\n }\n }\n\n const baseString = buildSignatureBaseString(params.method, baseUrl, allParams);\n const signature = signHmacSha256(baseString, params.consumerSecret, params.tokenSecret);\n\n return { signature, oauthParams };\n}\n","import { generateOAuthSignature, percentEncode } from './oauth.js';\nimport type { OAuthParams } from '../types.js';\n\n/**\n * Build the OAuth Authorization header value.\n * Format: OAuth realm=\"...\", oauth_consumer_key=\"...\", ..., oauth_signature=\"...\"\n */\nexport function buildAuthorizationHeader(params: OAuthParams): string {\n const { signature, oauthParams } = generateOAuthSignature(params);\n\n const headerParams: Record<string, string> = {\n realm: params.realm,\n ...oauthParams,\n oauth_signature: signature,\n };\n\n const parts = Object.entries(headerParams)\n .map(([key, value]) => `${percentEncode(key)}=\"${percentEncode(value)}\"`)\n .join(', ');\n\n return `OAuth ${parts}`;\n}\n","import type { NetSuiteConfig } from './types.js';\n\nconst REQUIRED_FIELDS = [\n 'accountId',\n 'consumerKey',\n 'consumerSecret',\n 'tokenId',\n 'tokenSecret',\n] as const;\n\nconst DEFAULTS = {\n timeout: 30_000,\n concurrency: 5,\n maxRetries: 3,\n} as const;\n\nexport interface ResolvedConfig extends Required<Omit<NetSuiteConfig, 'baseUrl' | 'requestsPerSecond' | 'burstSize'>> {\n baseUrl: string;\n requestsPerSecond?: number;\n burstSize?: number;\n}\n\n/** Validate config and fill defaults. */\nexport function resolveConfig(config: NetSuiteConfig): ResolvedConfig {\n for (const field of REQUIRED_FIELDS) {\n if (!config[field]) {\n throw new Error(`NetSuiteConfig: \"${field}\" is required`);\n }\n }\n\n return {\n accountId: config.accountId,\n consumerKey: config.consumerKey,\n consumerSecret: config.consumerSecret,\n tokenId: config.tokenId,\n tokenSecret: config.tokenSecret,\n timeout: config.timeout ?? DEFAULTS.timeout,\n concurrency: config.concurrency ?? DEFAULTS.concurrency,\n maxRetries: config.maxRetries ?? DEFAULTS.maxRetries,\n requestsPerSecond: config.requestsPerSecond,\n burstSize: config.burstSize,\n baseUrl: config.baseUrl ?? deriveBaseUrl(config.accountId),\n };\n}\n\n/**\n * Derive the NetSuite REST API base URL from the account ID.\n * Account IDs with underscores (sandbox) have underscores replaced with hyphens.\n * Example: \"1234567_SB1\" → \"https://1234567-sb1.suitetalk.api.netsuite.com\"\n */\nexport function deriveBaseUrl(accountId: string): string {\n const normalized = accountId.toLowerCase().replace(/_/g, '-');\n return `https://${normalized}.suitetalk.api.netsuite.com`;\n}\n\n/** Get the realm (account ID in uppercase, underscores preserved). */\nexport function getRealm(accountId: string): string {\n return accountId.toUpperCase().replace(/-/g, '_');\n}\n","/** Base error class for all NetSuite connector errors. */\nexport class NetSuiteError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly code?: string,\n public readonly details?: unknown,\n ) {\n super(message);\n this.name = 'NetSuiteError';\n }\n}\n\n/** Authentication or authorization failure. */\nexport class AuthError extends NetSuiteError {\n constructor(message: string, status?: number, details?: unknown) {\n super(message, status, 'AUTH_ERROR', details);\n this.name = 'AuthError';\n }\n}\n\n/** Rate limit (429) exceeded. */\nexport class RateLimitError extends NetSuiteError {\n constructor(\n public readonly retryAfterMs: number,\n details?: unknown,\n ) {\n super(`Rate limited. Retry after ${retryAfterMs}ms`, 429, 'RATE_LIMIT', details);\n this.name = 'RateLimitError';\n }\n}\n\n/** Request timeout via AbortController. */\nexport class TimeoutError extends NetSuiteError {\n constructor(timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, undefined, 'TIMEOUT');\n this.name = 'TimeoutError';\n }\n}\n\n/** HTTP status codes that are safe to retry. */\nconst RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);\n\n/** Check if an HTTP status code is retryable. */\nexport function isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUSES.has(status);\n}\n","/**\n * Async semaphore for concurrency governance.\n * Limits the number of in-flight requests to prevent overwhelming NetSuite.\n */\nexport class RateLimiter {\n private active = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly maxConcurrent: number) {}\n\n /** Acquire a slot. Resolves when a slot is available. */\n async acquire(): Promise<void> {\n if (this.active < this.maxConcurrent) {\n this.active++;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.queue.push(() => {\n this.active++;\n resolve();\n });\n });\n }\n\n /** Release a slot, unblocking the next waiter if any. */\n release(): void {\n this.active--;\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n\n /** Run an async function within the concurrency limit. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.active;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.queue.length;\n }\n}\n","export interface TokenBucketOptions {\n /** Maximum requests per second. */\n requestsPerSecond: number;\n /** Maximum burst capacity. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Token bucket rate limiter for proactive request throttling.\n * Smooths request rate to stay within NetSuite's rate limits\n * instead of reacting to 429 errors.\n */\nexport class TokenBucket {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per ms\n private lastRefill: number;\n private readonly pendingTimers = new Set<ReturnType<typeof setTimeout>>();\n\n constructor(options: TokenBucketOptions) {\n this.maxTokens = options.burstSize ?? options.requestsPerSecond;\n this.refillRate = options.requestsPerSecond / 1000;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token, waiting if necessary.\n * Resolves immediately if a token is available, otherwise\n * delays until the bucket refills enough for one token.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens--;\n return;\n }\n\n const waitMs = (1 - this.tokens) / this.refillRate;\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.pendingTimers.delete(timer);\n resolve();\n }, waitMs);\n this.pendingTimers.add(timer);\n });\n this.refill();\n this.tokens--;\n }\n\n /** Number of tokens currently available. */\n get availableTokens(): number {\n this.refill();\n return this.tokens;\n }\n\n /** Cancel all pending timers. Call on client teardown. */\n destroy(): void {\n for (const timer of this.pendingTimers) {\n clearTimeout(timer);\n }\n this.pendingTimers.clear();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n if (elapsed <= 0) return;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);\n this.lastRefill = now;\n }\n}\n","import { RateLimiter } from './rate-limiter.js';\nimport { TokenBucket } from './token-bucket.js';\n\nexport interface ThrottlerOptions {\n /** Max concurrent requests. Default: 5. */\n concurrency?: number;\n /** Proactive rate limit (requests/sec). Undefined = disabled. */\n requestsPerSecond?: number;\n /** Burst capacity for the token bucket. Defaults to requestsPerSecond. */\n burstSize?: number;\n}\n\n/**\n * Composes concurrency limiting (semaphore) with proactive rate limiting (token bucket).\n * Token bucket fires first (delays to stay under rate limit), then the semaphore\n * limits parallelism.\n */\nexport class RequestThrottler {\n private readonly semaphore: RateLimiter;\n private readonly tokenBucket: TokenBucket | null;\n\n constructor(options: ThrottlerOptions = {}) {\n this.semaphore = new RateLimiter(options.concurrency ?? 5);\n this.tokenBucket =\n options.requestsPerSecond != null\n ? new TokenBucket({\n requestsPerSecond: options.requestsPerSecond,\n burstSize: options.burstSize,\n })\n : null;\n }\n\n /** Run an async function within both rate and concurrency limits. */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n if (this.tokenBucket) {\n await this.tokenBucket.acquire();\n }\n return this.semaphore.run(fn);\n }\n\n /** Number of currently active tasks. */\n get activeCount(): number {\n return this.semaphore.activeCount;\n }\n\n /** Number of tasks waiting for a slot. */\n get waitingCount(): number {\n return this.semaphore.waitingCount;\n }\n\n /** Cancel pending token bucket timers. Call on client teardown. */\n destroy(): void {\n this.tokenBucket?.destroy();\n }\n}\n","import { RateLimitError, isRetryableStatus, NetSuiteError } from './errors.js';\n\nexport interface RetryOptions {\n maxRetries: number;\n /** Base delay in ms before first retry. Default: 500. */\n baseDelay?: number;\n /** Maximum delay in ms. Default: 30000. */\n maxDelay?: number;\n}\n\n/**\n * Execute a function with exponential backoff + jitter.\n * Respects `Retry-After` from RateLimitError.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n options: RetryOptions,\n): Promise<T> {\n const { maxRetries, baseDelay = 500, maxDelay = 30_000 } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (attempt >= maxRetries) break;\n\n // Don't retry auth errors — they won't resolve by retrying\n if (error instanceof NetSuiteError && error.code === 'AUTH_ERROR') {\n throw error;\n }\n\n // Only retry on retryable status codes or network errors\n if (error instanceof NetSuiteError && error.status != null && !isRetryableStatus(error.status)) {\n throw error;\n }\n\n let delay: number;\n if (error instanceof RateLimitError) {\n delay = error.retryAfterMs;\n } else {\n // Exponential backoff with full jitter\n const exponential = baseDelay * Math.pow(2, attempt);\n delay = Math.min(exponential, maxDelay) * Math.random();\n }\n\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { buildAuthorizationHeader } from '../auth/headers.js';\nimport { resolveConfig, getRealm, type ResolvedConfig } from '../config.js';\nimport type { NetSuiteConfig, RequestOptions, NetSuiteResponse } from '../types.js';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from './errors.js';\nimport { RequestThrottler } from './request-throttler.js';\nimport { withRetry } from './retry.js';\n\n/**\n * Core HTTP client for the NetSuite REST API.\n * Handles OAuth signing, retries, rate limiting, and timeouts.\n */\nexport class NetSuiteClient {\n private readonly config: ResolvedConfig;\n private readonly throttler: RequestThrottler;\n\n constructor(config: NetSuiteConfig) {\n this.config = resolveConfig(config);\n this.throttler = new RequestThrottler({\n concurrency: this.config.concurrency,\n requestsPerSecond: this.config.requestsPerSecond,\n burstSize: this.config.burstSize,\n });\n }\n\n /** Make an authenticated request to the NetSuite REST API. */\n async request<T = unknown>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const shouldRetry = options.retry ?? options.method === 'GET';\n return this.throttler.run(() =>\n withRetry(\n () => this.executeRequest<T>(options),\n { maxRetries: shouldRetry ? this.config.maxRetries : 0 },\n ),\n );\n }\n\n /** Get the resolved base URL. */\n get baseUrl(): string {\n return this.config.baseUrl;\n }\n\n /** Get the resolved config (read-only). */\n get resolvedConfig(): Readonly<ResolvedConfig> {\n return this.config;\n }\n\n /** Release resources (cancel pending token bucket timers). */\n destroy(): void {\n this.throttler.destroy();\n }\n\n private async executeRequest<T>(options: RequestOptions): Promise<NetSuiteResponse<T>> {\n const url = this.buildUrl(options.path, options.query);\n const realm = getRealm(this.config.accountId);\n\n const authHeader = buildAuthorizationHeader({\n consumerKey: this.config.consumerKey,\n consumerSecret: this.config.consumerSecret,\n tokenId: this.config.tokenId,\n tokenSecret: this.config.tokenSecret,\n realm,\n method: options.method,\n url,\n });\n\n const headers: Record<string, string> = {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n };\n\n const timeoutMs = options.timeout ?? this.config.timeout;\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body != null ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n await this.handleErrorResponse(response);\n }\n\n // 204 No Content — no body to parse\n const data = response.status === 204\n ? (undefined as T)\n : (await response.json()) as T;\n return { status: response.status, headers: response.headers, data };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof NetSuiteError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(timeoutMs);\n }\n\n throw new NetSuiteError(\n `Network error: ${error instanceof Error ? error.message : String(error)}`,\n undefined,\n 'NETWORK_ERROR',\n );\n }\n }\n\n private buildUrl(path: string, query?: Record<string, string>): string {\n const url = new URL(path, this.config.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n }\n\n private async handleErrorResponse(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = await response.text().catch(() => undefined);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(\n `Authentication failed: ${response.status} ${response.statusText}`,\n response.status,\n body,\n );\n }\n\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n const retryMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000;\n throw new RateLimitError(retryMs, body);\n }\n\n throw new NetSuiteError(\n `HTTP ${response.status}: ${response.statusText}`,\n response.status,\n undefined,\n body,\n );\n }\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLResult, SuiteQLResponse, SuiteQLRow } from '../types.js';\n\nconst SUITEQL_PATH = '/services/rest/query/v1/suiteql';\n\n/**\n * Execute a SuiteQL query and return the first page of results.\n */\nexport async function executeSuiteQL<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: { limit?: number; offset?: number },\n): Promise<SuiteQLResult<T>> {\n const limit = options?.limit ?? 1000;\n const offset = options?.offset ?? 0;\n\n const response = await client.request<SuiteQLResponse>({\n method: 'POST',\n path: SUITEQL_PATH,\n body: { q: query },\n headers: {\n Prefer: 'transient',\n },\n query: {\n limit: limit.toString(),\n offset: offset.toString(),\n },\n retry: true, // POST-for-query is idempotent\n });\n\n // Strip HATEOAS `links` metadata from each row\n const items = (response.data.items ?? []).map(({ links, ...rest }: any) => rest) as T[];\n\n return {\n items,\n totalResults: response.data.totalResults,\n hasMore: response.data.hasMore,\n };\n}\n","import type { NetSuiteClient } from '../http/client.js';\nimport type { SuiteQLRow } from '../types.js';\nimport { executeSuiteQL } from './executor.js';\n\nexport interface PaginationOptions {\n /** Page size per request. Default: 1000. */\n pageSize?: number;\n /** Maximum total rows to fetch. Default: unlimited. */\n maxRows?: number;\n}\n\n/**\n * Execute a SuiteQL query and automatically paginate through all results.\n * Collects all pages into a single array.\n */\nexport async function executeSuiteQLPaginated<T = SuiteQLRow>(\n client: NetSuiteClient,\n query: string,\n options?: PaginationOptions,\n): Promise<T[]> {\n const pageSize = options?.pageSize ?? 1000;\n const maxRows = options?.maxRows ?? Infinity;\n const allItems: T[] = [];\n let offset = 0;\n\n while (allItems.length < maxRows) {\n const limit = Math.min(pageSize, maxRows - allItems.length);\n const result = await executeSuiteQL<T>(client, query, { limit, offset });\n\n allItems.push(...result.items);\n\n if (!result.hasMore || result.items.length === 0) {\n break;\n }\n\n offset += result.items.length;\n }\n\n return allItems;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\n\n/**\n * Create a new record via the REST Record API.\n * POST /services/rest/record/v1/{recordType}\n */\nexport async function createRecord(\n client: NetSuiteClient,\n recordType: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${recordType}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Update an existing record via the REST Record API.\n * PATCH /services/rest/record/v1/{recordType}/{id}\n */\nexport async function updateRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PATCH',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Delete a record via the REST Record API.\n * DELETE /services/rest/record/v1/{recordType}/{id}\n */\nexport async function deleteRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<void> {\n await client.request({\n method: 'DELETE',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n}\n\n/**\n * Upsert a record via the REST Record API (create or update by externalId).\n * PUT /services/rest/record/v1/{recordType}/eid:{externalId}\n */\nexport async function upsertRecord(\n client: NetSuiteClient,\n recordType: string,\n externalId: string,\n data: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'PUT',\n path: `${RECORD_BASE}/${recordType}/eid:${externalId}`,\n body: data,\n });\n return response.data;\n}\n\n/**\n * Get a single record by ID via the REST Record API.\n * GET /services/rest/record/v1/{recordType}/{id}\n */\nexport async function getRecord(\n client: NetSuiteClient,\n recordType: string,\n id: string | number,\n): Promise<Record<string, unknown>> {\n const response = await client.request<Record<string, unknown>>({\n method: 'GET',\n path: `${RECORD_BASE}/${recordType}/${id}`,\n });\n return response.data;\n}\n","import type { NetSuiteClient } from '../http/client.js';\n\nconst RECORD_BASE = '/services/rest/record/v1';\nconst DEFAULT_TRANSFORM_TIMEOUT = 120_000; // 2 minutes\n\nexport interface TransformOptions {\n /** Request body (field overrides on the target record). */\n body?: Record<string, unknown>;\n /** Timeout in ms. Default: 120000 (2 minutes). */\n timeout?: number;\n}\n\nexport interface TransformResult {\n /** The internal ID of the newly created record. */\n id: string;\n /** The target record type. */\n type: string;\n}\n\n/**\n * Transform a record from one type to another via the NetSuite REST API.\n *\n * POST /services/rest/record/v1/{sourceType}/{sourceId}/!transform/{targetType}\n *\n * Uses a longer default timeout (120s) since transforms can be slow.\n * Never retries — transforms are non-idempotent mutations.\n */\nexport async function transformRecord(\n client: NetSuiteClient,\n sourceType: string,\n sourceId: string | number,\n targetType: string,\n options?: TransformOptions,\n): Promise<TransformResult> {\n const response = await client.request<Record<string, unknown>>({\n method: 'POST',\n path: `${RECORD_BASE}/${sourceType}/${sourceId}/!transform/${targetType}`,\n body: options?.body ?? {},\n timeout: options?.timeout ?? DEFAULT_TRANSFORM_TIMEOUT,\n retry: false,\n });\n\n // Extract ID from the Location header or response body\n const location = response.headers.get('Location');\n let id: string | undefined;\n\n if (location) {\n // Location: /services/rest/record/v1/{targetType}/{id}\n const match = location.match(/\\/(\\d+)$/);\n if (match) {\n id = match[1];\n }\n }\n\n // Fall back to response body if Location header doesn't have the ID\n if (!id && response.data) {\n const rawId = response.data['id'] ?? response.data['internalid'] ?? response.data['internalId'];\n if (rawId != null) {\n id = String(rawId);\n }\n }\n\n if (!id) {\n throw new Error(\n `Transform succeeded (HTTP ${response.status}) but could not extract the new record ID. ` +\n `Location header: ${location ?? '(none)'}`,\n );\n }\n\n return { id, type: targetType };\n}\n"],"mappings":"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","names":["RECORD_BASE"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suiteportal/connector",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "NetSuite OAuth 1.0a connector with SuiteQL execution and REST Record CRUD",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,7 +24,7 @@
24
24
  "author": "Trey Hulse",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/treyhulse/netsuite-orm",
27
+ "url": "https://github.com/Suite-Portal/netsuite-orm",
28
28
  "directory": "packages/connector"
29
29
  },
30
30
  "publishConfig": {