@revenexx/integrations-node-sdk 0.14.0 → 0.16.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
@@ -23,29 +23,41 @@ __export(index_exports, {
23
23
  ApiKeyCredential: () => ApiKeyCredential,
24
24
  BaseCredential: () => BaseCredential,
25
25
  BasicAuthCredential: () => BasicAuthCredential,
26
+ DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
26
27
  DEFAULT_RETRY_ATTEMPTS: () => DEFAULT_RETRY_ATTEMPTS,
27
28
  DEFAULT_RETRY_DELAY_MS: () => DEFAULT_RETRY_DELAY_MS,
29
+ DEFAULT_RETRY_POLICY: () => DEFAULT_RETRY_POLICY,
28
30
  DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
29
31
  MANIFEST_VERSION: () => MANIFEST_VERSION,
32
+ MAX_RESPONSE_BYTES: () => MAX_RESPONSE_BYTES,
30
33
  MAX_RETRY_ATTEMPTS: () => MAX_RETRY_ATTEMPTS,
31
34
  MAX_TIMEOUT_MS: () => MAX_TIMEOUT_MS,
32
35
  NodeError: () => NodeError,
33
36
  OAuth2AuthCodeCredential: () => OAuth2AuthCodeCredential,
34
37
  OAuth2ClientCredentialsCredential: () => OAuth2ClientCredentialsCredential,
38
+ RetryableError: () => RetryableError,
35
39
  SimpleValueCredential: () => SimpleValueCredential,
40
+ backoffDelay: () => backoffDelay,
36
41
  buildManifest: () => buildManifest,
42
+ clampResponseBytes: () => clampResponseBytes,
37
43
  extractCredentialManifest: () => extractCredentialManifest,
38
44
  extractCredentialManifests: () => extractCredentialManifests,
39
45
  extractManifest: () => extractManifest,
40
46
  extractManifests: () => extractManifests,
41
47
  isNodeWithIteration: () => isNodeWithIteration,
42
48
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
49
+ maxBytesConfigField: () => maxBytesConfigField,
43
50
  normalizeCredentialType: () => normalizeCredentialType,
44
51
  normalizeLocalized: () => normalizeLocalized,
45
52
  parsePackageMeta: () => parsePackageMeta,
53
+ readArrayBuffer: () => readArrayBuffer,
54
+ readJsonOrText: () => readJsonOrText,
55
+ readText: () => readText,
46
56
  retryConfigFields: () => retryConfigFields,
47
57
  safeFetch: () => safeFetch,
48
- timeoutConfigField: () => timeoutConfigField
58
+ sleepWithSignal: () => sleepWithSignal,
59
+ timeoutConfigField: () => timeoutConfigField,
60
+ withRetry: () => withRetry
49
61
  });
50
62
  module.exports = __toCommonJS(index_exports);
51
63
 
@@ -123,6 +135,12 @@ var MAX_TIMEOUT_MS = 12e4;
123
135
  var DEFAULT_RETRY_ATTEMPTS = 0;
124
136
  var MAX_RETRY_ATTEMPTS = 5;
125
137
  var DEFAULT_RETRY_DELAY_MS = 1e3;
138
+ var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
139
+ var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
140
+ function clampResponseBytes(maxBytes) {
141
+ if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
142
+ return Math.min(maxBytes, MAX_RESPONSE_BYTES);
143
+ }
126
144
  async function safeFetch(url, options = {}) {
127
145
  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
128
146
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
@@ -160,6 +178,82 @@ async function safeFetch(url, options = {}) {
160
178
  }
161
179
  throw lastError;
162
180
  }
181
+ function tooLargeError(status, bytes, maxBytes) {
182
+ return new NodeError(
183
+ "RESPONSE_TOO_LARGE",
184
+ `Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
185
+ { status }
186
+ );
187
+ }
188
+ async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
189
+ const cap = clampResponseBytes(maxBytes);
190
+ const declared = Number(res.headers.get("content-length"));
191
+ if (Number.isFinite(declared) && declared > cap) {
192
+ throw tooLargeError(res.status, declared, cap);
193
+ }
194
+ const body = res.body;
195
+ if (!body) {
196
+ const buf = await res.arrayBuffer();
197
+ if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
198
+ return buf;
199
+ }
200
+ const reader = body.getReader();
201
+ const chunks = [];
202
+ let total = 0;
203
+ try {
204
+ for (; ; ) {
205
+ const { done, value } = await reader.read();
206
+ if (done) break;
207
+ total += value.byteLength;
208
+ if (total > cap) {
209
+ await reader.cancel().catch(() => {
210
+ });
211
+ throw tooLargeError(res.status, total, cap);
212
+ }
213
+ chunks.push(value);
214
+ }
215
+ } finally {
216
+ reader.releaseLock();
217
+ }
218
+ const out = new Uint8Array(total);
219
+ let offset = 0;
220
+ for (const chunk of chunks) {
221
+ out.set(chunk, offset);
222
+ offset += chunk.byteLength;
223
+ }
224
+ return out.buffer;
225
+ }
226
+ async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
227
+ const buf = await readArrayBuffer(res, maxBytes);
228
+ return new TextDecoder().decode(buf);
229
+ }
230
+ function isJsonContentType(contentType) {
231
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
232
+ return mediaType === "application/json" || mediaType.endsWith("+json");
233
+ }
234
+ async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
235
+ const contentType = res.headers.get("content-type") ?? "";
236
+ const text = await readText(res, maxBytes);
237
+ if (!isJsonContentType(contentType)) return text;
238
+ try {
239
+ return JSON.parse(text);
240
+ } catch (e) {
241
+ throw new NodeError(
242
+ "RESPONSE_PARSE_ERROR",
243
+ `Invalid JSON response: ${e.message}`,
244
+ { status: res.status }
245
+ );
246
+ }
247
+ }
248
+ function maxBytesConfigField(opts) {
249
+ return {
250
+ key: "maxBytes",
251
+ label: "Max response size (bytes)",
252
+ type: "number",
253
+ default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
254
+ validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
255
+ };
256
+ }
163
257
  function timeoutConfigField(opts) {
164
258
  return {
165
259
  key: "timeoutMs",
@@ -188,6 +282,74 @@ function retryConfigFields(opts) {
188
282
  ];
189
283
  }
190
284
 
285
+ // src/retry.ts
286
+ var RetryableError = class extends Error {
287
+ /** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
288
+ retryAfterMs;
289
+ constructor(message, opts) {
290
+ super(message, { cause: opts?.cause });
291
+ this.name = "RetryableError";
292
+ this.retryAfterMs = opts?.retryAfterMs;
293
+ }
294
+ };
295
+ var DEFAULT_RETRY_POLICY = {
296
+ maxAttempts: 3,
297
+ baseDelayMs: 500,
298
+ maxDelayMs: 3e4,
299
+ factor: 2,
300
+ jitter: true
301
+ };
302
+ function abortReason(signal) {
303
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
304
+ }
305
+ function backoffDelay(attempt, policy) {
306
+ const exponent = Math.max(0, attempt - 1);
307
+ const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
308
+ const cap = Math.min(policy.maxDelayMs, raw);
309
+ return policy.jitter ? Math.random() * cap : cap;
310
+ }
311
+ function sleepWithSignal(ms, signal) {
312
+ return new Promise((resolve, reject) => {
313
+ if (signal.aborted) {
314
+ reject(abortReason(signal));
315
+ return;
316
+ }
317
+ const onAbort = () => {
318
+ clearTimeout(timer);
319
+ reject(abortReason(signal));
320
+ };
321
+ const timer = setTimeout(() => {
322
+ signal.removeEventListener("abort", onAbort);
323
+ resolve();
324
+ }, ms);
325
+ signal.addEventListener("abort", onAbort, { once: true });
326
+ });
327
+ }
328
+ async function withRetry(fn, policy, hooks) {
329
+ const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
330
+ const maxAttempts = Math.max(1, effective.maxAttempts);
331
+ const { signal, logger, onRetry } = hooks;
332
+ for (let attempt = 1; ; attempt++) {
333
+ if (signal.aborted) throw abortReason(signal);
334
+ try {
335
+ return await fn(attempt);
336
+ } catch (err) {
337
+ if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
338
+ const retryAfter = err.retryAfterMs;
339
+ const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
340
+ const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
341
+ onRetry?.({ attempt, delayMs, error: err });
342
+ logger?.warn("Retrying after retryable error", {
343
+ attempt,
344
+ maxAttempts,
345
+ delayMs,
346
+ error: err.message
347
+ });
348
+ await sleepWithSignal(delayMs, signal);
349
+ }
350
+ }
351
+ }
352
+
191
353
  // src/manifest.ts
192
354
  var MANIFEST_VERSION = "v0-draft";
193
355
  function parsePackageMeta(raw) {
@@ -424,27 +586,39 @@ function base64Url(buf) {
424
586
  ApiKeyCredential,
425
587
  BaseCredential,
426
588
  BasicAuthCredential,
589
+ DEFAULT_MAX_RESPONSE_BYTES,
427
590
  DEFAULT_RETRY_ATTEMPTS,
428
591
  DEFAULT_RETRY_DELAY_MS,
592
+ DEFAULT_RETRY_POLICY,
429
593
  DEFAULT_TIMEOUT_MS,
430
594
  MANIFEST_VERSION,
595
+ MAX_RESPONSE_BYTES,
431
596
  MAX_RETRY_ATTEMPTS,
432
597
  MAX_TIMEOUT_MS,
433
598
  NodeError,
434
599
  OAuth2AuthCodeCredential,
435
600
  OAuth2ClientCredentialsCredential,
601
+ RetryableError,
436
602
  SimpleValueCredential,
603
+ backoffDelay,
437
604
  buildManifest,
605
+ clampResponseBytes,
438
606
  extractCredentialManifest,
439
607
  extractCredentialManifests,
440
608
  extractManifest,
441
609
  extractManifests,
442
610
  isNodeWithIteration,
443
611
  isOAuthAuthorizeCredential,
612
+ maxBytesConfigField,
444
613
  normalizeCredentialType,
445
614
  normalizeLocalized,
446
615
  parsePackageMeta,
616
+ readArrayBuffer,
617
+ readJsonOrText,
618
+ readText,
447
619
  retryConfigFields,
448
620
  safeFetch,
449
- timeoutConfigField
621
+ sleepWithSignal,
622
+ timeoutConfigField,
623
+ withRetry
450
624
  });
package/dist/index.d.cts CHANGED
@@ -113,6 +113,12 @@ interface INodeDescription {
113
113
  name: LocalizedString;
114
114
  description?: LocalizedString;
115
115
  icon?: string;
116
+ /**
117
+ * Curated node-picker group path, outermost first (max 4 levels), e.g.
118
+ * `[{ en: 'Business Central' }, { en: 'Sales Orders' }]`. Optional —
119
+ * pickers without it fall back to package/category grouping.
120
+ */
121
+ groups?: LocalizedString[];
116
122
  /** Associated images (screenshots, logos, banners) shipped with the package. */
117
123
  images?: IImage[];
118
124
  inputs: Record<string, IInputPort>;
@@ -434,6 +440,21 @@ declare const MAX_TIMEOUT_MS = 120000;
434
440
  declare const DEFAULT_RETRY_ATTEMPTS = 0;
435
441
  declare const MAX_RETRY_ATTEMPTS = 5;
436
442
  declare const DEFAULT_RETRY_DELAY_MS = 1000;
443
+ /**
444
+ * Default cap for response bodies read via {@link readArrayBuffer} /
445
+ * {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
446
+ * against a single oversized response exhausting its memory.
447
+ */
448
+ declare const DEFAULT_MAX_RESPONSE_BYTES: number;
449
+ /**
450
+ * Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
451
+ * (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
452
+ * large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
453
+ * the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
454
+ */
455
+ declare const MAX_RESPONSE_BYTES: number;
456
+ /** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
457
+ declare function clampResponseBytes(maxBytes: number): number;
437
458
  interface SafeFetchRetry {
438
459
  attempts: number;
439
460
  delayMs?: number;
@@ -445,6 +466,31 @@ interface SafeFetchOptions extends RequestInit {
445
466
  retry?: SafeFetchRetry;
446
467
  }
447
468
  declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
469
+ /**
470
+ * Read a response body into an ArrayBuffer while enforcing a hard byte cap.
471
+ *
472
+ * The `Content-Length` header is used as a fast-reject (bail before downloading
473
+ * anything), but the limit is *also* enforced while streaming, since the header
474
+ * can be absent or lie. On overrun the stream is cancelled and a
475
+ * `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
476
+ */
477
+ declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
478
+ /** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
479
+ declare function readText(res: Response, maxBytes?: number): Promise<string>;
480
+ /**
481
+ * Read a response body as JSON when the `Content-Type` denotes JSON (see
482
+ * {@link isJsonContentType}), otherwise as text — the content-type sniff
483
+ * previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
484
+ * `maxBytes` (see {@link readArrayBuffer}).
485
+ *
486
+ * A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
487
+ * rather than a raw `SyntaxError`, keeping to the SDK error contract.
488
+ */
489
+ declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
490
+ declare function maxBytesConfigField(opts?: {
491
+ default?: number;
492
+ max?: number;
493
+ }): IConfigField;
448
494
  declare function timeoutConfigField(opts?: {
449
495
  default?: number;
450
496
  max?: number;
@@ -454,6 +500,82 @@ declare function retryConfigFields(opts?: {
454
500
  defaultDelayMs?: number;
455
501
  }): IConfigField[];
456
502
 
503
+ /**
504
+ * Transport-agnostic retry/backoff primitive (PO-139).
505
+ *
506
+ * Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
507
+ * transport — with exponential backoff + full jitter, honouring a
508
+ * server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
509
+ *
510
+ * The retry decision lives with the connector: it throws a {@link RetryableError}
511
+ * only when an attempt failed *and* is worth retrying. Everything else is
512
+ * rethrown as-is, and terminal API errors that connectors model as *values*
513
+ * (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
514
+ *
515
+ * This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
516
+ * it does not replace Temporal activity-level retries — it is the fine-grained,
517
+ * idempotency-aware, `Retry-After`-honouring layer inside a single activity.
518
+ */
519
+ /** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
520
+ declare class RetryableError extends Error {
521
+ /** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
522
+ readonly retryAfterMs?: number;
523
+ constructor(message: string, opts?: {
524
+ retryAfterMs?: number;
525
+ cause?: unknown;
526
+ });
527
+ }
528
+ interface RetryPolicy {
529
+ /** Total tries including the first. */
530
+ maxAttempts: number;
531
+ /** Base delay for the exponential backoff. */
532
+ baseDelayMs: number;
533
+ /** Upper bound for any single computed delay. */
534
+ maxDelayMs: number;
535
+ /** Exponential growth factor. */
536
+ factor: number;
537
+ /** Apply full jitter to the computed backoff. */
538
+ jitter: boolean;
539
+ }
540
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
541
+ interface RetryHooks {
542
+ /** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
543
+ signal: AbortSignal;
544
+ logger?: {
545
+ warn(msg: string, meta?: Record<string, unknown>): void;
546
+ };
547
+ onRetry?(info: {
548
+ attempt: number;
549
+ delayMs: number;
550
+ error: unknown;
551
+ }): void;
552
+ }
553
+ /**
554
+ * Pure backoff math, exported for testing/reuse. Returns the delay before the
555
+ * retry that follows the given (1-based) failed `attempt`: an exponential term
556
+ * `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
557
+ * (`random() * cap`) when `policy.jitter` is set.
558
+ */
559
+ declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
560
+ /**
561
+ * Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
562
+ * (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
563
+ * caller's retry loop stops cleanly instead of finishing the wait.
564
+ */
565
+ declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
566
+ /**
567
+ * Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
568
+ * (`RetryableError.retryAfterMs` when present, else the computed
569
+ * {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
570
+ * rethrown immediately; on the final attempt the RetryableError itself is
571
+ * thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
572
+ *
573
+ * Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
574
+ * unchanged: that is a simple in-fetch retry; this is the richer, transport-
575
+ * agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
576
+ */
577
+ declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
578
+
457
579
  /**
458
580
  * Envelope version expected by the integrations server-side
459
581
  * `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
@@ -624,4 +746,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
624
746
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
625
747
  }
626
748
 
627
- export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
749
+ export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
package/dist/index.d.ts CHANGED
@@ -113,6 +113,12 @@ interface INodeDescription {
113
113
  name: LocalizedString;
114
114
  description?: LocalizedString;
115
115
  icon?: string;
116
+ /**
117
+ * Curated node-picker group path, outermost first (max 4 levels), e.g.
118
+ * `[{ en: 'Business Central' }, { en: 'Sales Orders' }]`. Optional —
119
+ * pickers without it fall back to package/category grouping.
120
+ */
121
+ groups?: LocalizedString[];
116
122
  /** Associated images (screenshots, logos, banners) shipped with the package. */
117
123
  images?: IImage[];
118
124
  inputs: Record<string, IInputPort>;
@@ -434,6 +440,21 @@ declare const MAX_TIMEOUT_MS = 120000;
434
440
  declare const DEFAULT_RETRY_ATTEMPTS = 0;
435
441
  declare const MAX_RETRY_ATTEMPTS = 5;
436
442
  declare const DEFAULT_RETRY_DELAY_MS = 1000;
443
+ /**
444
+ * Default cap for response bodies read via {@link readArrayBuffer} /
445
+ * {@link readText} / {@link readJsonOrText}. Guards the (shared) worker process
446
+ * against a single oversized response exhausting its memory.
447
+ */
448
+ declare const DEFAULT_MAX_RESPONSE_BYTES: number;
449
+ /**
450
+ * Hard upper ceiling for any per-node `maxBytes`. Even a permissive node author
451
+ * (who calls {@link maxBytesConfigField} without an explicit `max`, or passes a
452
+ * large `maxBytes` to the `read*` helpers) can never lift the cap above this, so
453
+ * the shared worker's memory stays bounded. Analogous to {@link MAX_TIMEOUT_MS}.
454
+ */
455
+ declare const MAX_RESPONSE_BYTES: number;
456
+ /** Clamp a requested `maxBytes` into `[1, MAX_RESPONSE_BYTES]`. */
457
+ declare function clampResponseBytes(maxBytes: number): number;
437
458
  interface SafeFetchRetry {
438
459
  attempts: number;
439
460
  delayMs?: number;
@@ -445,6 +466,31 @@ interface SafeFetchOptions extends RequestInit {
445
466
  retry?: SafeFetchRetry;
446
467
  }
447
468
  declare function safeFetch(url: string | URL, options?: SafeFetchOptions): Promise<Response>;
469
+ /**
470
+ * Read a response body into an ArrayBuffer while enforcing a hard byte cap.
471
+ *
472
+ * The `Content-Length` header is used as a fast-reject (bail before downloading
473
+ * anything), but the limit is *also* enforced while streaming, since the header
474
+ * can be absent or lie. On overrun the stream is cancelled and a
475
+ * `NodeError('RESPONSE_TOO_LARGE', …, { status })` is thrown.
476
+ */
477
+ declare function readArrayBuffer(res: Response, maxBytes?: number): Promise<ArrayBuffer>;
478
+ /** Read a response body as UTF-8 text, capped at `maxBytes` (see {@link readArrayBuffer}). */
479
+ declare function readText(res: Response, maxBytes?: number): Promise<string>;
480
+ /**
481
+ * Read a response body as JSON when the `Content-Type` denotes JSON (see
482
+ * {@link isJsonContentType}), otherwise as text — the content-type sniff
483
+ * previously duplicated across the HTTP/Upload/DeepL node sinks. Capped at
484
+ * `maxBytes` (see {@link readArrayBuffer}).
485
+ *
486
+ * A malformed JSON body surfaces as `NodeError('RESPONSE_PARSE_ERROR', …, { status })`
487
+ * rather than a raw `SyntaxError`, keeping to the SDK error contract.
488
+ */
489
+ declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
490
+ declare function maxBytesConfigField(opts?: {
491
+ default?: number;
492
+ max?: number;
493
+ }): IConfigField;
448
494
  declare function timeoutConfigField(opts?: {
449
495
  default?: number;
450
496
  max?: number;
@@ -454,6 +500,82 @@ declare function retryConfigFields(opts?: {
454
500
  defaultDelayMs?: number;
455
501
  }): IConfigField[];
456
502
 
503
+ /**
504
+ * Transport-agnostic retry/backoff primitive (PO-139).
505
+ *
506
+ * Wraps *any* async operation — raw `fetch`, an SDK client (axios), a future
507
+ * transport — with exponential backoff + full jitter, honouring a
508
+ * server-dictated delay (HTTP `Retry-After`) when the caller supplies one.
509
+ *
510
+ * The retry decision lives with the connector: it throws a {@link RetryableError}
511
+ * only when an attempt failed *and* is worth retrying. Everything else is
512
+ * rethrown as-is, and terminal API errors that connectors model as *values*
513
+ * (e.g. a `{ kind: 'http-error' }` union) simply flow back as the return value.
514
+ *
515
+ * This is deliberately NOT a circuit breaker, rate limiter, or HTTP client, and
516
+ * it does not replace Temporal activity-level retries — it is the fine-grained,
517
+ * idempotency-aware, `Retry-After`-honouring layer inside a single activity.
518
+ */
519
+ /** Thrown by the wrapped fn to signal "this attempt failed but is retryable". */
520
+ declare class RetryableError extends Error {
521
+ /** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
522
+ readonly retryAfterMs?: number;
523
+ constructor(message: string, opts?: {
524
+ retryAfterMs?: number;
525
+ cause?: unknown;
526
+ });
527
+ }
528
+ interface RetryPolicy {
529
+ /** Total tries including the first. */
530
+ maxAttempts: number;
531
+ /** Base delay for the exponential backoff. */
532
+ baseDelayMs: number;
533
+ /** Upper bound for any single computed delay. */
534
+ maxDelayMs: number;
535
+ /** Exponential growth factor. */
536
+ factor: number;
537
+ /** Apply full jitter to the computed backoff. */
538
+ jitter: boolean;
539
+ }
540
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
541
+ interface RetryHooks {
542
+ /** Pass `ctx.signal` so cancelling a workflow cancels the wait (and stops retrying) immediately. */
543
+ signal: AbortSignal;
544
+ logger?: {
545
+ warn(msg: string, meta?: Record<string, unknown>): void;
546
+ };
547
+ onRetry?(info: {
548
+ attempt: number;
549
+ delayMs: number;
550
+ error: unknown;
551
+ }): void;
552
+ }
553
+ /**
554
+ * Pure backoff math, exported for testing/reuse. Returns the delay before the
555
+ * retry that follows the given (1-based) failed `attempt`: an exponential term
556
+ * `baseDelayMs * factor^(attempt-1)` capped at `maxDelayMs`, with full jitter
557
+ * (`random() * cap`) when `policy.jitter` is set.
558
+ */
559
+ declare function backoffDelay(attempt: number, policy: RetryPolicy): number;
560
+ /**
561
+ * Abort-aware sleep. Resolves after `ms`, or rejects with the signal's reason
562
+ * (a DOMException `AbortError` by default) as soon as `signal` aborts — so a
563
+ * caller's retry loop stops cleanly instead of finishing the wait.
564
+ */
565
+ declare function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void>;
566
+ /**
567
+ * Runs `fn`; if it throws a {@link RetryableError} and attempts remain, waits
568
+ * (`RetryableError.retryAfterMs` when present, else the computed
569
+ * {@link backoffDelay}) respecting `signal`, then retries. Any other throw is
570
+ * rethrown immediately; on the final attempt the RetryableError itself is
571
+ * thrown (preserving its `retryAfterMs`/`cause`), not its `.cause`.
572
+ *
573
+ * Distinct from `safeFetch`'s built-in fixed-delay `retry` option, which is
574
+ * unchanged: that is a simple in-fetch retry; this is the richer, transport-
575
+ * agnostic, `Retry-After`-aware primitive connectors wrap around any operation.
576
+ */
577
+ declare function withRetry<T>(fn: (attempt: number) => Promise<T>, policy: Partial<RetryPolicy>, hooks: RetryHooks): Promise<T>;
578
+
457
579
  /**
458
580
  * Envelope version expected by the integrations server-side
459
581
  * `TarballInspector`. Earlier (pre-registry) builds emitted a bare array
@@ -624,4 +746,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
624
746
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
625
747
  }
626
748
 
627
- export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, buildManifest, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, normalizeCredentialType, normalizeLocalized, parsePackageMeta, retryConfigFields, safeFetch, timeoutConfigField };
749
+ export { ApiKeyCredential, BaseCredential, BasicAuthCredential, type ConfigType, type CredentialAuthKind, type CredentialFieldType, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAY_MS, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, type DataType, type IConfigField, type IConfigFieldBase, type IConfigOption, type IConfigValidation, type ICredential, type ICredentialContext, type ICredentialDescription, type ICredentialField, type ICredentialOAuthAuthorize, type ICredentialResolveResult, type ICredentialTestResult, type IImage, type IInputPort, type INode, type INodeAuthorContext, type INodeContext, type INodeDescription, type INodeResult, type INodeWithIteration, type IOutputField, type IOutputPort, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, MANIFEST_VERSION, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
package/dist/index.js CHANGED
@@ -68,6 +68,12 @@ var MAX_TIMEOUT_MS = 12e4;
68
68
  var DEFAULT_RETRY_ATTEMPTS = 0;
69
69
  var MAX_RETRY_ATTEMPTS = 5;
70
70
  var DEFAULT_RETRY_DELAY_MS = 1e3;
71
+ var DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
72
+ var MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
73
+ function clampResponseBytes(maxBytes) {
74
+ if (!Number.isFinite(maxBytes) || maxBytes < 1) return MAX_RESPONSE_BYTES;
75
+ return Math.min(maxBytes, MAX_RESPONSE_BYTES);
76
+ }
71
77
  async function safeFetch(url, options = {}) {
72
78
  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: ctxSignal, retry, ...fetchOptions } = options;
73
79
  const effectiveMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
@@ -105,6 +111,82 @@ async function safeFetch(url, options = {}) {
105
111
  }
106
112
  throw lastError;
107
113
  }
114
+ function tooLargeError(status, bytes, maxBytes) {
115
+ return new NodeError(
116
+ "RESPONSE_TOO_LARGE",
117
+ `Response body of ${bytes} bytes exceeds the ${maxBytes}-byte limit`,
118
+ { status }
119
+ );
120
+ }
121
+ async function readArrayBuffer(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
122
+ const cap = clampResponseBytes(maxBytes);
123
+ const declared = Number(res.headers.get("content-length"));
124
+ if (Number.isFinite(declared) && declared > cap) {
125
+ throw tooLargeError(res.status, declared, cap);
126
+ }
127
+ const body = res.body;
128
+ if (!body) {
129
+ const buf = await res.arrayBuffer();
130
+ if (buf.byteLength > cap) throw tooLargeError(res.status, buf.byteLength, cap);
131
+ return buf;
132
+ }
133
+ const reader = body.getReader();
134
+ const chunks = [];
135
+ let total = 0;
136
+ try {
137
+ for (; ; ) {
138
+ const { done, value } = await reader.read();
139
+ if (done) break;
140
+ total += value.byteLength;
141
+ if (total > cap) {
142
+ await reader.cancel().catch(() => {
143
+ });
144
+ throw tooLargeError(res.status, total, cap);
145
+ }
146
+ chunks.push(value);
147
+ }
148
+ } finally {
149
+ reader.releaseLock();
150
+ }
151
+ const out = new Uint8Array(total);
152
+ let offset = 0;
153
+ for (const chunk of chunks) {
154
+ out.set(chunk, offset);
155
+ offset += chunk.byteLength;
156
+ }
157
+ return out.buffer;
158
+ }
159
+ async function readText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
160
+ const buf = await readArrayBuffer(res, maxBytes);
161
+ return new TextDecoder().decode(buf);
162
+ }
163
+ function isJsonContentType(contentType) {
164
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
165
+ return mediaType === "application/json" || mediaType.endsWith("+json");
166
+ }
167
+ async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
168
+ const contentType = res.headers.get("content-type") ?? "";
169
+ const text = await readText(res, maxBytes);
170
+ if (!isJsonContentType(contentType)) return text;
171
+ try {
172
+ return JSON.parse(text);
173
+ } catch (e) {
174
+ throw new NodeError(
175
+ "RESPONSE_PARSE_ERROR",
176
+ `Invalid JSON response: ${e.message}`,
177
+ { status: res.status }
178
+ );
179
+ }
180
+ }
181
+ function maxBytesConfigField(opts) {
182
+ return {
183
+ key: "maxBytes",
184
+ label: "Max response size (bytes)",
185
+ type: "number",
186
+ default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
187
+ validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
188
+ };
189
+ }
108
190
  function timeoutConfigField(opts) {
109
191
  return {
110
192
  key: "timeoutMs",
@@ -133,6 +215,74 @@ function retryConfigFields(opts) {
133
215
  ];
134
216
  }
135
217
 
218
+ // src/retry.ts
219
+ var RetryableError = class extends Error {
220
+ /** Server-dictated delay before the next attempt, e.g. parsed from `Retry-After`. */
221
+ retryAfterMs;
222
+ constructor(message, opts) {
223
+ super(message, { cause: opts?.cause });
224
+ this.name = "RetryableError";
225
+ this.retryAfterMs = opts?.retryAfterMs;
226
+ }
227
+ };
228
+ var DEFAULT_RETRY_POLICY = {
229
+ maxAttempts: 3,
230
+ baseDelayMs: 500,
231
+ maxDelayMs: 3e4,
232
+ factor: 2,
233
+ jitter: true
234
+ };
235
+ function abortReason(signal) {
236
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
237
+ }
238
+ function backoffDelay(attempt, policy) {
239
+ const exponent = Math.max(0, attempt - 1);
240
+ const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
241
+ const cap = Math.min(policy.maxDelayMs, raw);
242
+ return policy.jitter ? Math.random() * cap : cap;
243
+ }
244
+ function sleepWithSignal(ms, signal) {
245
+ return new Promise((resolve, reject) => {
246
+ if (signal.aborted) {
247
+ reject(abortReason(signal));
248
+ return;
249
+ }
250
+ const onAbort = () => {
251
+ clearTimeout(timer);
252
+ reject(abortReason(signal));
253
+ };
254
+ const timer = setTimeout(() => {
255
+ signal.removeEventListener("abort", onAbort);
256
+ resolve();
257
+ }, ms);
258
+ signal.addEventListener("abort", onAbort, { once: true });
259
+ });
260
+ }
261
+ async function withRetry(fn, policy, hooks) {
262
+ const effective = { ...DEFAULT_RETRY_POLICY, ...policy };
263
+ const maxAttempts = Math.max(1, effective.maxAttempts);
264
+ const { signal, logger, onRetry } = hooks;
265
+ for (let attempt = 1; ; attempt++) {
266
+ if (signal.aborted) throw abortReason(signal);
267
+ try {
268
+ return await fn(attempt);
269
+ } catch (err) {
270
+ if (!(err instanceof RetryableError) || attempt >= maxAttempts) throw err;
271
+ const retryAfter = err.retryAfterMs;
272
+ const rawDelay = typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : backoffDelay(attempt, effective);
273
+ const delayMs = Number.isFinite(rawDelay) && rawDelay >= 0 ? rawDelay : 0;
274
+ onRetry?.({ attempt, delayMs, error: err });
275
+ logger?.warn("Retrying after retryable error", {
276
+ attempt,
277
+ maxAttempts,
278
+ delayMs,
279
+ error: err.message
280
+ });
281
+ await sleepWithSignal(delayMs, signal);
282
+ }
283
+ }
284
+ }
285
+
136
286
  // src/credentials.ts
137
287
  import { createHash, randomBytes } from "crypto";
138
288
  function readString(source, key) {
@@ -341,27 +491,39 @@ export {
341
491
  ApiKeyCredential,
342
492
  BaseCredential,
343
493
  BasicAuthCredential,
494
+ DEFAULT_MAX_RESPONSE_BYTES,
344
495
  DEFAULT_RETRY_ATTEMPTS,
345
496
  DEFAULT_RETRY_DELAY_MS,
497
+ DEFAULT_RETRY_POLICY,
346
498
  DEFAULT_TIMEOUT_MS,
347
499
  MANIFEST_VERSION,
500
+ MAX_RESPONSE_BYTES,
348
501
  MAX_RETRY_ATTEMPTS,
349
502
  MAX_TIMEOUT_MS,
350
503
  NodeError,
351
504
  OAuth2AuthCodeCredential,
352
505
  OAuth2ClientCredentialsCredential,
506
+ RetryableError,
353
507
  SimpleValueCredential,
508
+ backoffDelay,
354
509
  buildManifest,
510
+ clampResponseBytes,
355
511
  extractCredentialManifest,
356
512
  extractCredentialManifests,
357
513
  extractManifest,
358
514
  extractManifests,
359
515
  isNodeWithIteration,
360
516
  isOAuthAuthorizeCredential,
517
+ maxBytesConfigField,
361
518
  normalizeCredentialType,
362
519
  normalizeLocalized,
363
520
  parsePackageMeta,
521
+ readArrayBuffer,
522
+ readJsonOrText,
523
+ readText,
364
524
  retryConfigFields,
365
525
  safeFetch,
366
- timeoutConfigField
526
+ sleepWithSignal,
527
+ timeoutConfigField,
528
+ withRetry
367
529
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",