@revenexx/integrations-node-sdk 1.2.0 → 1.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
@@ -54,11 +54,14 @@ __export(index_exports, {
54
54
  backoffDelay: () => backoffDelay,
55
55
  buildManifest: () => buildManifest,
56
56
  clampResponseBytes: () => clampResponseBytes,
57
+ errorPort: () => errorPort,
58
+ errorResult: () => errorResult,
57
59
  evaluate: () => evaluate,
58
60
  extractCredentialManifest: () => extractCredentialManifest,
59
61
  extractCredentialManifests: () => extractCredentialManifests,
60
62
  extractManifest: () => extractManifest,
61
63
  extractManifests: () => extractManifests,
64
+ httpErrorResult: () => httpErrorResult,
62
65
  isBlockedAddress: () => isBlockedAddress,
63
66
  isNodeWithIteration: () => isNodeWithIteration,
64
67
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
@@ -76,6 +79,8 @@ __export(index_exports, {
76
79
  sleepWithSignal: () => sleepWithSignal,
77
80
  takesValue: () => takesValue,
78
81
  timeoutConfigField: () => timeoutConfigField,
82
+ toErrorOutput: () => toErrorOutput,
83
+ toErrorResult: () => toErrorResult,
79
84
  withRetry: () => withRetry
80
85
  });
81
86
  module.exports = __toCommonJS(index_exports);
@@ -203,6 +208,76 @@ var NodeError = class extends Error {
203
208
  }
204
209
  };
205
210
 
211
+ // src/nodeErrors.ts
212
+ function toErrorOutput(err) {
213
+ if (err instanceof NodeError) {
214
+ const raw = err.meta?.["status"];
215
+ const status = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
216
+ return { code: err.code, message: err.message, status };
217
+ }
218
+ if (err instanceof Error && err.name === "AbortError") {
219
+ return { code: "TIMEOUT", message: "Request timed out", status: 0 };
220
+ }
221
+ if (err instanceof Error) {
222
+ return { code: "REQUEST_FAILED", message: err.message, status: 0 };
223
+ }
224
+ return { code: "REQUEST_FAILED", message: String(err), status: 0 };
225
+ }
226
+ function errorResult(output, port = "error") {
227
+ return { outputs: { [port]: output }, branch: port };
228
+ }
229
+ function toErrorResult(err, port = "error") {
230
+ return errorResult(toErrorOutput(err), port);
231
+ }
232
+ function httpErrorResult(err, port = "error") {
233
+ const body = err instanceof NodeError ? err.meta?.["body"] ?? null : null;
234
+ return errorResult({ ...toErrorOutput(err), body }, port);
235
+ }
236
+ function errorPort(opts = {}) {
237
+ return {
238
+ name: "error",
239
+ kind: "error",
240
+ dataType: "object",
241
+ label: { en: "Error", de: "Fehler" },
242
+ description: {
243
+ en: "Taken when the attempt fails in a way the workflow can carry on from. A node that cannot run as configured fails the run instead of taking this path.",
244
+ de: "Wird genommen, wenn der Versuch auf eine Weise fehlschl\xE4gt, mit der der Workflow weiterarbeiten kann. Ein Node, der so konfiguriert nicht laufen kann, l\xE4sst den Run scheitern, statt diesen Pfad zu nehmen."
245
+ },
246
+ fields: {
247
+ code: {
248
+ dataType: "string",
249
+ description: {
250
+ en: "Machine-readable name of the failure \u2014 branch on this rather than on the message. A node that talks to a remote system shares `TIMEOUT` and `REQUEST_FAILED` with every other one; the remaining codes belong to this node alone.",
251
+ de: "Maschinenlesbarer Name des Fehlers \u2014 hierauf verzweigen, nicht auf die Meldung. Ein Node, der mit einem Fremdsystem spricht, teilt `TIMEOUT` und `REQUEST_FAILED` mit allen anderen; die \xFCbrigen Codes geh\xF6ren allein zu diesem Node."
252
+ }
253
+ },
254
+ message: {
255
+ dataType: "string",
256
+ description: {
257
+ en: "Wording for whoever reads the run afterwards, frequently the wording the far end used. It is not a stable value to branch on.",
258
+ de: "Wortlaut f\xFCr den, der den Run sp\xE4ter liest, h\xE4ufig der des Gegen\xFCbers. Kein stabiler Wert, auf den sich Verzweigungen st\xFCtzen sollten."
259
+ }
260
+ },
261
+ status: {
262
+ dataType: "number",
263
+ description: {
264
+ en: "How the far end answered, as an HTTP status code, where one answered with a status at all. `0` otherwise \u2014 a timeout, a refused connection, an input the node could not use, a node that never leaves the workflow, or a failure the far end reported in its own answer rather than in a status, as a bulk job that ends as failed does. So `0` means there is no status to read, not that nothing was reached.",
265
+ de: "Wie das Gegen\xFCber geantwortet hat, als HTTP-Statuscode \u2014 sofern eines \xFCberhaupt mit einem Status geantwortet hat. Sonst `0`: Timeout, abgelehnte Verbindung, eine unbrauchbare Eingabe, ein Node, der den Workflow nicht verl\xE4sst, oder ein Fehler, den das Gegen\xFCber in seiner Antwort statt im Status gemeldet hat \u2014 wie ein Bulk-Job, der als fehlgeschlagen endet. `0` hei\xDFt also: kein Status zu lesen, nicht: nichts erreicht."
266
+ }
267
+ },
268
+ ...opts.body ? {
269
+ body: {
270
+ dataType: "any",
271
+ description: {
272
+ en: "The answer the far end gave, where the failure got far enough to have one: the response body, or the job the platform reported as failed.",
273
+ de: "Die Antwort des Gegen\xFCbers, sofern der Fehler weit genug kam, um eine zu haben: der Response-Body oder der von der Plattform als fehlgeschlagen gemeldete Job."
274
+ }
275
+ }
276
+ } : {}
277
+ }
278
+ };
279
+ }
280
+
206
281
  // src/extract.ts
207
282
  function extractManifest(node) {
208
283
  return node.description;
@@ -596,7 +671,11 @@ async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
596
671
  function maxBytesConfigField(opts) {
597
672
  return {
598
673
  key: "maxBytes",
599
- label: "Max response size (bytes)",
674
+ label: { en: "Max response size (bytes)", de: "Maximale Antwortgr\xF6\xDFe (Bytes)" },
675
+ description: {
676
+ en: "Answers larger than this are refused instead of read. The limit this package enforces applies above whatever is set here.",
677
+ de: "Gr\xF6\xDFere Antworten werden abgelehnt statt gelesen. \xDCber dem hier gesetzten Wert gilt zus\xE4tzlich die Obergrenze dieses Pakets."
678
+ },
600
679
  type: "number",
601
680
  default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
602
681
  validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
@@ -605,7 +684,11 @@ function maxBytesConfigField(opts) {
605
684
  function timeoutConfigField(opts) {
606
685
  return {
607
686
  key: "timeoutMs",
608
- label: "Timeout (ms)",
687
+ label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
688
+ description: {
689
+ en: "How long one attempt may take before it fails as a timeout. Counted per attempt and per redirect hop, not across the whole call.",
690
+ de: "Wie lange ein einzelner Versuch dauern darf, bevor er als Zeit\xFCberschreitung fehlschl\xE4gt. Gilt je Versuch und je Weiterleitung, nicht f\xFCr den gesamten Aufruf."
691
+ },
609
692
  type: "number",
610
693
  default: opts?.default ?? DEFAULT_TIMEOUT_MS,
611
694
  validation: { min: 100, max: opts?.max ?? MAX_TIMEOUT_MS }
@@ -615,14 +698,22 @@ function retryConfigFields(opts) {
615
698
  return [
616
699
  {
617
700
  key: "retryAttempts",
618
- label: "Retry attempts",
701
+ label: { en: "Retry attempts", de: "Wiederholungsversuche" },
702
+ description: {
703
+ en: "How often a failed attempt is repeated. 0 sends the request once and reports the first failure.",
704
+ de: "Wie oft ein fehlgeschlagener Versuch wiederholt wird. 0 sendet die Anfrage einmal und meldet den ersten Fehler."
705
+ },
619
706
  type: "number",
620
707
  default: opts?.defaultAttempts ?? DEFAULT_RETRY_ATTEMPTS,
621
708
  validation: { min: 0, max: MAX_RETRY_ATTEMPTS }
622
709
  },
623
710
  {
624
711
  key: "retryDelayMs",
625
- label: "Retry delay (ms)",
712
+ label: { en: "Retry delay (ms)", de: "Wartezeit vor Wiederholung (ms)" },
713
+ description: {
714
+ en: "How long to wait before the next attempt.",
715
+ de: "Wie lange vor dem n\xE4chsten Versuch gewartet wird."
716
+ },
626
717
  type: "number",
627
718
  default: opts?.defaultDelayMs ?? DEFAULT_RETRY_DELAY_MS,
628
719
  validation: { min: 100 }
@@ -823,11 +914,39 @@ var SimpleValueCredential = class extends BaseCredential {
823
914
  }
824
915
  };
825
916
  var ApiKeyCredential = class extends BaseCredential {
917
+ /**
918
+ * Which field of the credential form the key is typed into. Override it to
919
+ * name the form field this provider calls its key — `botToken`, `authKey`,
920
+ * `webhookUrl`.
921
+ *
922
+ * **What this does not do** (PO-497): it does not decide what a node looks
923
+ * the key up under at run time. It used to do only half of that — it changed
924
+ * which field was *read* while {@link credentialShape} went on emitting
925
+ * `apiKey` — so a credential that renamed its form field alone handed the
926
+ * node a key under a name it was never told to expect, and the node refused
927
+ * a perfectly good key as a missing one. The default shape now follows this
928
+ * name, so overriding this alone is correct on both sides. Overriding
929
+ * {@link credentialShape} as well is still how a credential emits a
930
+ * different name, or more than the key.
931
+ *
932
+ * Note that no node test can see a mismatch between the two: a mocked node
933
+ * context is seeded with the resolved blob directly and never calls
934
+ * `resolve`. Only a credential test asserting the whole resolved shape
935
+ * catches it, and asserting it loosely still misses — a partial match is
936
+ * happy with the wrong key present beside the right one.
937
+ */
826
938
  apiKeyField() {
827
939
  return "apiKey";
828
940
  }
941
+ /**
942
+ * What the node receives. Defaults to the key under the name
943
+ * {@link apiKeyField} returns, so the two cannot drift apart by default.
944
+ *
945
+ * Override it where the node needs a different name than the form uses, or
946
+ * needs more than the key — a base URL derived from it, say.
947
+ */
829
948
  credentialShape(apiKey) {
830
- return { apiKey };
949
+ return { [this.apiKeyField()]: apiKey };
831
950
  }
832
951
  async resolve(_ctx, config, _durableCreds) {
833
952
  const apiKey = requireString(config, this.apiKeyField(), this.description.slug);
@@ -980,11 +1099,14 @@ function base64Url(buf) {
980
1099
  backoffDelay,
981
1100
  buildManifest,
982
1101
  clampResponseBytes,
1102
+ errorPort,
1103
+ errorResult,
983
1104
  evaluate,
984
1105
  extractCredentialManifest,
985
1106
  extractCredentialManifests,
986
1107
  extractManifest,
987
1108
  extractManifests,
1109
+ httpErrorResult,
988
1110
  isBlockedAddress,
989
1111
  isNodeWithIteration,
990
1112
  isOAuthAuthorizeCredential,
@@ -1002,5 +1124,7 @@ function base64Url(buf) {
1002
1124
  sleepWithSignal,
1003
1125
  takesValue,
1004
1126
  timeoutConfigField,
1127
+ toErrorOutput,
1128
+ toErrorResult,
1005
1129
  withRetry
1006
1130
  });
package/dist/index.d.cts CHANGED
@@ -650,6 +650,67 @@ declare class NodeError extends Error {
650
650
  constructor(code: string, message: string, meta?: Record<string, unknown>);
651
651
  }
652
652
 
653
+ /**
654
+ * Canonical error-output shape emitted on a node's `error` port. It lives here
655
+ * rather than in a node package because all four of them were building it
656
+ * separately — `integrations-nodes-core` in an internal module, `deepl` in a
657
+ * copy of that module, Business Central in `bc-fetch.ts` and Pipedrive in
658
+ * `pipedrive-client.ts` — so the workflow editor's guarantee of one
659
+ * `{ code, message, status }` payload rested on four files agreeing by hand
660
+ * (PO-442; asked for by PO-134 in June and not done then).
661
+ */
662
+ interface NodeErrorOutput {
663
+ /** Machine-readable error code, e.g. `HTTP_ERROR`, `TIMEOUT`, `REQUEST_FAILED`. */
664
+ code: string;
665
+ message: string;
666
+ /** HTTP status code, or `0` for timeouts / network / non-HTTP errors. */
667
+ status: number;
668
+ /** Optional response body — HTTP-specific. */
669
+ body?: unknown;
670
+ }
671
+ /**
672
+ * Normalize any caught value into the canonical {@link NodeErrorOutput} shape.
673
+ * Use this in a node's `catch` block before routing to the `error` port.
674
+ */
675
+ declare function toErrorOutput(err: unknown): NodeErrorOutput;
676
+ /** Wrap an error output into an `INodeResult` routed to the `error` port. */
677
+ declare function errorResult(output: NodeErrorOutput, port?: string): INodeResult;
678
+ /**
679
+ * Catch-block shortcut: normalize any thrown value and route it to the failure
680
+ * output. The one helper every node's `catch` funnels into, so the
681
+ * `{ code, message, status }` mapping lives in a single place instead of being
682
+ * re-implemented per protocol (FTP/SFTP) or per package.
683
+ *
684
+ * `port` is the output taken, `error` unless the node declares its failure
685
+ * output under another name (`{ ...errorPort(), name: 'failed' }`) — pass the
686
+ * same name here, or the branch names a port that node does not have.
687
+ */
688
+ declare function toErrorResult(err: unknown, port?: string): INodeResult;
689
+ /**
690
+ * Catch-block shortcut for the HTTP-style nodes whose failure output additionally
691
+ * declares `body`: the canonical `{ code, message, status }` mapping plus the
692
+ * response body a {@link NodeError} carries in `meta.body` (`null` when the
693
+ * failure never reached a response). `port` names the output as above.
694
+ */
695
+ declare function httpErrorResult(err: unknown, port?: string): INodeResult;
696
+ /**
697
+ * Shared `error` output port — the uniform `{ code, message, status }` triple
698
+ * declared on every node that has an `error` branch. Pass `body: true` for
699
+ * HTTP-style nodes that additionally emit a response body on the port.
700
+ *
701
+ * The sentences travel with the port. The three values mean the same thing
702
+ * behind every node, so they are written here once instead of per node; a node
703
+ * whose failures are worth naming more precisely spreads the port and replaces
704
+ * only its `description` (`{ ...errorPort(), description: … }`).
705
+ *
706
+ * The second sentence of the description is the rule this port exists under,
707
+ * and it is promised in `specs/error-handling.md`: a fault in the author's own
708
+ * configuration ends the run, a fault met while running is offered here.
709
+ */
710
+ declare function errorPort(opts?: {
711
+ body?: boolean;
712
+ }): IOutputPort;
713
+
653
714
  declare function extractManifest(node: INode): INodeDescription;
654
715
  declare function extractManifests(nodes: INode[]): INodeDescription[];
655
716
  declare function extractCredentialManifest(credential: ICredential): ICredentialDescription;
@@ -710,14 +771,45 @@ declare function readText(res: Response, maxBytes?: number): Promise<string>;
710
771
  * rather than a raw `SyntaxError`, keeping to the SDK error contract.
711
772
  */
712
773
  declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
774
+ /**
775
+ * The setting a node offers its workflow author for the response size cap,
776
+ * ready to spread into `description.config`.
777
+ *
778
+ * **What the node still has to do** (PO-497): declaring the setting does not
779
+ * wire it up. Read the configured value in `execute` and pass it on — the
780
+ * helpers here only produce the declaration, and a node that spreads this and
781
+ * never reads `config.maxBytes` offers a setting that does nothing.
782
+ *
783
+ * Everything an author reads is supplied in both languages the node packages
784
+ * write, so spreading this unchanged already satisfies the rule they hold.
785
+ * Nothing is left to the caller on that side: a node that wants different
786
+ * words replaces the field rather than completing it.
787
+ */
713
788
  declare function maxBytesConfigField(opts?: {
714
789
  default?: number;
715
790
  max?: number;
716
791
  }): IConfigField;
792
+ /**
793
+ * The setting a node offers its workflow author for the request budget, ready
794
+ * to spread into `description.config`.
795
+ *
796
+ * **What the node still has to do** (PO-497): pass the configured value to
797
+ * `safeFetch` as `timeoutMs`. See {@link maxBytesConfigField} for the same note
798
+ * and for why the label and description are not the caller's to complete.
799
+ */
717
800
  declare function timeoutConfigField(opts?: {
718
801
  default?: number;
719
802
  max?: number;
720
803
  }): IConfigField;
804
+ /**
805
+ * The two settings a node offers its workflow author for the retry budget,
806
+ * ready to spread into `description.config`.
807
+ *
808
+ * **What the node still has to do** (PO-497): pass both configured values to
809
+ * `safeFetch` as `retry.attempts` and `retry.delayMs`. See
810
+ * {@link maxBytesConfigField} for the same note and for why the labels and
811
+ * descriptions are not the caller's to complete.
812
+ */
721
813
  declare function retryConfigFields(opts?: {
722
814
  defaultAttempts?: number;
723
815
  defaultDelayMs?: number;
@@ -982,11 +1074,40 @@ declare abstract class SimpleValueCredential extends BaseCredential {
982
1074
  resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
983
1075
  }
984
1076
  /**
985
- * API-key credential. By default exposes the configured key under
986
- * `{ apiKey }`; override {@link apiKeyField}/{@link credentialShape} to map.
1077
+ * API-key credential. Reads the key from the configured field and hands it to
1078
+ * the node under that same name; override {@link credentialShape} where the
1079
+ * node needs something else, or more than the key alone.
987
1080
  */
988
1081
  declare abstract class ApiKeyCredential extends BaseCredential {
1082
+ /**
1083
+ * Which field of the credential form the key is typed into. Override it to
1084
+ * name the form field this provider calls its key — `botToken`, `authKey`,
1085
+ * `webhookUrl`.
1086
+ *
1087
+ * **What this does not do** (PO-497): it does not decide what a node looks
1088
+ * the key up under at run time. It used to do only half of that — it changed
1089
+ * which field was *read* while {@link credentialShape} went on emitting
1090
+ * `apiKey` — so a credential that renamed its form field alone handed the
1091
+ * node a key under a name it was never told to expect, and the node refused
1092
+ * a perfectly good key as a missing one. The default shape now follows this
1093
+ * name, so overriding this alone is correct on both sides. Overriding
1094
+ * {@link credentialShape} as well is still how a credential emits a
1095
+ * different name, or more than the key.
1096
+ *
1097
+ * Note that no node test can see a mismatch between the two: a mocked node
1098
+ * context is seeded with the resolved blob directly and never calls
1099
+ * `resolve`. Only a credential test asserting the whole resolved shape
1100
+ * catches it, and asserting it loosely still misses — a partial match is
1101
+ * happy with the wrong key present beside the right one.
1102
+ */
989
1103
  protected apiKeyField(): string;
1104
+ /**
1105
+ * What the node receives. Defaults to the key under the name
1106
+ * {@link apiKeyField} returns, so the two cannot drift apart by default.
1107
+ *
1108
+ * Override it where the node needs a different name than the form uses, or
1109
+ * needs more than the key — a base URL derived from it, say.
1110
+ */
990
1111
  protected credentialShape(apiKey: string): Record<string, unknown>;
991
1112
  resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
992
1113
  }
@@ -1046,4 +1167,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
1046
1167
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
1047
1168
  }
1048
1169
 
1049
- 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 INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, withRetry };
1170
+ 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 INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeErrorOutput, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, errorPort, errorResult, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, httpErrorResult, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, toErrorOutput, toErrorResult, withRetry };
package/dist/index.d.ts CHANGED
@@ -650,6 +650,67 @@ declare class NodeError extends Error {
650
650
  constructor(code: string, message: string, meta?: Record<string, unknown>);
651
651
  }
652
652
 
653
+ /**
654
+ * Canonical error-output shape emitted on a node's `error` port. It lives here
655
+ * rather than in a node package because all four of them were building it
656
+ * separately — `integrations-nodes-core` in an internal module, `deepl` in a
657
+ * copy of that module, Business Central in `bc-fetch.ts` and Pipedrive in
658
+ * `pipedrive-client.ts` — so the workflow editor's guarantee of one
659
+ * `{ code, message, status }` payload rested on four files agreeing by hand
660
+ * (PO-442; asked for by PO-134 in June and not done then).
661
+ */
662
+ interface NodeErrorOutput {
663
+ /** Machine-readable error code, e.g. `HTTP_ERROR`, `TIMEOUT`, `REQUEST_FAILED`. */
664
+ code: string;
665
+ message: string;
666
+ /** HTTP status code, or `0` for timeouts / network / non-HTTP errors. */
667
+ status: number;
668
+ /** Optional response body — HTTP-specific. */
669
+ body?: unknown;
670
+ }
671
+ /**
672
+ * Normalize any caught value into the canonical {@link NodeErrorOutput} shape.
673
+ * Use this in a node's `catch` block before routing to the `error` port.
674
+ */
675
+ declare function toErrorOutput(err: unknown): NodeErrorOutput;
676
+ /** Wrap an error output into an `INodeResult` routed to the `error` port. */
677
+ declare function errorResult(output: NodeErrorOutput, port?: string): INodeResult;
678
+ /**
679
+ * Catch-block shortcut: normalize any thrown value and route it to the failure
680
+ * output. The one helper every node's `catch` funnels into, so the
681
+ * `{ code, message, status }` mapping lives in a single place instead of being
682
+ * re-implemented per protocol (FTP/SFTP) or per package.
683
+ *
684
+ * `port` is the output taken, `error` unless the node declares its failure
685
+ * output under another name (`{ ...errorPort(), name: 'failed' }`) — pass the
686
+ * same name here, or the branch names a port that node does not have.
687
+ */
688
+ declare function toErrorResult(err: unknown, port?: string): INodeResult;
689
+ /**
690
+ * Catch-block shortcut for the HTTP-style nodes whose failure output additionally
691
+ * declares `body`: the canonical `{ code, message, status }` mapping plus the
692
+ * response body a {@link NodeError} carries in `meta.body` (`null` when the
693
+ * failure never reached a response). `port` names the output as above.
694
+ */
695
+ declare function httpErrorResult(err: unknown, port?: string): INodeResult;
696
+ /**
697
+ * Shared `error` output port — the uniform `{ code, message, status }` triple
698
+ * declared on every node that has an `error` branch. Pass `body: true` for
699
+ * HTTP-style nodes that additionally emit a response body on the port.
700
+ *
701
+ * The sentences travel with the port. The three values mean the same thing
702
+ * behind every node, so they are written here once instead of per node; a node
703
+ * whose failures are worth naming more precisely spreads the port and replaces
704
+ * only its `description` (`{ ...errorPort(), description: … }`).
705
+ *
706
+ * The second sentence of the description is the rule this port exists under,
707
+ * and it is promised in `specs/error-handling.md`: a fault in the author's own
708
+ * configuration ends the run, a fault met while running is offered here.
709
+ */
710
+ declare function errorPort(opts?: {
711
+ body?: boolean;
712
+ }): IOutputPort;
713
+
653
714
  declare function extractManifest(node: INode): INodeDescription;
654
715
  declare function extractManifests(nodes: INode[]): INodeDescription[];
655
716
  declare function extractCredentialManifest(credential: ICredential): ICredentialDescription;
@@ -710,14 +771,45 @@ declare function readText(res: Response, maxBytes?: number): Promise<string>;
710
771
  * rather than a raw `SyntaxError`, keeping to the SDK error contract.
711
772
  */
712
773
  declare function readJsonOrText(res: Response, maxBytes?: number): Promise<unknown>;
774
+ /**
775
+ * The setting a node offers its workflow author for the response size cap,
776
+ * ready to spread into `description.config`.
777
+ *
778
+ * **What the node still has to do** (PO-497): declaring the setting does not
779
+ * wire it up. Read the configured value in `execute` and pass it on — the
780
+ * helpers here only produce the declaration, and a node that spreads this and
781
+ * never reads `config.maxBytes` offers a setting that does nothing.
782
+ *
783
+ * Everything an author reads is supplied in both languages the node packages
784
+ * write, so spreading this unchanged already satisfies the rule they hold.
785
+ * Nothing is left to the caller on that side: a node that wants different
786
+ * words replaces the field rather than completing it.
787
+ */
713
788
  declare function maxBytesConfigField(opts?: {
714
789
  default?: number;
715
790
  max?: number;
716
791
  }): IConfigField;
792
+ /**
793
+ * The setting a node offers its workflow author for the request budget, ready
794
+ * to spread into `description.config`.
795
+ *
796
+ * **What the node still has to do** (PO-497): pass the configured value to
797
+ * `safeFetch` as `timeoutMs`. See {@link maxBytesConfigField} for the same note
798
+ * and for why the label and description are not the caller's to complete.
799
+ */
717
800
  declare function timeoutConfigField(opts?: {
718
801
  default?: number;
719
802
  max?: number;
720
803
  }): IConfigField;
804
+ /**
805
+ * The two settings a node offers its workflow author for the retry budget,
806
+ * ready to spread into `description.config`.
807
+ *
808
+ * **What the node still has to do** (PO-497): pass both configured values to
809
+ * `safeFetch` as `retry.attempts` and `retry.delayMs`. See
810
+ * {@link maxBytesConfigField} for the same note and for why the labels and
811
+ * descriptions are not the caller's to complete.
812
+ */
721
813
  declare function retryConfigFields(opts?: {
722
814
  defaultAttempts?: number;
723
815
  defaultDelayMs?: number;
@@ -982,11 +1074,40 @@ declare abstract class SimpleValueCredential extends BaseCredential {
982
1074
  resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
983
1075
  }
984
1076
  /**
985
- * API-key credential. By default exposes the configured key under
986
- * `{ apiKey }`; override {@link apiKeyField}/{@link credentialShape} to map.
1077
+ * API-key credential. Reads the key from the configured field and hands it to
1078
+ * the node under that same name; override {@link credentialShape} where the
1079
+ * node needs something else, or more than the key alone.
987
1080
  */
988
1081
  declare abstract class ApiKeyCredential extends BaseCredential {
1082
+ /**
1083
+ * Which field of the credential form the key is typed into. Override it to
1084
+ * name the form field this provider calls its key — `botToken`, `authKey`,
1085
+ * `webhookUrl`.
1086
+ *
1087
+ * **What this does not do** (PO-497): it does not decide what a node looks
1088
+ * the key up under at run time. It used to do only half of that — it changed
1089
+ * which field was *read* while {@link credentialShape} went on emitting
1090
+ * `apiKey` — so a credential that renamed its form field alone handed the
1091
+ * node a key under a name it was never told to expect, and the node refused
1092
+ * a perfectly good key as a missing one. The default shape now follows this
1093
+ * name, so overriding this alone is correct on both sides. Overriding
1094
+ * {@link credentialShape} as well is still how a credential emits a
1095
+ * different name, or more than the key.
1096
+ *
1097
+ * Note that no node test can see a mismatch between the two: a mocked node
1098
+ * context is seeded with the resolved blob directly and never calls
1099
+ * `resolve`. Only a credential test asserting the whole resolved shape
1100
+ * catches it, and asserting it loosely still misses — a partial match is
1101
+ * happy with the wrong key present beside the right one.
1102
+ */
989
1103
  protected apiKeyField(): string;
1104
+ /**
1105
+ * What the node receives. Defaults to the key under the name
1106
+ * {@link apiKeyField} returns, so the two cannot drift apart by default.
1107
+ *
1108
+ * Override it where the node needs a different name than the form uses, or
1109
+ * needs more than the key — a base URL derived from it, say.
1110
+ */
990
1111
  protected credentialShape(apiKey: string): Record<string, unknown>;
991
1112
  resolve(_ctx: ICredentialContext, config: Config, _durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
992
1113
  }
@@ -1046,4 +1167,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
1046
1167
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
1047
1168
  }
1048
1169
 
1049
- 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 INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, withRetry };
1170
+ 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 INodeState, type INodeWithIteration, type IOutputField, type IOutputPort, type IShowIfCondition, type ITemplateDescription, type ITemplateTrigger, type ImageCategory, type LocalizedString, type LookupAddress, type LookupFn, MANIFEST_VERSION, MAX_REDIRECTS, MAX_RESPONSE_BYTES, MAX_RETRY_ATTEMPTS, MAX_TIMEOUT_MS, type NodeCategory, NodeError, type NodeErrorOutput, type NodeManifest, type NodePackageMeta, OAuth2AuthCodeCredential, OAuth2ClientCredentialsCredential, type OAuthTokenResponse, OPERATORS, type Operator, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, VALUELESS_OPERATORS, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, errorPort, errorResult, evaluate, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, httpErrorResult, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, isOperator, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, settingApplies, sleepWithSignal, takesValue, timeoutConfigField, toErrorOutput, toErrorResult, withRetry };
package/dist/index.js CHANGED
@@ -131,6 +131,76 @@ var NodeError = class extends Error {
131
131
  }
132
132
  };
133
133
 
134
+ // src/nodeErrors.ts
135
+ function toErrorOutput(err) {
136
+ if (err instanceof NodeError) {
137
+ const raw = err.meta?.["status"];
138
+ const status = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
139
+ return { code: err.code, message: err.message, status };
140
+ }
141
+ if (err instanceof Error && err.name === "AbortError") {
142
+ return { code: "TIMEOUT", message: "Request timed out", status: 0 };
143
+ }
144
+ if (err instanceof Error) {
145
+ return { code: "REQUEST_FAILED", message: err.message, status: 0 };
146
+ }
147
+ return { code: "REQUEST_FAILED", message: String(err), status: 0 };
148
+ }
149
+ function errorResult(output, port = "error") {
150
+ return { outputs: { [port]: output }, branch: port };
151
+ }
152
+ function toErrorResult(err, port = "error") {
153
+ return errorResult(toErrorOutput(err), port);
154
+ }
155
+ function httpErrorResult(err, port = "error") {
156
+ const body = err instanceof NodeError ? err.meta?.["body"] ?? null : null;
157
+ return errorResult({ ...toErrorOutput(err), body }, port);
158
+ }
159
+ function errorPort(opts = {}) {
160
+ return {
161
+ name: "error",
162
+ kind: "error",
163
+ dataType: "object",
164
+ label: { en: "Error", de: "Fehler" },
165
+ description: {
166
+ en: "Taken when the attempt fails in a way the workflow can carry on from. A node that cannot run as configured fails the run instead of taking this path.",
167
+ de: "Wird genommen, wenn der Versuch auf eine Weise fehlschl\xE4gt, mit der der Workflow weiterarbeiten kann. Ein Node, der so konfiguriert nicht laufen kann, l\xE4sst den Run scheitern, statt diesen Pfad zu nehmen."
168
+ },
169
+ fields: {
170
+ code: {
171
+ dataType: "string",
172
+ description: {
173
+ en: "Machine-readable name of the failure \u2014 branch on this rather than on the message. A node that talks to a remote system shares `TIMEOUT` and `REQUEST_FAILED` with every other one; the remaining codes belong to this node alone.",
174
+ de: "Maschinenlesbarer Name des Fehlers \u2014 hierauf verzweigen, nicht auf die Meldung. Ein Node, der mit einem Fremdsystem spricht, teilt `TIMEOUT` und `REQUEST_FAILED` mit allen anderen; die \xFCbrigen Codes geh\xF6ren allein zu diesem Node."
175
+ }
176
+ },
177
+ message: {
178
+ dataType: "string",
179
+ description: {
180
+ en: "Wording for whoever reads the run afterwards, frequently the wording the far end used. It is not a stable value to branch on.",
181
+ de: "Wortlaut f\xFCr den, der den Run sp\xE4ter liest, h\xE4ufig der des Gegen\xFCbers. Kein stabiler Wert, auf den sich Verzweigungen st\xFCtzen sollten."
182
+ }
183
+ },
184
+ status: {
185
+ dataType: "number",
186
+ description: {
187
+ en: "How the far end answered, as an HTTP status code, where one answered with a status at all. `0` otherwise \u2014 a timeout, a refused connection, an input the node could not use, a node that never leaves the workflow, or a failure the far end reported in its own answer rather than in a status, as a bulk job that ends as failed does. So `0` means there is no status to read, not that nothing was reached.",
188
+ de: "Wie das Gegen\xFCber geantwortet hat, als HTTP-Statuscode \u2014 sofern eines \xFCberhaupt mit einem Status geantwortet hat. Sonst `0`: Timeout, abgelehnte Verbindung, eine unbrauchbare Eingabe, ein Node, der den Workflow nicht verl\xE4sst, oder ein Fehler, den das Gegen\xFCber in seiner Antwort statt im Status gemeldet hat \u2014 wie ein Bulk-Job, der als fehlgeschlagen endet. `0` hei\xDFt also: kein Status zu lesen, nicht: nichts erreicht."
189
+ }
190
+ },
191
+ ...opts.body ? {
192
+ body: {
193
+ dataType: "any",
194
+ description: {
195
+ en: "The answer the far end gave, where the failure got far enough to have one: the response body, or the job the platform reported as failed.",
196
+ de: "Die Antwort des Gegen\xFCbers, sofern der Fehler weit genug kam, um eine zu haben: der Response-Body oder der von der Plattform als fehlgeschlagen gemeldete Job."
197
+ }
198
+ }
199
+ } : {}
200
+ }
201
+ };
202
+ }
203
+
134
204
  // src/ssrf.ts
135
205
  import { subscribe } from "diagnostics_channel";
136
206
  import { isIP } from "net";
@@ -510,7 +580,11 @@ async function readJsonOrText(res, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
510
580
  function maxBytesConfigField(opts) {
511
581
  return {
512
582
  key: "maxBytes",
513
- label: "Max response size (bytes)",
583
+ label: { en: "Max response size (bytes)", de: "Maximale Antwortgr\xF6\xDFe (Bytes)" },
584
+ description: {
585
+ en: "Answers larger than this are refused instead of read. The limit this package enforces applies above whatever is set here.",
586
+ de: "Gr\xF6\xDFere Antworten werden abgelehnt statt gelesen. \xDCber dem hier gesetzten Wert gilt zus\xE4tzlich die Obergrenze dieses Pakets."
587
+ },
514
588
  type: "number",
515
589
  default: opts?.default ?? DEFAULT_MAX_RESPONSE_BYTES,
516
590
  validation: { min: 1, max: Math.min(opts?.max ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES) }
@@ -519,7 +593,11 @@ function maxBytesConfigField(opts) {
519
593
  function timeoutConfigField(opts) {
520
594
  return {
521
595
  key: "timeoutMs",
522
- label: "Timeout (ms)",
596
+ label: { en: "Timeout (ms)", de: "Zeitlimit (ms)" },
597
+ description: {
598
+ en: "How long one attempt may take before it fails as a timeout. Counted per attempt and per redirect hop, not across the whole call.",
599
+ de: "Wie lange ein einzelner Versuch dauern darf, bevor er als Zeit\xFCberschreitung fehlschl\xE4gt. Gilt je Versuch und je Weiterleitung, nicht f\xFCr den gesamten Aufruf."
600
+ },
523
601
  type: "number",
524
602
  default: opts?.default ?? DEFAULT_TIMEOUT_MS,
525
603
  validation: { min: 100, max: opts?.max ?? MAX_TIMEOUT_MS }
@@ -529,14 +607,22 @@ function retryConfigFields(opts) {
529
607
  return [
530
608
  {
531
609
  key: "retryAttempts",
532
- label: "Retry attempts",
610
+ label: { en: "Retry attempts", de: "Wiederholungsversuche" },
611
+ description: {
612
+ en: "How often a failed attempt is repeated. 0 sends the request once and reports the first failure.",
613
+ de: "Wie oft ein fehlgeschlagener Versuch wiederholt wird. 0 sendet die Anfrage einmal und meldet den ersten Fehler."
614
+ },
533
615
  type: "number",
534
616
  default: opts?.defaultAttempts ?? DEFAULT_RETRY_ATTEMPTS,
535
617
  validation: { min: 0, max: MAX_RETRY_ATTEMPTS }
536
618
  },
537
619
  {
538
620
  key: "retryDelayMs",
539
- label: "Retry delay (ms)",
621
+ label: { en: "Retry delay (ms)", de: "Wartezeit vor Wiederholung (ms)" },
622
+ description: {
623
+ en: "How long to wait before the next attempt.",
624
+ de: "Wie lange vor dem n\xE4chsten Versuch gewartet wird."
625
+ },
540
626
  type: "number",
541
627
  default: opts?.defaultDelayMs ?? DEFAULT_RETRY_DELAY_MS,
542
628
  validation: { min: 100 }
@@ -710,11 +796,39 @@ var SimpleValueCredential = class extends BaseCredential {
710
796
  }
711
797
  };
712
798
  var ApiKeyCredential = class extends BaseCredential {
799
+ /**
800
+ * Which field of the credential form the key is typed into. Override it to
801
+ * name the form field this provider calls its key — `botToken`, `authKey`,
802
+ * `webhookUrl`.
803
+ *
804
+ * **What this does not do** (PO-497): it does not decide what a node looks
805
+ * the key up under at run time. It used to do only half of that — it changed
806
+ * which field was *read* while {@link credentialShape} went on emitting
807
+ * `apiKey` — so a credential that renamed its form field alone handed the
808
+ * node a key under a name it was never told to expect, and the node refused
809
+ * a perfectly good key as a missing one. The default shape now follows this
810
+ * name, so overriding this alone is correct on both sides. Overriding
811
+ * {@link credentialShape} as well is still how a credential emits a
812
+ * different name, or more than the key.
813
+ *
814
+ * Note that no node test can see a mismatch between the two: a mocked node
815
+ * context is seeded with the resolved blob directly and never calls
816
+ * `resolve`. Only a credential test asserting the whole resolved shape
817
+ * catches it, and asserting it loosely still misses — a partial match is
818
+ * happy with the wrong key present beside the right one.
819
+ */
713
820
  apiKeyField() {
714
821
  return "apiKey";
715
822
  }
823
+ /**
824
+ * What the node receives. Defaults to the key under the name
825
+ * {@link apiKeyField} returns, so the two cannot drift apart by default.
826
+ *
827
+ * Override it where the node needs a different name than the form uses, or
828
+ * needs more than the key — a base URL derived from it, say.
829
+ */
716
830
  credentialShape(apiKey) {
717
- return { apiKey };
831
+ return { [this.apiKeyField()]: apiKey };
718
832
  }
719
833
  async resolve(_ctx, config, _durableCreds) {
720
834
  const apiKey = requireString(config, this.apiKeyField(), this.description.slug);
@@ -866,11 +980,14 @@ export {
866
980
  backoffDelay,
867
981
  buildManifest,
868
982
  clampResponseBytes,
983
+ errorPort,
984
+ errorResult,
869
985
  evaluate,
870
986
  extractCredentialManifest,
871
987
  extractCredentialManifests,
872
988
  extractManifest,
873
989
  extractManifests,
990
+ httpErrorResult,
874
991
  isBlockedAddress,
875
992
  isNodeWithIteration,
876
993
  isOAuthAuthorizeCredential,
@@ -888,5 +1005,7 @@ export {
888
1005
  sleepWithSignal,
889
1006
  takesValue,
890
1007
  timeoutConfigField,
1008
+ toErrorOutput,
1009
+ toErrorResult,
891
1010
  withRetry
892
1011
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",